您可以轻松实现您提出的语法(我将其称为 toAmbient 而不是 mapReturn,后来我将 f 替换为 h 以使标识符的分离更加明显)。
这是一个在函数上使用隐式包装类的实现:
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
}
}
}
现在您可以定义f: Int => Option[Int] 和各种g, g2 接受Int => Int 并返回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)
然后将f 传递给g 和g2,如下所示:
println(f.toAmbient(h => g(h)))
println(f.toAmbient(h => g2(h)))
这将打印:
Some(5)
None
扩展评论
我想解释一下为什么我发现Try(g(fOpt(_).get)).toOption 实际上很好。
假设有一些自然的方式来转换每个
f: X => Option[Y]
进入
fPrime: X => Y
这意味着有一种自然的方法可以将每个Unit => Option[Y] 转换为Unit => Y。由于Unit => Y 本质上与Y 相同,这反过来意味着有某种方法可以将每个Option[Y] 转换为Y。但是没有从Option[Y] 到Y 的自然转换。这是一个相当普遍的现象:虽然有point/unit,而且从X 到M[X] 总是很容易进入monad,但通常没有安全/简单/无损的方式来获得M[X] 到X 的单子的em>out,例如:
- 在
Option[X] 上调用get 返回X,但可以抛出NoSuchElementException
- 同样,在
List 上调用head 会抛出异常,同时也会丢弃tail。
- 等待
Future 被阻塞
- 从随机
Distribution[X] 中采样X 会留下固定的X,但会删除所有其他可能的X 的概率信息
等等。
您可以使用Try 解决类型签名g(f: Int => Int) 的事实是因为Int => Int 部分并不十分精确:它不是身份单子,而是支持状态和异常的默认环境单子.在“现实”中,g 有点像g(f: Int => DefaultAmbientMonad[Int]),因为f 也可以抛出异常。
现在,有趣的是,虽然没有从Option[X] 到X 的保证方法,但实际上有一种从Option[X] 到DefaultAmbientMonad[X] 的方法:如果Option 是None,只需抛出一些非常特别的NoneEmbeddingException。从DefaultAmbientMonad 到Option 又是不安全的:你可以捕捉到你的特殊NoneEmbeddingException,但是你必须“祈祷”不会抛出其他异常(这就是它“不安全”的原因)。
因此,将fOpt 传递给g 的最“系统”方式实际上是
class NoneEmbeddingException extends RuntimeException
try {
Option(g(fOpt(_).getOrElse(throw new NoneEmbeddingException)))
} catch {
case e: NoneEmbeddingException => None
}
这是我在上面的代码 sn-p 中实现的。
但这几乎是您使用Try(...).toOption 所拥有的,只是您使用预定义的NoSuchElementException 而不是有些做作的NoneEmbeddingException!
所以,我只想说:你的解决方案是正确的,可以通过系统讨论从 Option monad 到默认环境 monad 的自然转换来证明它是正确的,这并不是特别令人惊讶。个人意见:用Try(...).toOption就可以了。