Harmeet Singh Taara
Harmeet Singh Taara

Reputation: 6611

Scala: Create custom OptionT monad from cats for learning

we are creating our own OptionT of cats for understanding, how monads are work and monads transformation flow. While creating our own custom monad getting some of the errors. First thing below is our code:

case class WhateverOpt[W[_], A] (value: W[Option[A]]) {

    def map[B] (f: A => B) (implicit M: Monad[W]): WhateverOpt[W, B] =
      WhateverOpt(M.map(value)(_.map(f)))

    def flatMap[B] (f: A => WhateverOpt[W, B]) (implicit M: Monad[W]): WhateverOpt[W, B] =
      WhateverOpt(M.flatMap(value)(optA => optA match {
        case Some(v) => f(v).value
      }))
  }

  implicit val optionTMonad = new Monad[Option] {

    override def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
    override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f)
  }

  val optionResult = for {
    user <- WhateverOpt(repository.getUserOption(1))
    addres <- WhateverOpt(repository.getAddressOption(user))
  } yield addres.city

Below are the points, where we stuck:

  1. How to handle None case in WhateverOpt flatMap method?
  2. When executing the code, getting runtime error: Error:(26, 12) could not find implicit value for parameter M: usercases.mtransfomer.Monad[scala.concurrent.Future] addres <- WhateverOpt(repository.getAddressOption(user))

We are not sure about the error because we are creating optionTMonad implicit and by default, all are in the same scope. How can we resolve these two issues?

Update

Full code is available on Github branch https://github.com/harmeetsingh0013/fp_scala/blob/master/src/main/scala/usercases/mtransfomer/Example5.scala

Upvotes: 2

Views: 470

Answers (3)

Gabriele Petronella
Gabriele Petronella

Reputation: 108111

How to handle None case in WhateverOpt flatMap method?

When you flatMap over None you return None. When you flatMap over WhateverOpt[W[_], B] you want to return the pure of it, which in your code would be M.pure(None).

When executing the code, getting runtime error: Error:(26, 12) could not find implicit value for parameter M: usercases.mtransfomer.Monad[scala.concurrent.Future] addres <- WhateverOpt(repository.getAddressOption(user))

That's a compile-time error (not a runtime one) and it's due to the missing instance of Monad[Future]. In order to get an instance of Monad[Future] in scope with cats, you can do:

import cats.instances.future._
import scala.concurrent.ExecutionContext.Implicits.global

Also, you can avoid declaring your own Monad[Option] by importing it from cats with

import cats.instances.option._

Upvotes: 1

Harmeet Singh Taara
Harmeet Singh Taara

Reputation: 6611

How to handle None case in WhateverOpt flatMap method?

This answer is already explained by @Gabriele Petronella and @Andrey Tyukin with details.

When executing the code, getting runtime error: Error:(26, 12) could not find implicit value for parameter M: usercases.mtransfomer.Monad[scala.concurrent.Future] addres <- WhateverOpt(repository.getAddressOption(user))

This error occurs, because In WhateverOpt constructor we know that our value is W[Option[A]], where Option is already defined and handle by the code, but repository.getUserOption(1) return Future[Option[User]] where Future is handled by generic parameter W and in that, case, we need to define, how to handle monads for Future. For resolving that issue, we need to implement new Monad[Future] rather than, new Monad[Option] as below:

case class WhateverOpt[W[_], A] (value: W[Option[A]]) {

    def map[B] (f: A => B) (implicit M: Monad[W]): WhateverOpt[W, B] =
      WhateverOpt(M.map(value)(_.map(f)))

    def flatMap[B] (f: A => WhateverOpt[W, B]) (implicit M: Monad[W]): WhateverOpt[W, B] =
      WhateverOpt(M.flatMap(value)(optA => optA match {
        case Some(v) => f(v).value
        case None => M.pure(None)
      }))
  }

  implicit val futureMonad = new Monad[Future] {
    override def pure[A](a: A): Future[A] = Future.successful(a)

    override def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)

    override def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f)
  }

  val optionResult: WhateverOpt[Future, String] = for {
    user <- WhateverOpt(repository.getUserOption(1))
    addres <- WhateverOpt(repository.getAddressOption(user))
  } yield addres.city

I am not sure about my disription, which I mention in answere, but current my assumptions are this and for me above code is working fine. For a complete example, please click on the GitHub repo, which is mention in the question.

Upvotes: 1

Andrey Tyukin
Andrey Tyukin

Reputation: 44918

About how to deal with None:

case class WhateverOpt[W[_], A] (value: W[Option[A]]) {

  def map[B] (f: A => B) (implicit M: Monad[W]): WhateverOpt[W, B] =
    WhateverOpt(M.map(value)(_.map(f)))

  def flatMap[B] 
    (f: A => WhateverOpt[W, B])
    (implicit wMonad: Monad[W])
  : WhateverOpt[W, B] = {
    WhateverOpt(wMonad.flatMap(value) { (oa: Option[A]) => 
      oa match {
        case None => wMonad.pure(None)
        case Some(a) => f(a).value
      }
    })
  }
}

Imagine for a second that W is Future. Then the above code says:

  • wait until the wrapped value yields a result oa of type Option[A]
  • If oa turns out to be None, then there is nothing we can do, because we cannot obtain any instances of type A in order to call f. Therefore, immediately return None. The immediately return is the pure-method of the Future-monad, so for the general case we have to invoke wMonad.pure(None).
  • If oa yields Some(a), we can give this a to f, and then immediately unpack it to get to the value of type W[Option[B]].
  • Once we have the W[Option[B]] (whether empty or not), we can wrap it into WhateverOpt and return from the flatMap method.

I assume that you wanted to reimplement Monad[Option] just for fun (it's already in the library (the catsStdInstancesForOption thing is a CommutativeMonad), but here is how you could re-build it:

implicit val optionTMonad = new Monad[Option] {
  override def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
  def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f)
  def pure[A](a: A): Option[A] = Some(a)
  def tailRecM[A, B](a: A)(f: (A) => Option[Either[A, B]]): Option[B] = {
    f(a) match {
      case Some(Left(nextA)) => tailRecM(nextA)(f)
      case Some(Right(res)) => Some(res)
      case None => None
    }
  }
}

Notice that 1.0.1 requires to implement pure and tailRecM, and does not provide default implementations for that.


I don't want to say much about the necessary imports for future, but the latest version has cats.instances.future which provides a Monad instance. Check this again, because it seems as if you are using a different version of cats (your version didn't complain because of the missing tailRecM in your Option-monad).

Upvotes: 1

Related Questions