Search code examples
scalafunctional-programming

Implementing a Monad trait in scala with map as a derived primitive, not satisfying the criteria for use with for-comprehension


I am in the process of building my own little functional programming library for educational purposes in scala 2.13, and while doing so, I am having trouble meeting the requirement for the map operation for use in for-comprehensions.

Here, I am implementing map as a derived primitive using a combination of flatMap and pure, so that I don't have to provide an implementation for it each time I define a monad instance, but somehow, the compiler is not happy with that:


trait Monad[F[_]] {
  def pure[A](a: A): F[A]

  def flatMap[A, B](fa: F[A])(g: A => F[B]): F[B]

  def map[A, B](fa: F[A])(f: A => B): F[B] =
    flatMap(fa)(f andThen pure)
}

case class Id[A](get: A)

object Monad {
  val idMonad = new Monad[Id] {
    def pure[A](a: A) = Id(a)
    def flatMap[A,B](ma: Id[A])(f: A => Id[B]) =
      f(ma.get)
  }

  val example: Id[Unit] = for {
    _ <- Id(())
  } yield ()

Compiler error:

[error] value map is not a member of fp.Id[Unit]
[error]     _ <- Id(())
[error]          ^^^^^^
Error compiling project (Scala 2.13.13, JVM (17))

scala version: 2.13.13

Does anyone see any blatant mistake in what I've done that would not accept my definition for map?

I have defined map as a method to the monad instance Id, which enables the use of for-comprehension, but completely voids my intent of not having to provide an implementation for map, given that pure and flatMap are implemented.

case class Id[A](get: A) {
  def flatMap[B](f: A => Id[B]): Id[B] = f(get)
  // Adding map here will satisfy the requirement for for-comprehensions.
  def map[B](f: A => B): Id[B] = Id(f(get))
}

My goal is to have derived primitives such as map, as all Monads are Functors.


Solution

  • As mentioned in a comment, you did not implement the map method on the Id class. Notice furthermore that while your map method takes an instance and two type parameters, for a map method call to be synthesized by the compiler you would rather need a map method defined on an instance.

    What you have done so far is implementing part of the "type class pattern" in Scala. A common mechanism used to "bridge" between the type class instance and object instances is by using an implicit class, as well as adding your type class instance in the implicit scope, like in the following example:

    trait Monad[F[_]] {
      def pure[A](a: A): F[A]
    
      def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
    
      def map[A, B](fa: F[A])(f: A => B): F[B] =
        flatMap(fa)(f andThen pure)
    }
    
    case class Id[A](get: A)
    
    object Monad {
    
      implicit class MonadOps[F[_], A](fa: F[A])(implicit val M: Monad[F]) {
        def flatMap[B](f: A => F[B]): F[B] = M.flatMap[A, B](fa)(f)
        def map[B](f: A => B): F[B] = M.map[A, B](fa)(f)
      }
    
      implicit val idMonad: Monad[Id] = new Monad[Id] {
        def pure[A](a: A): Id[A] = Id(a)
        def flatMap[A, B](ma: Id[A])(f: A => Id[B]): Id[B] =
          f(ma.get)
      }
    }
    
    import Monad._
    
    val fourtytwo = for {
      twentyone <- Id(21)
    } yield twentyone * 2
    
    assert(fourtytwo.get == 42)
    

    You can play around with this code here on Scastie.

    For more information on implicit classes, you can read this article on the Scala website.

    You can read more about type classes on this article on the Typelevel blog.

    Notice that this answer is valid for Scala 2. Scala 3 evolved everything revolving around implicits, including implicit classes. The introduction of extension methods to take their place lead to a few changes in how you work with type classes. If you're interested in type classes in Scala 3 the Scala website has an article about them.