【问题标题】:Understanding Free monad in scalaz理解 scalaz 中的 Free monad
【发布时间】:2018-02-26 16:20:19
【问题描述】:

我正在Scalaz 中尝试使用Free monad,并尝试构建简单的解释器来解析和评估表达式,例如:

dec(inc(dec(dec(10)))

其中dec 表示递减,inc 表示递增。这是我得到的:

trait Interpreter[A]
case class V[A](a: A) extends Interpreter[A]

object Inc {
  private[this] final val pattern = Pattern.compile("^inc\\((.*)\\)$")
  def unapply(arg: String): Option[String] = {
    val m = pattern.matcher(arg)
    if(m.find()){
      Some(m.group(1))
    } else None
  }
}

object Dec {
  private[this] final val pattern = Pattern.compile("^dec\\((.*)\\)$")
  def unapply(arg: String): Option[String] = {
    val m = pattern.matcher(arg)
    if(m.find()){
      Some(m.group(1))
    } else None
  }
}

object Val {
  def unapply(arg: String): Option[Int] =
    if(arg.matches("^[0-9]+$")) Some(Integer.valueOf(arg))
    else None
}

现在这就是我构建 AST 所需的全部内容。目前看起来如下:

def buildAst(expression: String): Free[Interpreter, Int] = 
   expression match {
     case Inc(arg) => inc(buildAst(arg))
     case Dec(arg) => dec(buildAst(arg))
     case Val(arg) => value(arg)
}

private def inc(i: Free[Interpreter, Int]) = i.map(_ + 1)
private def dec(d: Free[Interpreter, Int]) = d.map(_ - 1)
private def value(v: Int): Free[Interpreter, Int] = Free.liftF(V(v))

现在在测试应用程序时:

object Test extends App{

  val expression = "inc(dec(inc(inc(inc(dec(10))))))"

  val naturalTransform = new (Interpreter ~> Id) {
    override def apply[A](fa: Interpreter[A]): Id[A] = fa match {
      case V(a) => a
    }
  }

  println(buildAst(expression).foldMap(naturalTransform)) //prints 12
}

而且它工作得非常好(我不确定它是否是scalaz 风格)。

问题是提取器对象IncDecVal 感觉就像样板代码。有没有办法减少这样的代码重复。

如果支持的功能越来越多,这肯定会成为问题。

【问题讨论】:

  • 解析字符串不是Free 的好用例。它旨在描述许多可以排序的操作(DB 调用、FS 操作),而无需定义其实现,例如无需更改实际代码即可轻松测试和在同步/异步执行之间切换
  • @OlegPyzhcov 在我的情况下,V 不会是Interpreter 的唯一实现。将有另一个具有副作用(从数据库中检索)。这就是为什么我认为使用Free 是个好主意。
  • 整个问题似乎出在自上而下的解析器上,而不是Freecase class V[A](a: A)Interpreter 特征中的 only 操作,可以用不同的解释有意义地替换。无需输入的单一操作:在某种程度上看起来像是一种相当不完整的嵌入式领域特定语言?
  • @AndreyTyukin 仍然不清楚为什么Free 在这里没有用。你能解释一下我对Free的理解哪里错了吗?问题是case class V(a) 可能被解释为数据库中的行数。因此它的价值事先是未知的。所以使用Free 可能对测试有用(避免与数据库相关的副作用)。
  • @St.Antario 我没有声称Free 没有用。我刚刚指出,问题似乎是 95% 自上而下解析和 5% 关于Free。整个pattern-compile-pattern-matching-group-match-case 的东西与Free 无关,所以Free 不是这里的样板的原因。此外,如果V(a) 应该被解释为存储A 类型对象信息的行数,那么V 应该是case class V[A](rowIdx: Int) extends Interpreter[A],否则我不明白你为什么想要传入A 类型的值以获取A

标签: scala scalaz free-monad


【解决方案1】:

免费的 monad 正在创建一些样板文件,这是事实。但是,如果您愿意遵守一些约定,您可以使用Freasy Monad 重写解释器:

@free trait Interpreter {
  type InterpreterF[A] = Free[InterpreterADT, A]
  sealed trait InterpreterADT[A]


  def inc(arg: InterpreterF[Int]): InterpreterF[Int]
  def dec(arg: InterpreterF[Int]): InterpreterF[Int]
  def value(arg: Int): InterpreterF[Int]
}

这将生成所有案例类并在它们上进行匹配。解释器只是一个需要实现的特征。

但是,您在 unapply 中已经有一些逻辑 - 因此您必须拆分解析和执行逻辑:

import Interpreter.ops._
val incP = """^inc\\((.*)\\)$""".r
val decP = """^dec\\((.*)\\)$""".r
val valP = """^val\\((.*)\\)$""".r
def buildAst(expression: String): InterpreterF[Int] = expression match {
  case incP(arg) => inc(buildAst(arg))
  case decP(arg) => dec(buildAst(arg))
  case valP(arg) => value(arg.toInt)
}

然后你可以实现一个实际的解释器:

val impureInterpreter = new Interpreter.Interp[Id] {
  def inc(arg: Int): Int = arg+1
  def dec(arg: Int): Int = arg-1
  def value(arg: Int): Int = arg
}

并运行它:

impureInterpreter.run(buildAst(expression))

我承认这更像是一个伪代码,而不是经过测试的工作解决方案,但它应该提供一个大致的概念。另一个使用类似想法的库是 Freestyle,但他们使用自己的免费 monads 实现,而不是依赖于 cat/scalaz。

所以,我想说,只要您对拆分解析和解释没有问题,就可以删除一些样板文件。当然不是所有的都可以被删除——你必须在你的Interpreter代数上声明可能的操作,并且你必须自己实现解释器。

【讨论】:

  • 感谢您的回答。但是我仍然不清楚的是何时使用Return 节点以及何时使用Suspend 节点。能否请您简要说明一下?
  • 对我来说 return 和suspend 是一种泄漏的“实现细节”,return 已经包装了计算机纯值,suspend 使用按名称参数,以便稍后会懒惰地计算 Free 值 -这样你可以避免堆栈溢出,例如创建任意深度的嵌套结构。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多