1

我正在使用一个待办事项列表应用程序

  • 斯卡拉伊斯,
  • 猫(免费单子),和
  • scalajs反应

当我使用像下面的代码这样的简单模型时,一切都按预期工作。

class TodoModel() {
  private object State {
    var todos = Seq.empty[Todo]

    def mod(f: Seq[Todo] => Seq[Todo]): Callback = {
      val newTodos = f(todos)
      Callback(todos = newTodos)
    }
  }

  def add(t: Todo): Callback = State.mod(_ :+ t)
  def todos: Seq[Todo] = State.todos
}

一旦我使用了猫的免费单子,我就会有一种奇怪的行为。第一次单击总是插入两个待办事项条目。之后的每次点击都按预期工作。请参阅下面的图片。

这里有什么问题?

import cats.free.Free
import cats.free.Free.liftF
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom

case class Todo(text: String)

sealed trait TodoModelOp[A]
case class Add(todo: Todo) extends TodoModelOp[Unit]
case class Todos() extends TodoModelOp[Seq[Todo]]

object FreeTodoModelOps {
  // type alias for lifted TodoModelOp
  type TodoModelOpF[A] = Free[TodoModelOp, A]

  def add(Todo: Todo): TodoModelOpF[Unit] = liftF[TodoModelOp, Unit](Add(Todo))
  def todos: TodoModelOpF[Seq[Todo]] = liftF[TodoModelOp, Seq[Todo]](Todos())
}

object StateInterpreter {
  import cats.arrow.FunctionK
  import cats.{ Id, ~> }

  val interpet: TodoModelOp ~> Id = new (TodoModelOp ~> Id) {
    val todos = scala.collection.mutable.ArrayBuffer.empty[Todo]

    def apply[A](fa: TodoModelOp[A]): Id[A] = fa match {
      case Add(todo) => todos += todo; ()
      case Todos() => todos.toSeq
    }
  }

}

class TodoModel() {
  import cats.instances.list._
  import cats.syntax.traverse._
  import FreeTodoModelOps._

  def add(t: Todo): Callback = {
    def program: TodoModelOpF[Unit] = for {
      _ <- FreeTodoModelOps.add(t)
    } yield ()

    Callback(program.foldMap(StateInterpreter.interpet))
  }

  def todos: Seq[Todo] = {
    def program: TodoModelOpF[Seq[Todo]] = for {
      n <- FreeTodoModelOps.todos
    } yield n

    program.foldMap(StateInterpreter.interpet)
  }
}

object TodoPage {

  case class Props(model: TodoModel)

  case class State(todos: Seq[Todo])

  class Backend($: BackendScope[Props, State]) {
    val t = Todo("a new todo")

    def onSubmit(e: ReactEventFromInput) =
      e.preventDefaultCB >>
        $.modState(s => State(s.todos :+ t)) >>
        $.props.flatMap(P => P.model.add(t))

    def render(S: State) =
      <.div(
        <.form(
          ^.onSubmit ==> onSubmit,
          <.button("Add #", S.todos.length + 1)),
        <.ul(S.todos.map(t => <.li(t.text)): _*))

  }

  val component = ScalaComponent.builder[Props]("Todo")
    .initialStateFromProps(p => State(p.model.todos))
    .renderBackend[Backend]
    .build

  def apply(model: TodoModel) = component(Props(model))
}

object Test {
  val model = new TodoModel()

  def main(args: Array[String]): Unit = {
    TodoPage.apply(model).renderIntoDOM(dom.document.getElementById("mount-node"))
  }
}

空的,没有点击按钮
空的

首先点击按钮
第一次点击

第二次点击按钮
第二次点击

4

1 回答 1

4

在您的第一个片段中有一个错误:

在这里,您有一个todos在纯上下文中访问的变量(非纯):

def mod(f: Seq[Todo] => Seq[Todo]): Callback = {
  val newTodos = f(todos)
  Callback(todos = newTodos)

杂质应在Callback. 即使在回调之外读取变量也是不安全的,所以它应该是:

def mod(f: Seq[Todo] => Seq[Todo]): Callback =
  Callback(todos = f(todos))

(请参阅 scalajs-react 的Ref.scala安全使用变量的示例。)

其次,关于您的较大片段,scalajs-react 对 FP 非常友好,但这是尝试使用它的非常规的方式,并且存在一些重大问题:

  • StateInterpreter.interpet不是参照透明的;这背后有共享的全球状态。FP 测试失败。不再是合法的自然转化。
  • 您正在分别跟踪两组相同的状态:组件状态和状态TodoModel(不纯,未通过 FP 测试)。这种方法不仅是多余的,并且存在两种状态不同步的风险,而且还降低了组件的可重用性;想象一下,您决定在同一个屏幕上为相同的数据绘制两次——它们将不同步。最好保持组件无状态和纯净。
  • 如果您要将自由结构转换为组件效果,最好将其转换为状态单子,请参见此处的示例。

你正在学习免费的 monad 和 scalajs-react,这真的非常棒。FP 将使您的整个程序非常非常易于推理并防止行为中出现令人困惑的意外,但您必须不走捷径并确保您保持所有代码的纯净。任何杂质都会使整个堆栈一直到入口点不纯,并从这些层中删除那些不错的可靠 FP 属性。我建议使用以上几点作为起点,使所有内容尽可能纯净,然后我认为您会发现该错误消失了,或者至少很容易检测到。干杯

于 2018-03-15T08:39:51.967 回答