【问题标题】:F# Hiding stuff?F# 隐藏东西?
【发布时间】:2017-08-06 21:06:03
【问题描述】:

对不起,糟糕的标题可以找到合适的词。

目前我正在尝试制作一些 F# 可以在任何情况下使用的基本数据结构,第一个是双链表。

我的问题不是如何实现它,而是是否有一个为什么要隐藏数据结构的丑陋。简短的形式,我有一些作为节点的东西可能看起来像

type Node<'N> = 
  | (node<'N> ref, 'N, node<'N>)
  | Empty

当我们有超过三个列表项时分析这一点很容易出错。那么有没有一种方法可以让库的用户看到“外观”,使其看起来更像.NET 中的列表。我要求一种不依赖于已经建立的数据类型的方法,并且不返回字符串外观(“...”)

【问题讨论】:

  • 快速评论:双向链表不适合 F# 的“不可变数据”设计理念。 “不可变数据”理念表明,addremove 之类的操作不应就地更改现有列表,而应返回添加或删除项目的 new 列表。任何引用 old 列表的代码仍然具有该引用,并且该引用中的数据未更改。这可以防止大量与线程和意外行为相关的错误。此外,通过单链表,您可以使用结构共享来...(续)
  • ... 确保将项目添加到列表中的工作时间为 O(1),因为“新”头可以指向“旧”列表,然后就完成了。对于双向链表,这是不可能的,因为需要复制和重写“旧”列表的头部以指向“新”头部,然后“旧”列表的第二项将必须被复制和重写,很快你就复制了整个列表,一个 O(N) 操作来添加单个项目(在单链接实现中是 O(1))。请参阅stackoverflow.com/questions/7709632/… 了解更多信息。
  • 我知道函数式编程是关于不可变数据结构的。但我现在并没有尝试使用 F# 的功能部分,我想要关闭它的唯一原因是我将来可能会使用它。但是我的双链表在 O(1) 中运行,如果我们假设只有数据而不是“指针”是不可变的,那么你仍然可以在 O(1) 时间内复制孔列表,因为你唯一要做的就是当添加或删除正在更改或将指针(参考单元格)指向旧列表时执行...(续)
  • 那么我们仍然拥有旧列表的副本,而无需再次复制每个元素。

标签: f#


【解决方案1】:

您可以将 F# 类型包装在一个类中,并隐藏实际的 F# 表示。例如,如果你想要一个超级简单的可变列表,你可以这样做:

type private MyListNode<'T> = 
  | Empty 
  | Cons of 'T * MyListNode<'T>

type MyList<'T>() =
  let mutable nodes = Empty
  member x.Prepend(el) = nodes <- Cons(el, nodes)
  member x.ToArray() = 
    let rec loop el = seq {
      match el with 
      | Empty -> ()
      | Cons(x, xs) -> 
          yield x
          yield! loop xs }
    loop nodes |> Array.ofSeq

C# 用户可以使用MyList,这是具有PrependToArray 方法的普通类。 MyListNode 类型是私有的(隐藏在您的 F# 库中),C# 用户永远不会看到它。

【讨论】:

    【解决方案2】:

    这不是问题的答案,而是 cmets 的答案,因为我要说的内容需要图表,因此在 cmets 中不起作用。

    锦写道:

    但是我的双链表在 O(1) 中运行,如果我们假设只有数据而不是“指针”是不可变的,那么你仍然可以在 O(1) 时间内复制整个列表,因为您在添加或删除时所做的唯一事情是更改或创建指向旧列表的指针(参考单元格),然后我们仍然拥有旧列表的副本,而无需再次复制每个元素。

    如果您尝试这样做,您会发现对于双向链表,实际上您不能保留旧的列表指针。原因如下。

    使用单链表,您可以在 O(1) 时间内预先添加到列表中,同时保持指向旧列表的所有指针不变。这是一个例子:

    包含三个项目的旧列表:

    添加新头后的新列表:

    注意其他代码的引用是如何保持不变的。其他代码引用的旧列表是["Item 1"; "Item 2"; "Item 3"]。新列表是["New head item"; "Item 1"; "Item 2"; "Item 3"]。但是代码的不同部分所持有的引用仍然指向一个格式良好的列表。正如您即将看到的那样,“格式良好”的部分很重要。

    使用双向链表,事情会变得更加复杂——事实证明,不可能保持不变性并且 O(1) 时间。首先让我们看一下包含三个项目的旧列表:

    这是一个结构良好的双向链表。它遵循所有格式良好的双向链表都应遵循的以下属性:

    1. 所有节点都有一个 Fwd 和 Back 指针。
    2. 除头节点外的所有节点都有一个有效的(非空)反向指针。只有头节点的 Back 指针是null
    3. 除尾节点外的所有节点都有一个有效的(非空)Fwd 指针。只有尾节点的 Fwd 指针是null
    4. 从不是尾部的任何节点,前进然后后退应该会带您回到您开始的同一节点
    5. 从不是头部的任何节点,后退然后前进应该会将您带回到 您开始的同一节点

    现在,我们如何添加一个新的头项,同时仍然确保来自其他代码的引用继续指向一个格式良好的双向链表?

    这是第一次尝试。我们添加新的头项,调整它的 Fwd 指针指向“旧”头节点,并重写该节点的 Back 指针指向新的头节点:

    这仍然是一个格式正确的列表,您可以轻松验证。所有五个属性仍然适用于每个节点。但是等等!来自代码其他部分的引用已经从下面的列表中更改了!之前它指向三个项目的列表,现在它指向 四个 项目列表的第二个项目!如果其他代码只是向前迭代,它不会注意到变化。但是当它试图向后迭代时,它会注意到有一个以前不存在的新头部项目! 我们已经打破了不变性承诺。不变性是对使用我们数据结构的其他代码的保证,即“如果你引用了这个数据结构,你看到的数据永远不会从你下面改变出来。” 我们刚刚打破了这个承诺:旧代码用于查看列表["Item 1"; "Item 2"; "Item 3"],现在它看到列表["New head item"; "Item 1"; "Item 2"; "Item 3"]

    好的,那么。有没有办法保持这个承诺,而不改变其他代码看到的内容?好吧,我们可以尝试不重写那个旧的头节点;这样旧代码仍然会看到一个包含三个项目的双向链表,每个人都很高兴,对吧?好吧,让我们看看如果我们这样做会是什么样子:

    太棒了:其他代码仍然看到与以前完全相同的双向链表,并且无法从旧列表到新的头节点。因此,其他代码的任何部分如果试图从列表的头部向后移动,就会发现头部仍然是null,就像它应该的那样。但是等等:一个格式良好的列表的五个属性呢?好吧,事实证明我们已经违反了属性 #4:从头节点开始,向前然后向后返回以空指针结束,不是我们开始的节点。所以我们不再有一个格式正确的列表:太糟糕了。

    好的,所以这种方法行不通。我们还能尝试什么。嗯……嘿!我有个主意!让我们复制旧的头节点,并在我们不理会旧的头节点的同时调整副本!这仍然是 O(1),因为我们知道我们只复制一个节点。然后其他代码看到的正是它以前看到的内容,一个包含三个项目的列表,但新列表有四个项目。杰出的!正是我们想要的,对吧?好吧,我们来看看:

    好的,这行得通吗?好吧,其他代码引用了旧的、未更改的头节点,所以这很好:它永远不会意外地看到新数据,所以它仍然可以继续看到它曾经拥有的东西,一个包含三个项目的列表。好的。从新的头节点开始,我们可以前进和后退,最终回到我们开始的地方,这很好……但是等等……不,还是有问题。从 item 1 的副本 节点,向前然后返回将我们带到 old“item 1”节点,而不是“item 1 的副本”节点。所以我们还是违反了良构列表的性质,而这个列表也不是良构的。

    对此也有一个答案:复制其中包含项目 2 的节点。我已经厌倦了绘制图表并且这个答案越来越长,所以我会让你自己解决这个问题 - 但你很快就会看到,带有第 2 项副本的节点有同样的问题之前:前进和后退会将您带到“旧”项目 2。(否则您已经调整了“旧”项目 2 节点,从而破坏了不变性承诺,因为其他代码现在可以通过以下方式查看“新”数据一些 Fwd 和/或 Back 操作)。

    但是那个也有一个解决方案:也复制第 3 项。我也不会画那个图,但你可以自己画出来。您会发现,一旦将项目 1、2、 3 复制到新列表中,您就成功地满足了 both 不变性承诺,格式良好的列表的所有属性。其他代码仍然看到未修改的旧列表,而新列表中有四个项目。唯一的问题是,您必须复制 列表中的每个项目——根据定义,这是一个 O(N) 操作——才能获得此结果。

    总结:单链表具有三个属性:

    1. 您可以在 O(1) 时间内预先添加项目。
    2. 引用旧列表的其他代码在您执行前置操作后仍会看到相同的数据。
    3. 旧列表和新列表都是格式正确的。

    但是,使用双向链表,您只能拥有这三个属性中的 两个。您可以进行 O(1) 前置操作并维护格式正确的列表,但随后任何其他代码都会看到列表数据发生变化。或者您可以在前面添加 O(1) 并且仍然让其他代码看到与以前相同的数据,但是您的列表将不再是格式正确的。或者,通过复制列表中的每个节点,您可以让其他代码仍然看到与以前相同的数据,并且您的新列表将是格式良好的——但您必须执行 O(N) 操作才能实现这个结果。

    这就是为什么我说不可能有一个具有 O(1) 操作的不可变双向链表。

    【讨论】:

    • 啊……我明白你的意思了,对不起。但正如我所说,这不是我正在尝试构建的 F# 的功能部分:) 目前我仍然不关心这个问题。我将制作自​​己的 Fib 堆库,只要我知道问题不应该是一个 ;)
    • 不用担心。在 F# 和函数式代码中编写 OOP 风格的代码是完全可能的,并且有时可能是一步一步学习该语言的好方法。最终,如果您不利用 F# 的功能部分,那么您只是在编写“语法不熟悉的 C#”,并且您不会从 F# 中获得太多优势。但作为一种学习方式,它可能是一个很好的方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-04
    • 1970-01-01
    • 1970-01-01
    • 2015-07-05
    • 2017-12-04
    • 2019-01-05
    相关资源
    最近更新 更多