что такое правильное понимание монады или последовательности как для отображения, так и для переноса состояния?

Я пишу интерпретатор языка программирования.

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

Это не раз, потому что результаты выходят как карта. Это не карта из-за государственной опоры.

У меня есть этот код, который я использую, чтобы попытаться понять это. Сначала проведите несколько строк тестовой установки:

// test rig
class MonadLearning extends JUnit3Suite {

  val d = List("1", "2", "3") // some expressions to evaluate. 

  type ResType = Int 
  case class State(i : ResType) // trivial state for experiment purposes
  val initialState = State(0)

// my stub/dummy "eval" function...obviously the real one will be...real.
  def computeResultAndNewState(s : String, st : State) : (ResType, State) = {
    val State(i) = st
    val res = s.toInt + i
    val newStateInt = i + 1
    (res, State(newStateInt))
  }

Мое текущее решение. Использует переменную, которая обновляется по мере оценки тела карты:

  def testTheVarWay() {
    var state = initialState
    val r = d.map {
      s =>
        {
          val (result, newState) = computeResultAndNewState(s, state)
          state = newState
          result
        }
    }
    println(r)
    println(state)
  }

У меня есть то, что я считаю неприемлемым решением, используя foldLeft, который делает то, что я называю идиомой «упаковать, когда вы сложите»:

def testTheFoldWay() {

// This startFold thing, requires explicit type. That alone makes it muddy.
val startFold : (List[ResType], State) = (Nil, initialState)
val (r, state) = d.foldLeft(startFold) {
  case ((tail, st), s) => {
    val (r, ns) = computeResultAndNewState(s, st)
    (tail :+ r, ns) // we want a constant-time append here, not O(N). Or could Cons on front and reverse later
  }
}

println(r)
println(state)

}

У меня также есть несколько рекурсивных вариантов (которые очевидны, но также не ясны или хорошо мотивированы), один из которых использует потоки, что почти терпимо:

def testTheStreamsWay() {
  lazy val states = initialState #:: resultStates // there are states
  lazy val args = d.toStream // there are arguments
  lazy val argPairs = args zip states // put them together
  lazy val resPairs : Stream[(ResType, State)] = argPairs.map{ case (d1, s1) => computeResultAndNewState(d1, s1) } // map across them
  lazy val (results , resultStates) = myUnzip(resPairs)// Note .unzip causes infinite loop. Had to write my own.

  lazy val r = results.toList
  lazy val finalState = resultStates.last

  println(r)
  println(finalState)
}

Но я не могу придумать ничего более компактного или ясного, чем исходное решение 'var' выше, с которым я готов жить, но я думаю, что кто-то, кто ест/пьет/спит идиомы монад, собирается просто сказать .. , используйте это... (Надеюсь!)


person Mike Beckerle    schedule 04.09.2012    source источник
comment
Кроме того, вы хотите добавить постоянное время, используйте Vector вместо List.   -  person Sean Parsons    schedule 04.09.2012


Ответы (3)


С комбинатором карта-с-аккумулятором (простой способ)

Нужна функция высшего порядка mapAccumL. Он находится в стандартной библиотеке Haskell, но для Scala вам понадобится нужно использовать что-то вроде Scalaz.

Сначала импорт (обратите внимание, что здесь я использую Scalaz 7; в предыдущих версиях вы бы импортировали Scalaz._):

import scalaz._, syntax.std.list._

И тогда это однострочный:

scala> d.mapAccumLeft(initialState, computeResultAndNewState)
res1: (State, List[ResType]) = (State(3),List(1, 3, 5))

Обратите внимание, что мне пришлось изменить порядок аргументов вашего оценщика и кортеж возвращаемого значения, чтобы они соответствовали сигнатурам, ожидаемым mapAccumLeft (укажите сначала в обоих случаях).

С помощью монады состояния (чуть менее простой способ)

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

Во-первых, Scalaz предоставляет mapM — он просто называется traverse (что немного более общее, как отмечает Петр Пудлак в своем комментарии). Итак, предположим, что у нас есть следующее (здесь я снова использую Scalaz 7):

import scalaz._, Scalaz._

type ResType = Int
case class Container(i: ResType)

val initial = Container(0)
val d = List("1", "2", "3")

def compute(s: String): State[Container, ResType] = State {
  case Container(i) => (Container(i + 1), s.toInt + i)
}

Мы можем написать это:

d.traverse[({type L[X] = State[Container, X]})#L, ResType](compute).run(initial)

Если вам не нравится лямбда уродливого типа, вы можете избавиться от нее следующим образом:

type ContainerState[X] = State[Container, X]

d.traverse[ContainerState, ResType](compute).run(initial)

Но становится еще лучше! Scalaz 7 предоставляет вам версию traverse, специализированную для монады состояния:

scala> d.traverseS(compute).run(initial)
res2: (Container, List[ResType]) = (Container(3),List(1, 3, 5))

И как будто этого недостаточно, есть даже версия со встроенным run:

scala> d.runTraverseS(initial)(compute)
res3: (Container, List[ResType]) = (Container(3),List(1, 3, 5))

На мой взгляд, все еще не так красиво, как версия mapAccumLeft, но довольно чисто.

person Travis Brown    schedule 04.09.2012
comment
Конечно, я не против, ваш ответ намного приятнее. Я видел там монаду состояния, но не знал, как ее правильно использовать в Scala. На самом деле, в Haskell это также называется пересекать. Разница в том, что mapM работает только со списками (и его легче понять), тогда как traverse с любым проходимая структура. (Я использовал Scalaz 6, это только Scalaz 7?) - person Petr; 05.09.2012
comment
Правильно — я добавляю примечание о traverse (которое было в Scalaz с тех пор, как я его использовал — это просто traverseS новый материал). - person Travis Brown; 05.09.2012
comment
Хотел бы я дать вам более одного голоса за поднятие traverse и traverseS... Мне пришлось столкнуться с mapM haskell в чужом коде, чтобы понять, что это именно то, что я ищу. - person nietaki; 22.11.2012
comment
Как только вы увидите метод со следующей сигнатурой, def foo[A](s:State):(State,A), вы знаете, что должны использовать монаду State. Монады состояния инкапсулируют поведение S => (S,A). Действительно приятно то, что вы можете поместить их в для понимания, и они будут выполняться последовательно и позаботятся о передаче состояния от одного к другому без необходимости делать это вручную [eed3si9n.com/learning-scalaz-day7] — отличный пример! - person jordan3; 30.05.2014

То, что вы описываете, является вычислением внутри монады состояния. Я считаю, что ответ на ваш вопрос

Это не раз, потому что результаты выходят как карта. Это не карта из-за государственной опоры.

заключается в том, что это монадическая карта, использующая монаду состояния.

Значения монады состояния — это вычисления, которые считывают некоторое внутреннее состояние, возможно, изменяют его и возвращают некоторое значение. Он часто используется в Haskell (см. здесь или здесь).

Для Scala в библиотеке ScalaZ есть trait с именем Статус, который его моделирует (см. также источник). В States есть служебные методы для создания экземпляров State. Обратите внимание, что с монадической точки зрения State — это просто монадическое значение. Сначала это может показаться запутанным, поскольку описывается функцией, зависящей от состояния. (Монадическая функция может иметь тип A => State[B].)

Далее вам понадобится функция monadic map, которая вычисляет значения ваших выражений, передавая состояние через вычисления. В Haskell есть библиотечный метод mapM, который делает именно это, если он специализирован на монаде состояния.

В Scala такой библиотечной функции нет (если есть, поправьте меня). Но создать его можно. Чтобы привести полный пример:

import scalaz._;

object StateExample
  extends App
  with States /* utility methods */
{
  // The context that is threaded through the state.
  // In our case, it just maps variables to integer values.
  class Context(val map: Map[String,Int]);

  // An example that returns the requested variable's value
  // and increases it's value in the context.
  def eval(expression: String): State[Context,Int] =
    state((ctx: Context) => {
      val v = ctx.map.get(expression).getOrElse(0);
      (new Context(ctx.map + ((expression, v + 1)) ), v);
    });

  // Specialization of Haskell's mapM to our State monad.
  def mapState[S,A,B](f: A => State[S,B])(xs: Seq[A]): State[S,Seq[B]] =
    state((initState: S) => {
      var s = initState;
      // process the sequence, threading the state
      // through the computation
      val ys = for(x <- xs) yield { val r = f(x)(s); s = r._1; r._2 };
      // return the final state and the output result
      (s, ys);
    });


  // Example: Try to evaluate some variables, starting from an empty context.
  val expressions = Seq("x", "y", "y", "x", "z", "x");

  print( mapState(eval)(expressions) ! new Context(Map[String,Int]()) );
}

Таким образом, вы можете создавать простые функции, которые принимают некоторые аргументы и возвращают State, а затем объединяют их в более сложные, используя State.map или State.flatMap (или, возможно, лучше используя for включения), а затем вы можете выполнить все вычисления для списка выражений с помощью mapM.


См. также http://blog.tmorris.net/posts/the-state-monad-for-scala-users/


Редактировать: см. ответ Трэвиса Брауна, он описал, как использовать монаду состояния в Scala. гораздо приятнее.

Он также спрашивает:

Но зачем, когда есть стандартный комбинатор, который делает именно то, что нужно в данном случае? (Я спрашиваю это как человек, получивший пощечину за использование монады состояния, когда подойдет mapAccumL.)

Это потому, что исходный вопрос задавался:

Это не раз, потому что результаты выходят как карта. Это не карта из-за государственной опоры.

и я считаю, что правильным ответом будет это монадическая карта, использующая монаду состояния.

Использование mapAccumL, безусловно, быстрее, как меньше памяти, так и накладных расходов ЦП. Но монада состояния фиксирует концепцию происходящего, суть проблемы. Я считаю, что во многих (если не в большинстве) случаях это важнее. Как только мы осознаем суть проблемы, мы можем либо использовать концепции высокого уровня, чтобы красиво описать решение (возможно, немного пожертвовав скоростью/памятью), либо оптимизировать его, чтобы оно было быстрым (или, возможно, даже сделать и то, и другое).

С другой стороны, mapAccumL решает эту конкретную проблему, но не дает более широкого ответа. Если нам нужно немного изменить его, может случиться так, что он больше не будет работать. Или, если библиотека станет сложной, код может стать беспорядочным, и мы не будем знать, как его улучшить, как снова сделать первоначальную идею понятной.

Например, в случае оценки выражений с состоянием библиотека может стать сложной и сложной. Но если мы используем монаду состояния, мы можем построить библиотеку вокруг небольших функций, каждая из которых принимает некоторые аргументы и возвращает что-то вроде State[Context,Result]. Эти атомарные вычисления можно комбинировать с более сложными, используя flatMap метод или for вложений, и, наконец, мы построим желаемую задачу. Принцип останется одинаковым для всей библиотеки, и конечная задача также будет возвращать State[Context,Result].

В заключение: я не говорю, что использование монады состояния — лучшее решение, и уж точно не самое быстрое. Я просто считаю, что это наиболее поучительно для функционального программиста — оно описывает проблему в чистом, абстрактном виде.

person Petr    schedule 04.09.2012
comment
Но зачем, когда есть стандартный комбинатор, который делает именно то, что нужно в данном случае? (Я спрашиваю это как человек, получивший пощечину за использование монады состояния, когда mapAccumL подошло бы.) - person Travis Brown; 05.09.2012
comment
Я обновил свой ответ версией монады состояния, вдохновленной вашей - надеюсь, вы не возражаете. - person Travis Brown; 05.09.2012

Вы можете сделать это рекурсивно:

def testTheRecWay(xs: Seq[String]) = {
  def innerTestTheRecWay(xs: Seq[String], priorState: State = initialState, result: Vector[ResType] = Vector()): Seq[ResType] = {
    xs match {
      case Nil => result
      case x :: tail =>
        val (res, newState) = computeResultAndNewState(x, priorState)
        innerTestTheRecWay(tail, newState, result :+ res)
    }
  }
  innerTestTheRecWay(xs)
}

Рекурсия является обычной практикой в ​​функциональном программировании, и в большинстве случаев ее легче читать, писать и понимать, чем циклы. На самом деле в scala нет никаких циклов, кроме while. fold, map, flatMap, for (это просто сахар для flatMap/map) и т. д. - все рекурсивны.

Этот метод является хвостовой рекурсией и будет оптимизирован компилятором, чтобы не создавать стек, поэтому его абсолютно безопасно использовать. Вы можете добавить аннотацию @annotation.tailrec, чтобы заставить компилятор применить устранение хвостовой рекурсии. Если ваш метод не tailrec, компилятор будет жаловаться.

edit: переименован внутренний метод, чтобы избежать двусмысленности

person drexin    schedule 04.09.2012