Кошки со списком монад состояний "быстро проваливаются" в методе <...>. Sequence? - PullRequest
2 голосов
/ 30 марта 2020

допустим, у нас есть список состояний, и мы хотим упорядочить их:

import cats.data.State
import cats.instances.list._
import cats.syntax.traverse._


trait MachineState
case object ContinueRunning extends MachineState
case object StopRunning extends MachineState

case class Machine(candy: Int)

val addCandy: Int => State[Machine, MachineState] = amount =>
  State[Machine, MachineState] { machine =>
    val newCandyAmount = machine.candy + amount
    if(newCandyAmount > 10)
      (machine, StopRunning)
    else
      (machine.copy(newCandyAmount), ContinueRunning)
  }


List(addCandy(1),
     addCandy(2),
     addCandy(5),
     addCandy(10),
     addCandy(20),
     addCandy(50)).sequence.run(Machine(0)).value

Результат будет

(Machine(10),List(ContinueRunning, ContinueRunning, ContinueRunning, StopRunning, StopRunning, StopRunning))

Очевидно, что 3 последних шага являются избыточными. Можно ли как-нибудь остановить эту последовательность раньше? Здесь, когда возвращается StopRunning, я хотел бы остановить. Например, список Either'ов быстро потерпит неудачу и остановит последовательность рано, если потребуется (потому что он действует как монада).

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

Спасибо! :))

1 Ответ

1 голос
/ 30 марта 2020

Здесь необходимо сделать 2 вещи.

Первое - это понимание того, что на самом деле происходит:

  • State принимает некоторое значение состояния, потоки между многими составными вызывает и в процессе выдает некоторое выходное значение, а также
  • , в вашем случае Machine - это состояние , пронизывающее между вызовами, в то время как MachineState - это выход одной операции
  • sequence (обычно) берет коллекцию (здесь List) некоторых параметров c stuff here State[Machine, _] и поворачивает вложение на левой стороне (здесь: List[State[Machine, _]] -> State[Machine, List[_]]) (_ - это пробел, который вы будете заполнять своим типом)
  • в результате вы будете проходить состояние (Machine(0)) по всем функциям, а вы объединяете выходные данные каждой из них (MachineState) в список выходов
// ammonite

// to better see how many times things are being run
@ {
  val addCandy: Int => State[Machine, MachineState] = amount =>
    State[Machine, MachineState] { machine =>
      val newCandyAmount = machine.candy + amount
      println("new attempt with " + machine + " and " + amount)
      if(newCandyAmount > 10)
        (machine, StopRunning)
      else
        (machine.copy(newCandyAmount), ContinueRunning)
    }
  }
addCandy: Int => State[Machine, MachineState] = ammonite.$sess.cmd24$$$Lambda$2669/1733815710@25c887ca

@ List(addCandy(1),
       addCandy(2),
       addCandy(5),
       addCandy(10),
       addCandy(20),
       addCandy(50)).sequence.run(Machine(0)).value
new attempt with Machine(0) and 1
new attempt with Machine(1) and 2
new attempt with Machine(3) and 5
new attempt with Machine(8) and 10
new attempt with Machine(8) and 20
new attempt with Machine(8) and 50
res25: (Machine, List[MachineState]) = (Machine(8), List(ContinueRunning, ContinueRunning, ContinueRunning, StopRunning, StopRunning, StopRunning))

Другими словами, то, что вы хотите, это разрыв цепи, тогда .sequence может быть не тем, что вы хотите.

Как раз на самом деле, вы, вероятно, хотите что-то еще - объединить список A => (A, B) функций в одну функция, которая останавливает следующее вычисление, если результат вычисления равен StopRunning (в вашем коде ничего не сказано коду, каково состояние разрыва цепи и как оно должно выполняться). Я бы предложил сделать это явно с какой-то другой функцией, например:

@ {
  List(addCandy(1),
       addCandy(2),
       addCandy(5),
       addCandy(10),
       addCandy(20),
       addCandy(50))
    .reduce { (a, b) =>
      a.flatMap {
        // flatMap and map uses MachineState
        // - the second parameter is the result after all!
        // we are pattern matching on it to decide if we want to
        // proceed with computation or stop it

        case ContinueRunning => b // runs next computation
        case StopRunning     => State.pure(StopRunning) // returns current result without modifying it
      }
    }
    .run(Machine(0))
    .value
  }
new attempt with Machine(0) and 1
new attempt with Machine(1) and 2
new attempt with Machine(3) and 5
new attempt with Machine(8) and 10
res23: (Machine, MachineState) = (Machine(8), StopRunning)

Это избавит от необходимости запускать код внутри addCandy - но вы не можете действительно избавиться от кода, который объединяет состояния вместе, поэтому reduce logi c будет применяться во время выполнения n-1 раз (где n - размер вашего списка), и с этим ничего не поделаешь.

Кстати, если вы присмотритесь к Either вы обнаружите, что он также вычисляет n результаты и только затем объединяет их так, что это выглядит как разрыв цепи, но на самом деле это не так. Последовательность объединяет результаты «параллельных» вычислений, но не прервет их, если какой-либо из них завершится неудачей.

...