Идиоматический перевод молний Киселева на Scala?

Олег Киселев показал, как сделать застежку-молнию из любой проходимой, используя продолжения с разделителями. Его код на Haskell довольно короткий:

module ZipperTraversable where 

import qualified Data.Traversable as T
import qualified Data.Map as M


-- In the variant Z a k, a is the current, focused value
-- evaluate (k Nothing) to move forward
-- evaluate (k v)       to replace the current value with v and move forward.

data Zipper t a = ZDone (t a) 
                | Z a (Maybe a -> Zipper t a)

make_zipper :: T.Traversable t => t a -> Zipper t a
make_zipper t = reset $ T.mapM f t >>= return . ZDone
 where
 f a = shift (\k -> return $ Z a (k . maybe a id))

-- The Cont monad for delimited continuations, implemented here to avoid
-- importing conflicting monad transformer libraries

newtype Cont r a = Cont{runCont :: (a -> r) -> r}

instance Monad (Cont r) where
    return x = Cont $ \k -> k x
    m >>= f  = Cont $ \k -> runCont m (\v -> runCont (f v) k)

-- Two delimited control operators,
-- without answer-type polymorphism or modification
-- These features are not needed for the application at hand

reset :: Cont r r -> r
reset m = runCont m id

shift :: ((a -> r) -> Cont r r) -> Cont r a
shift e = Cont (\k -> reset (e k))

Я столкнулся с несколькими проблемами, пытаясь реализовать его на Scala. Я начал с попытки использовать пакет продолжений Scala с разделителями, но даже использовал richIterable <от Rompf. / a> идея обобщена на @cps [X] вместо @suspendable, невозможно, чтобы функция, предоставленная для сдвига, возвращала другой тип, чем функция, предоставленная для сброса.

Я пробовал реализовать монаду продолжения, следуя определению Киселева, но Scala затрудняет каррирование параметров типа, и я не мог понять, как превратить Cont [R] в монаду так, как это устраивало метод traverse в scalaz.

Я новичок как в Haskell, так и в Scala, и был бы очень признателен за помощь в этом.


person Mike Stay    schedule 11.04.2013    source источник


Ответы (1)


Вы можете использовать плагин продолжения. После того, как плагин выполнит свои переводческие работы, он станет похож на монаду Cont и shift и reset от Олега. Сложная часть - выяснить типы. Итак, вот мой перевод:

import util.continuations._
import collection.mutable.ListBuffer

sealed trait Zipper[A] { def zipUp: Seq[A] }
case class ZDone[A](val zipUp: Seq[A]) extends Zipper[A]
case class Z[A](current: A, forward: Option[A] => Zipper[A]) extends Zipper[A] {
  def zipUp = forward(None).zipUp
}

object Zipper {
  def apply[A](seq: Seq[A]): Zipper[A] = reset[Zipper[A], Zipper[A]] {
    val coll = ListBuffer[A]()
    val iter = seq.iterator
    while (iter.hasNext) {
      val a = iter.next()
      coll += shift { (k: A=>Zipper[A]) =>
        Z(a, (a1: Option[A]) => k(a1.getOrElse(a)))
      }
    }
    ZDone(coll.toList)
  }
}

Плагин продолжения поддерживает цикл while, но не map или flatMap, поэтому я решил использовать while и изменяемый ListBuffer для захвата возможно обновленных элементов. Функция make_zipper переводится в сопутствующее Zipper.apply - типичное место Scala для создания новых объектов или коллекций. Тип данных преобразуется в запечатанный типаж с расширением двух классов case. И я поместил функцию zip_up как метод Zipper с разными реализациями для каждого класса case. Также довольно типично.

Вот он в действии:

object ZipperTest extends App {
  val sample = for (v <- 1 to 5) yield (v, (1 to v).reduceLeft(_ * _))
  println(sample) // Vector((1,1), (2,2), (3,6), (4,24), (5,120))

  def extract134[A](seq: Seq[A]) = {
    val Z(a1, k1) = Zipper(seq)
    val Z(a2, k2) = k1(None)
    val Z(a3, k3) = k2(None)
    val Z(a4, k4) = k3(None)
    List(a1, a3, a4)
  }
  println(extract134(sample)) // List((1,1), (3,6), (4,24))

  val updated34 = {
    val Z(a1, k1) = Zipper(sample)
    val Z(a2, k2) = k1(None)
    val Z(a3, k3) = k2(None)
    val Z(a4, k4) = k3(Some(42 -> 42))
    val z = k4(Some(88 -> 22))
    z.zipUp
  }
  println(updated34) // List((1,1), (2,2), (42,42), (88,22), (5,120))
}

Как я определил типы для shift, k и reset или как перевести T.mapM?

Я посмотрел на mapM и знаю, что это позволит мне получить Cont, но я не уверен, что находится внутри Cont, поскольку это зависит от смены. Итак, я начинаю внутри смены. Игнорируя haskell return для построения Cont, shift возвращает Zipper. Я также предполагаю, что мне нужно добавить элемент типа A в мою коллекцию для сборки. Таким образом, сдвиг будет в «дыре», где ожидается элемент типа A, поэтому k будет A=>? функцией. Предположим это. Я ставлю вопросительные знаки после типов, в которых я не был так уверен. Итак, я начал с:

shift { (k: A?=>?) =>
  Z(a, ?)
}

