Reputation: 4469
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
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.:
get
on Option[X]
returns X
, but can throw NoSuchElementException
head
on List
can throw exceptions, and also throws away the tail
.Future
is blockingX
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