How to create mutually referencing data structures in Haskell?
我利用了这样一个事实,即当 JVM 创建一个对象(不可变或不可变)时,它的指针是在其字段初始化之前创建的。
这让我可以创建这样的东西:
1
2 3 4 5 |
Haskell 不是这种情况(据我所知),如果是这种情况,Haskell 不会给我工具来利用它(我没有提到”this”)。
所以我想知道,我如何在 Haskell 中实现这一点?
我认为也许 th fix 函数可以解决问题,但这实际上不会给我一个”this”引用,而是一个对 thunk 的引用,在计算时,理论上它的结构与创建了 BackRefdNode
- Haskell 确实允许使用 let 进行循环引用。
- 相关:如何实现双向链表
Haskell 实际上在这里更进一步。它是惰性求值的,这意味着您可以在初始化之前获得对任何东西的引用,而不仅仅是带有字段的对象。使用数据类型
1
2 |
data ClassicNode = ClassicNode { children :: [ClassicNode] }
data BackRefdNode = BackRefdNode { parent :: Maybe BackRefdNode, children :: [BackRefdNode] } |
你可以创建一个函数
1
2 3 |
backRefdNode :: Maybe BackRefdNode –> ClassicNode –> BackRefdNode
backRefdNode parent node = let result = BackRefdNode parent (backRefdNode result <$> children node) in result |
请注意在初始化 result 本身的表达式中如何引用 result。这可以很好地工作并且有效地共享具有循环引用的树对象。
比 Scala 更难的是解开这种数据结构,因为 Haskell 中没有参考 eq 实体。 BackRefdNode 的每个孩子都将其作为其父母的不变量无法测试,必须从构造中证明。
不是 Scala 代码
1
2 3 4 5 6 7 8 9 10 |
类似于 Haskell 代码
1
2 3 4 5 6 7 8 9 |
?
或者使用 Haskell 中的类型类
1
2 3 4 5 6 7 8 9 10 11 12 |
class GetChildren a where
children :: a –> [a] data ClassicNode data BackRefdNode = BRN { parent :: Maybe BackRefdNode, node :: ClassicNode } instance GetChildren ClassicNode where instance GetChildren BackRefdNode where |
即双重翻译成 Scala
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
trait ClassicNode
class BackRefdNode(val parent: Option[BackRefdNode], trait GetChildren[A] { implicit class GetChildrenOps[A](val a: A) extends AnyVal { |
或者你的意思是在 Java/Scala 中 this 上的调度是动态的(与类型类的静态调度相反)。那么请看
Haskell 中的动态调度
Haskell TypeClass 的调度是动态的吗?
GHC 是否对存在类型使用动态调度?
- @user好吧,如果一个函数是纯函数,您可以根据需要多次重新运行它,但是如果输入相同,则输出是相同的。 children “作为 BRN 的一部分” 是输入参数 this 的函数。我可以看到的区别是函数参数是静态解析还是动态解析。
- @user 好吧,当 this 更改时, this.children “必须每次都计算”: backRefdNode1.children, backRefdNode2.children …可以添加记忆(例如通过 State )。
- @user “Bergi\\ 的回答似乎更符合 OP\\ 的意图” 好吧,我不确定我是否理解 OP\\ 的意图。该代码对我来说似乎具有误导性(因为我不确定我是否理解基于 Scala/JVM 的语言和 Haskell OP 之间的主要区别是什么意思)。
- @user关于Bergi的回答主要是关于懒惰,但我们也可以在Scala中做懒惰(lazy val,按名称=>,Stream/LazyList …),这是只是不是默认行为。
- @user 无论如何,第一次必须计算它。好的,然后是关于记忆的。正如我所说,可以添加备忘录。所以我仍然不确定这是什么主要区别:)
- 嗯,我只是在挑剔
更一般地说,您可以在不利用 Future/Promise 组合(或 Cats Effect 中的 Deferred)利用 JVM 行为的情况下创建这种循环:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class BackRefdNode(val parent: Option[BackRefdNode]) {
private[this] val leftPromise: Promise[Option[BackRefdNode]]() private[this] val rightPromise: Promise[Option[BackRefdNode]]() // leftChild.value will be: def leftChildIs(nodeOpt: Option[BackRefdNode]): Try[Unit] = |
您通过将循环的一个方向设为链单子(-ish)来付出代价,但请注意,您根本不依赖于 this 或其他变幻莫测的 JVM 实现。
所以如果有一个 Haskell 等价物(也许是 Data.Promise?) Scala\\ 的 Promise/Future,翻译应该很简单。
- 我认为你不需要Promise,Haskell 是懒惰的
来源:https://www.codenong.com/64288942/