WeiChing 林煒清
WeiChing 林煒清

Reputation: 4469

difficulty about passing function returning functor or monad type

I run into a case that monad on return type hinders the high-order function programming.

val fOpt: (x: Int) => Option[Int]

def g(f: Int=>Int): Int

How do I call g(fOpt) and get result as Option[Int] ?
My solution is Try(g(fOpt(_).get)).toOption. but I'm not satisfied.

Is there methods that I don't know can solve this. I ask this because I know less about functional programming (so many pattern and theory). I expect there would be something like functor for function return such that it can work like val ret:Option[Int] = fOpt.mapReturn(f=>g(f))

Upvotes: 3

Views: 134

Answers (1)

Andrey Tyukin
Andrey Tyukin

Reputation: 44918

You can easily implement the syntax that you proposed (I called it toAmbient instead of mapReturn, and later I replaced f by h to make the separation of the identifiers more obivous).

Here is an implementation that uses an implicit wrapper class on functions:

implicit class UnsafeEmbedAmbientOps[X, Y](f: X => Option[Y]) {
  class NoneToAmbientEmbeddingException extends RuntimeException
  def toAmbient[Z](a: (X => Y) => Z): Option[Z] = {
    try {
      Some(a(f(_).getOrElse(throw new NoneToAmbientEmbeddingException)))
    } catch {
      case e: NoneToAmbientEmbeddingException => None
    }
  }
}

Now you can define f: Int => Option[Int] and various g, g2 that take Int => Int and return Int:

val f: Int => Option[Int] = x => Map(1 -> 1, 2 -> 4, 3 -> 9).get(x)
def g(f: Int => Int): Int = f(1) + f(2)
def g2(f: Int => Int): Int = f(1) + f(42)

and then pass f to g and g2 as follows:

println(f.toAmbient(h => g(h)))
println(f.toAmbient(h => g2(h)))

This will print:

Some(5)
None

Extended comment

I want to try to explain why I find Try(g(fOpt(_).get)).toOption actually good.

Suppose that there were some natural way to transform every

f: X => Option[Y]

into an

fPrime: X => Y

This would imply that there is a natural way to transform every Unit => Option[Y] into an Unit => Y. Since Unit => Y is essentially the same as Y, this would in turn imply that there is some way to transform every Option[Y] into an Y. But there is no natural transformation from Option[Y] to Y. This is a rather general phenomenon: while there is point/unit, and it is always easy to get into the monad from X to M[X], there is generally no safe/easy/lossless way to get out of a monad from M[X] to X, e.g.:

  • Calling get on Option[X] returns X, but can throw NoSuchElementException
  • Likewise, calling head on List can throw exceptions, and also throws away the tail.
  • Awaiting a Future is blocking
  • Sampling an X from a random Distribution[X] leaves you with a fixed X, but erases information about probabilities of all the other possible X

etc.

The fact that you can work around the type signature g(f: Int => Int) with Try is because the Int => Int part is not really precise: it is not the identity monad, but rather the default ambient monad that supports state and exceptions. In "reality", g is rather something like g(f: Int => DefaultAmbientMonad[Int]), because f can also throw exceptions.

Now, the interesting thing is that while there is no guaranteed way to get from Option[X] to X, there actually is a way to get from Option[X] to DefaultAmbientMonad[X]: just throw some very special NoneEmbeddingException if the Option is None. Getting from DefaultAmbientMonad to Option is again unsafe: you can catch your special NoneEmbeddingException, but then you have to "pray" that no other exceptions will be thrown (that's why it is "unsafe").

So, the most "systematic" way to pass fOpt to g would actually be

class NoneEmbeddingException extends RuntimeException

try {
  Option(g(fOpt(_).getOrElse(throw new NoneEmbeddingException)))
} catch {
  case e: NoneEmbeddingException => None
}

This is what I've implemented in the code snippet above.

But this is almost what you have already with Try(...).toOption, except that you use the predefined NoSuchElementException instead of the somewhat contrived NoneEmbeddingException!

So, I would just say: your solution is correct, it can be justified by a systematic discussion of the natural transformations from the Option monad to the default ambient monad, and it is not particularly astonishing. My personal opinion: just use Try(...).toOption, it's ok.

Upvotes: 3

Related Questions