Затем я посмотрел (внимательно) на (k . maybe a id). Функция maybe a id вернет A, так что это согласуется с тем, что k принимает в качестве аргумента. Это эквивалент a1.getOrElse(a). Кроме того, поскольку мне нужно заполнить Z(a, ?), мне нужно выяснить, как получить функцию от option до Zipper. Самый простой способ - предположить, что k возвращает Zipper. Кроме того, глядя на то, как используется застежка-молния k1(None) или k1(Some(a)), я знаю, что должен предоставить пользователю возможность произвольно заменять элементы, что и делает функция forward. Он продолжает исходный a или обновленный a. Это начинает обретать смысл. Итак, теперь у меня есть:

shift { (k: A=>Zipper[A]) =>
  Z(a, (a1: Option[A]) => k(a1.getOrElse(a)))
}

Затем я снова смотрю на mapM. Я вижу, что он составлен из return . ZDone. Снова игнорируя return (потому что это только для монады Cont), я вижу, что ZDone возьмет результирующую коллекцию. Так что это прекрасно, мне просто нужно вставить в него coll, и к тому времени, когда программа прибудет туда, в ней будут обновленные элементы. Кроме того, тип выражения внутри reset теперь соответствует типу возврата k, который равен Zipper[A].

Наконец, я указываю тип для сброса, который компилятор может вывести, но когда я угадаю, это дает мне (ложное) чувство уверенности, что я знаю, что происходит.

Мой перевод не такой общий или красивый, как перевод на Haskell, потому что он не сохраняет тип из коллекции и использует мутации, но, надеюсь, его легче понять.

Изменить: вот версия, которая сохраняет тип и использует неизменяемый список, так что z.zipUp == z.zipUp:

import util.continuations._
import collection.generic.CanBuild
import collection.SeqLike

sealed trait Zipper[A, Repr] { def zipUp: Repr }
case class ZDone[A, Repr](val zipUp: Repr) extends Zipper[A, Repr]
case class Z[A, Repr](current: A,
    forward: Option[A] => Zipper[A, Repr]) extends Zipper[A, Repr] {
  def zipUp = forward(None).zipUp
}

object Zipper {
  def apply[A, Repr](seq: SeqLike[A, Repr])
                    (implicit cb: CanBuild[A, Repr]): Zipper[A, Repr] = {
    type ZAR = Zipper[A, Repr]

    def traverse[B](s: Seq[A])(f: A => B@cps[ZAR]): List[B]@cps[ZAR] =
      if (s.isEmpty) List()
      else f(s.head) :: traverse(s.tail)(f)

    reset {
      val list = traverse(seq.toSeq)(a => shift { (k: A=>ZAR) =>
        Z(a, (a1: Option[A]) => k(a1.getOrElse(a)))
      })
      val builder = cb() ++= list
      ZDone(builder.result): ZAR
    }
  }
}

Кстати, вот дополнительные ресурсы по монаде продолжения в scala:

person huynhjl    schedule 11.04.2013
comment
Спасибо за подробный ответ! (Кстати: если вы хотите его отредактировать, я думаю, вы имели в виду seq вместо образца во второй строке extract134.) Что вы имеете в виду, говоря, что он не сохраняет тип из коллекции? Я не вижу ничего в коде. - person Mike Stay; 11.04.2013
comment
Кроме того, вы сказали, что продолжения не поддерживают карту, но это то, что я имел в виду с той ссылкой на слайды Rompf (см. Слайд 48 для определения карты, которая поддерживает продолжения). Мне нужно сделать это в чисто функциональном стиле, поэтому я начну с того, что вы написали, и попробую изменить это. Есть предложения по этому поводу? - person Mike Stay; 11.04.2013
comment
Мне удалось выяснить, как использовать приостанавливаемую карту Rompf для создания чисто функциональной версии: pastie.org/7460281 Спасибо за вашу помощь! - person Mike Stay; 12.04.2013
comment
Спасибо, поправил образец на seq. Что касается типа, моя версия возвращает List, когда начальным вводом является Vector, как для Zipper(Vector(1)).zipUp. Ваша версия с CanBuildFrom решает эту проблему. Обратите внимание, что версия с Suspendable Rompf все еще использует изменяемый Builder. Таким образом, вы столкнетесь с ошибками со следующим кодом: {val z = Zipper(Vector(1)); z.zipUp == z.zipUp}. Он вернет false. - person huynhjl; 12.04.2013
comment
О, вы правы насчет версии Rompf - я этого не заметил :( Я был слишком озабочен работой над ошибкой компилятора. Спасибо за тестовый пример! - person Mike Stay; 12.04.2013
comment
Хм. Код Rompf не позволяет вам дважды вызывать одно и то же продолжение с разными значениями! В этом смысле продолжения линейны. Вот версия, которая полностью исключает пакет продолжений и позволяет воспроизводить данное продолжение сколько угодно раз: pastie.org/7470775 < / а> - person Mike Stay; 13.04.2013
comment
Я предполагаю, что использование изменяемого построителя для реализации richIterable предотвращает повторное использование продолжения (потому что один и тот же построитель захватывается последовательными продолжениями). В вашей новой версии (и в моей редакции прошлой ночью - не уверен, что вы ее видели) мы используем неизменяемую структуру (список), и каждый контекст продолжения остается неизменным, независимо от того, сколько раз он использовался или что произошло. после ... Спасибо за вопрос, в любом случае он был очень познавательным. Также не стесняйтесь помещать код из своих пирожков в качестве дополнительного ответа на свой вопрос здесь, на SO. - person huynhjl; 13.04.2013