是否可以在Scala中实现liftM2?

mer*_*ict 28 monads haskell scala typeclass lifting

在Haskell中,liftM2可以定义为:

liftM2 :: (Monad m) => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
liftM2 f m1 m2 = do
  x1 <- m1
  x2 <- m2
  return $ f x1 x2
Run Code Online (Sandbox Code Playgroud)

我想把它翻译成Scala.我的第一次尝试如下:

def liftM2[T1, T2, R, M[_]](f: (T1, T2) => R)(ma: M[T1], mb: M[T2]) : M[R] = for {
  a <- ma
  b <- mb
} yield f(a, b)
Run Code Online (Sandbox Code Playgroud)

我认为这是最明显的可行方式:"flat flatMap不是类型参数M [T1]的成员".是的,我没有说明M[_]是某种单子.所以接下来我尝试的是定义一些结构类型,如:

type Monad[A] = {
  def flatMap[B](f: (A) => Monad[B]): Monad[B]
}
Run Code Online (Sandbox Code Playgroud)

......并且拥有M[A] <: Monad[A].但这不起作用,因为Scala没有递归结构类型.

所以接下来我尝试的几件事涉及到类似的旋转M[A] <: FilterMonadic[A, _].那些都失败了,可能是因为我无法弄清楚正确的隐含CanBuildFrom.

我在StackOverflow上可以找到的最密切相关的问题就是这个问题,涉及递归结构类型以及如何在Scala中模仿Haskell的类型类.但是这种方法需要定义从你关心的每个类型到定义类型类的特性的隐式转换,在这种情况下看起来非常循环......

有什么好方法可以做我想做的事情吗?

ret*_*nym 31

在Scala中编码类型类的常用方法结果是非常接近Haskell:List不实现Monad接口(正如您在面向对象语言中所期望的那样),而是在单独的对象中定义类型类实例.

trait Monad[M[_]] {
   def point[A](a: => A): M[A]
   def bind[A, B](ma: M[A])(f: A => M[B]): M[B]
   def map[A, B](ma: M[A])(f: A => B): M[B] = bind(ma)(a => point(f(a)))
}

implicit object listMonad extends Monad[List] {
   def point[A](a: => A) = List(a)
   def bind[A, B](ma: List[A])(f: A => List[B]) = ma flatMap f
}
Run Code Online (Sandbox Code Playgroud)

这个想法是在穷人的类型类中引入的,并且在类型类中作为对象和含义进行了更深入的探索.请注意,该point方法可以被在一个面向对象的接口中定义的,因为它不具有M[A]作为它的一个参数被转换到this在一个面向对象的编码参考.(或换句话说:它不能成为接口的一部分,原因相同,构造函数签名无法在接口中表示.)

然后你可以写liftM2为:

def liftM2[M[_], A, B, C](f: (A, B) => C)
                         (implicit M: Monad[M]): (M[A], M[B]) => M[C] =
  (ma, mb) => M.bind(ma)(a => M.map(mb)(b => f(a, b)))

val f = liftM2[List, Int, Int, Int](_ + _)

f(List(1, 2, 3), List(4, 5)) // List(5, 6, 6, 7, 7, 8)
Run Code Online (Sandbox Code Playgroud)

这种模式已在Scalaz中广泛应用.目前正在开发的版本7包括类型类索引.

除了为标准库类型提供类型类和实例之外,它还提供了一个"语法"层,允许更熟悉的receiver.method(args)方法调用样式.这通常提供更好的类型推断(考虑到Scala的从左到右的推理算法),并允许使用for-comprehension语法糖.下面,我们使用它来重写liftM2,基于map和中的flatMap方法MonadV.

// Before Scala 2.10
trait MonadV[M[_], A] {
   def self: M[A]
   implicit def M: Monad[M]

   def flatMap[B](f: A => M[B]): M[B] = M.bind(self)(f)
   def map[B](f: A => B): M[B] = M.map(self)(f)
}
implicit def ToMonadV[M[_], A](ma: M[A])
                              (implicit M0: Monad[M]) =
  new MonadV[M, A] {
    val M = M0
    val self = ma
  }

// Or, as of Scala 2.10
implicit class MonadOps[M[_], A](self: M[A])(implicit M: Monad[M]) {
  def flatMap[B](f: A => M[B]): M[B] = M.flatMap(self)(f)
  def map[B](f: A => B): M[B] = M.map(self)(f)
}


def liftM2[M[_]: Monad, A, B, C](f: (A, B) => C): (M[A], M[B]) => M[C] =
  (ma, mb) => for {a <- ma; b <- mb} yield f(a, b)
Run Code Online (Sandbox Code Playgroud)

更新

是的,它可以liftM2为Scala集合编写不太通用的版本.您只需要提供所有必需的CanBuildFrom实例.

scala> def liftM2[CC[X] <: TraversableLike[X, CC[X]], A, B, C]
     |           (f: (A, B) => C)
     |           (implicit ba: CanBuildFrom[CC[A], C, CC[C]], bb: CanBuildFrom[CC[B], C, CC[C]])
     |           : (CC[A], CC[B]) => CC[C] =
     |   (ca, cb) => ca.flatMap(a => cb.map(b => f(a, b)))
liftM2: [CC[X] <: scala.collection.TraversableLike[X,CC[X]], A, B, C](f: (A, B) => C)(implicit ba: scala.collection.generic.CanBuildFrom[CC[A],C,CC[C]], implicit bb: scala.collection.generic.CanBuildFrom[CC[B],C,CC[C]])(CC[A], CC[B]) => CC[C]

scala> liftM2[List, Int, Int, Int](_ + _)
res0: (List[Int], List[Int]) => List[Int] = <function2>

scala> res0(List(1, 2, 3), List(4, 5))
res1: List[Int] = List(5, 6, 6, 7, 7, 8)
Run Code Online (Sandbox Code Playgroud)

  • 非常感谢,杰森!我听说过scalaz-7的好东西.但我仍然有点难过:我想用`liftM2`的东西是明显的,比如`List`和`Option`,它们已经对`map`和`flatMap`有了很好的定义.有没有办法定义`liftM2`直接使用它们? (2认同)
  • 我可能会补充一点,Haskell并没有尽可能好地实现它.liftM2所需要的只是(<*>)和map.点操作不是必需的,它经常"妨碍"(有(<*>)但没有点的仿函数).最接近的Haskell得到的是按(<*>)和点(又称纯)定义的liftA2.这篇文章使用Scala http://blog.tmorris.net/lifting/ (2认同)