4

在没有可变状态的 FP 中,每个操作都会返回一个新的世界状态。给定:我有一个联系人列表和一个个人联系人。

我将 Dirk 添加到我的通讯录中。Dirk 是我通讯录的一个孩子。我的通讯录是 Dirk 的父级。因为我不能同时设置两个参考,所以我进退两难。父子关系应该定义一个无限循环,我可以永远从父子到子到父到同一个子。

使用 JavaScript 语法:

var addresses = new AddressBook();
var dirk = new Contact(addresses, 'Dirk', ...);

在第二行,我在没有 Dirk 的情况下传入通讯录。Dirk 有一个地址簿的父引用,其中没有他。

我怀疑答案,但我想确定。我真的要改变状态以正确设置它还是有一些我忽略的技术?

4

3 回答 3

3

如果您希望这种事情像在您的 JavaScript 示例中那样工作(因此您可以直接在实际孩子中查找实际地址簿),您必须使地址簿可变。这甚至不是因为最初创建父子节点(可以管理,在某些功能语言中比在其他语言中更容易),而是因为如果您继续添加对地址簿的进一步引用,旧条目将仍然持有他们过时版本的通讯录。

在 Clojure 中,在这种情况下,很容易使用 Atom 或 Ref 来保存整个地址簿,然后在每个子节点中也放置一个指向地址簿的 Atom 或 Ref,但 Clojure 引用类型只是真正设计用于保存不可变数据嵌套它们可能会导致问题。

更好的解决方案是为您的实体提供符号名称(关键字、数字、UUID 都可以)并将它们存储在某处的地图中。使用单个原子可能如下所示:

(def state (atom {:contacts {:dirk ...}
                  :address-books {}}))

然后您可以将 Dirk 添加到一个新的地址簿(以哈希映射的形式一路创建),如下所示:

(swap! state (fn [state-map]
               (update-in state-map [:address-book :my-address-book]
                 (fn [abook]
                   (let [entries (get abook :entries [])]
                     (assoc abook :entries (conj entries :dirk)))))))

请注意,这会将 Dirk 以符号引用 ( :dirk) 的形式添加到地址簿中,以便在键下的顶级状态映射中查找:contacts。如果您还希望 Dirk 联系人维护其所属的地址簿列表,请使用进一步update-in向 Dirk 联系人添加适当的信息,可能会删除一些嵌套->

(-> state-map
    (update-in [...] ...)
    (update-in [...] ...))
于 2013-07-13T01:15:02.207 回答
2

由于您通常提到了 FP,我将添加一个额外的视图 -惰性评估。我既不擅长 JS 也不擅长 Clojure,所以我会用不同的语言给出例子,但也许也可以使用这个想法。

许多函数式语言都有惰性求值的概念。这意味着仅在实际需要时才计算值。自然,这种惰性计算必须是引用透明的(必须不依赖于外部信息,必须没有可变状态和副作用等),因为我们永远不知道何时(或是否有)它们会被评估。

例如,在 Haskell 中所有的计算都是惰性的,所以我们可以只写

let address = Address contact {- other fields -}
    contact = Contact address {- other fields -}
 in {- some expression that uses address and contact -}

或者我们可以创建一个列表,其尾部是列表本身。结果是一个包含重复元素的无限列表,它只占用恒定的内存量

infList :: a -> [a]
infList x = l
  where l = x : l

有关更多信息,请参阅Haskell Wiki 上的结缘

如果一种语言缺乏惰性求值,你可以自己实现它:如果一个值还没有被请求,计算它,存储它并返回它。下一次,只需返回之前计算的内容。当然,你需要它的可变性,但是可变状态隐藏在软件组件中,如果计算是引用透明的,那么可变性永远不会泄露出去。

于 2013-07-13T14:28:32.950 回答
2

你可以用同样的想法在有和没有可变状态的情况下做到这一点。您运行一个函数,该函数采用地址的原始状态和您想要添加的状态,然后返回一组新地址,包括新状态。当然,它不会破坏原件,因为有人可能会看到它。

定义基本地址簿:

user> (def addresses [])                         
#'user/addresses                        

定义一个包含新值的新地址簿:

user> (def book-with-dirk 
        (conj addresses {:name "dirk" :address "123 internet st."}))
#'user/book-with-dirk
user> book-with-dirk
[{:name "dirk", :address "123 internet st."}]

这不会更改基本地址簿,而是创建一个新的地址簿,将原始地址簿与 dirk 的新值有效地结合起来。所以地址还是一样的。

user> addresses    
[]                                              

您还可以使用托管可变状态以功能方式维护名为地址的身份的内容。如果有人在看,地址原子中的原始值仍然存在(否则为 GCd)

user> (def addresses (atom []))      
#'user/addresses

制作一个新的地址簿,就像上面一样包含 dirk,除了这个地址簿还在地址标识中创建下一个值:

user> (def book-with-dirk (swap! addresses conj {:name "dirk" :address "123 internet st."}))
#'user/book-with-dirk

现在 book-with-dirk 是一个包含带有 dirk 的书的值。

user> book-with-dirk                                       
[{:name "dirk", :address "123 internet st."}]

并且地址也包含新值。

user> @addresses
[{:name "dirk", :address "123 internet st."}]

如果我再添加 Joe,book-with-dirk 将不会改变

user> (swap! addresses conj {:name "Joe" :address "321 internet st."})
[{:name "dirk", :address "123 internet st."} 
 {:name "Joe", :address "321 internet st."}]

user> book-with-dirk
[{:name "dirk", :address "123 internet st."}]
于 2013-07-13T00:41:03.177 回答