Можно ли реализовать liftM2 на Scala?

В 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

Я хотел бы перевести это на 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)

Это не удается, как я полагаю, наиболее очевидным из возможных способов: «значение flatMap не является членом параметра типа M [T1]». Правильно, я не указал, что M[_] это какая-то монада. Следующее, что я попытался сделать, это определить некоторый структурный тип, например:

type Monad[A] = {
  def flatMap[B](f: (A) => Monad[B]): Monad[B]
}

... и иметь M[A] <: Monad[A]. Но это не работает, потому что в Scala нет рекурсивных структурных типов.

Следующие несколько вещей, которые я попробовал, были связаны с вращениями, подобными M[A] <: FilterMonadic[A, _]. Все они потерпели неудачу, вероятно, потому, что я не смог определить правильное неявное-фу для CanBuildFrom.

Самый близкий вопрос, который я мог найти здесь, в StackOverflow, был этот, касающийся как рекурсивных структурных типов, так и того, как имитировать классы типов Haskell в Scala. Но этот подход требует определения неявного преобразования каждого типа, который вас интересует, в черту, определяющую класс типов, что в данном случае кажется ужасно замкнутым ...

Есть ли хороший способ сделать то, что я пытаюсь сделать?


person mergeconflict    schedule 03.12.2011    source источник
comment
У метода неявного преобразования есть свои недостатки, но он широко используется. Это ближайший аналог объявления instance в Haskell (и самое надежное последнее средство, когда другие решения Scalaish терпят неудачу).   -  person Owen    schedule 03.12.2011


Ответы (1)


Оказывается, что обычный способ кодирования классов типов в 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
}

Эта идея представлена ​​в Типовых классах для бедняков и исследована подробнее глубоко в Типовые классы как объекты и следствия. Обратите внимание, что метод point мог не быть определен в объектно-ориентированном интерфейсе, поскольку у него нет M[A] в качестве одного из аргументов, который нужно преобразовать в ссылку this в кодировке OO. (Или, другими словами: он не может быть частью интерфейса по той же причине, по которой подпись конструктора не может быть представлена ​​в интерфейсе.)

Затем вы можете написать 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)

Этот шаблон широко применяется в Scalaz. Версия 7, которая сейчас находится в разработке, включает индекс типовые классы.

Помимо предоставления классов и экземпляров типов для стандартных библиотечных типов, он предоставляет «синтаксический» уровень, который позволяет использовать более знакомый стиль вызова методов Receiver.method (args). Это часто обеспечивает лучший вывод типов (с учетом алгоритма вывода слева направо в Scala) и позволяет использовать синтаксический сахар для понимания. Ниже мы используем это, чтобы переписать 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)

Обновить

Да, можно написать менее общую версию 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)
person retronym    schedule 03.12.2011
comment
Большое спасибо, Джейсон! Я слышал много хорошего о скалазе-7. Но мне все еще грустно: вещи, которые я хочу использовать liftM2, очевидны, например, List и Option, у которых уже есть отличные определения для map и flatMap. Есть ли способ определить liftM2, чтобы использовать их напрямую? - person mergeconflict; 03.12.2011
comment
Действительно хорошее объяснение, спасибо. Я время от времени играл со Scala, и этот пост прояснил некоторые моменты, которые ранее были очень непрозрачными. - person James Cunningham; 05.01.2012
comment
Могу добавить, что Haskell не реализует это настолько хорошо, насколько это возможно. Все, что нужно для liftM2, - это (‹*›) и карта. Операция с точкой не требуется, и она часто мешает (есть функторы с (‹*›), но не точка). Ближайшее к Haskell - это liftA2, определяемая в терминах (‹*›) и point (иначе говоря, pure). В этом сообщении говорится об использовании Scala blog.tmorris.net/lifting - person Tony Morris; 12.08.2012