【问题标题】:How to create mutually referencing data structures in Haskell?如何在 Haskell 中创建相互引用的数据结构?
【发布时间】:2021-01-25 01:26:52
【问题描述】:

我利用了这样一个事实,即当 JVM 创建一个对象(不可变或不可变)时,它的指针是在其字段初始化之前创建的。

这让我可以创建这样的东西:

class BackRefdNode(val parent:Option[BackRefdNode],
node:ClassicNode){
  val children=node.children.map{c=> 
    new BackRefdNode(Some(this), c)
}

Haskell 不是这种情况(据我所知),如果是这种情况,Haskell 不会给我工具来利用它(我没有提到“this”)。

所以我想知道,我如何在 Haskell 中实现这一点?

我认为也许 fix 函数可以解决问题,但这实际上不会给我一个“this”引用,而是一个对 thunk 的引用,在计算时,理论上它的结构与创建了 BackRefdNode

【问题讨论】:

标签: scala haskell code-translation cyclic-reference


【解决方案1】:

不是 Scala 代码

trait ClassicNode {
  def children: List[ClassicNode]
}

class BackRefdNode(val parent: Option[BackRefdNode],
                   node: ClassicNode) {
  val children = node.children.map { c =>
    new BackRefdNode(Some(this), c)
  }
}

类似于 Haskell 代码

data ClassicNode

data BackRefdNode = BRN { parent :: Maybe BackRefdNode, node :: ClassicNode }

children1 :: ClassicNode -> [ClassicNode]
children1 _ = undefined

children :: BackRefdNode -> [BackRefdNode]
children this = map (\c -> BRN (Just this) c) (children1 (node this))

?

或者在 Haskell 中使用类型类

class GetChildren a where
  children :: a -> [a]

data ClassicNode

data BackRefdNode = BRN { parent :: Maybe BackRefdNode, node :: ClassicNode }

instance GetChildren ClassicNode where
  children _ = undefined

instance GetChildren BackRefdNode where
  children this = map (\c -> BRN (Just this) c) (children (node this))

即双重翻译成Scala

trait ClassicNode

class BackRefdNode(val parent: Option[BackRefdNode],
                   val node: ClassicNode)

trait GetChildren[A] {
  def children(a: A): List[A]
}
object GetChildren {
  implicit val classicNodeGetChildren: GetChildren[ClassicNode] = _ => ???
  implicit val backRefdNodeGetChildren: GetChildren[BackRefdNode] = a =>
    a.node.children.map { c =>
      new BackRefdNode(Some(a), c)
    }
}

implicit class GetChildrenOps[A](val a: A) extends AnyVal {
  def children(implicit getChildren: GetChildren[A]): List[A] = 
    getChildren.children(a)
}

或者您的意思是,在 Java/Scala 中,this 上的调度是动态的(与类型类的静态调度相反)。那么请看

Dynamic dispatch in Haskell

Is the dispatch of a Haskell TypeClass dynamic?

Does GHC use dynamic dispatch with existential types?

【讨论】:

  • @user 好吧,如果一个函数是纯函数,您可以根据需要重新运行它多次,但如果输入相同,则输出相同。 children“成为BRN 的一部分”是输入参数this 的函数。我可以看到的区别是函数参数是静态解析还是动态解析。
  • @user 好吧,this.children“每次都必须计算”当this 更改时:backRefdNode1.children, backRefdNode2.children ... 可以添加记忆(例如通过State )。
  • @user “Bergi 的回答似乎更符合 OP 的意图” 好吧,我不确定我是否理解 OP 的意图。该代码对我来说似乎具有误导性(因为我不确定我是否理解基于 Scala/JVM 的语言和 Haskell OP 之间的主要区别是什么意思)。
  • @user 关于Bergi的回答,主要是关于懒惰,但我们也可以在Scala中做懒惰(lazy val,别名=>Stream/LazyList ...),这不是默认行为。
  • @user 无论如何,第一次必须计算它。好的,然后是关于记忆的。正如我所说,可以添加记忆。所以我仍然不确定这是什么主要区别:)
【解决方案2】:

更一般地说,您可以使用 Future/Promise 组合(或 Cats Effect 中的 Deferred)来创建这种循环而不利用 JVM 行为:

class BackRefdNode(val parent: Option[BackRefdNode]) {
  private[this] val leftPromise: Promise[Option[BackRefdNode]]()
  private[this] val rightPromise: Promise[Option[BackRefdNode]]()

  // leftChild.value will be:
  //  None if we haven't completed yet
  //  Some(Success(None)) if there will never be a left child
  //  Some(Success(Some(node))) if node is the left child
  // (technically this Future never fails, but that's an implementation detail
  def leftChild: Future[Option[BackRefdNode]] = leftPromise.future
  def rightChild: Future[Option[BackRefdNode]] = rightPromise.future

  def leftChildIs(nodeOpt: Option[BackRefdNode]): Try[Unit] =
    Try { leftPromise.success(nodeOpt) }
  def rightChildIs(node: Option[BackRefdNode]): Try[Unit] =
    Try { rightPromise.success(nodeOpt) }
}

您通过将循环的一个方向设为链单子 (-ish) 来付出代价,但请注意,您根本不依赖于 this 或其他变幻莫测的 JVM 实现。

因此,如果有与 Scala 的 Promise/Future 等效的 Haskell (perhaps Data.Promise?),则翻译应该很简单。

【讨论】:

  • 我认为你不需要承诺,Haskell 反正很懒
【解决方案3】:

Haskell 实际上在这里更进一步。它是惰性求值的,这意味着您可以在初始化之前获得对 anything 的引用,而不仅仅是带有字段的对象。使用数据类型

data ClassicNode = ClassicNode { children :: [ClassicNode] }
data BackRefdNode = BackRefdNode { parent :: Maybe BackRefdNode, children :: [BackRefdNode] }

你可以创建一个函数

backRefdNode :: Maybe BackRefdNode -> ClassicNode -> BackRefdNode
backRefdNode parent node = let result = BackRefdNode parent (backRefdNode result <$> children node)
                           in result

注意result 在初始化result 本身的表达式中是如何被引用的。这工作得非常好,并且有效地共享树对象与它们之间的循环引用。

比 Scala 更难的是解开这个数据结构,因为在 Haskell 中没有引用 equality。 BackRefdNode 的每个孩子都将其作为其父母的不变量无法测试,必须从构造中证明。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-18
    • 1970-01-01
    相关资源
    最近更新 更多