在函数式编程中,如何设置相互关系?

4
在函数式编程中,不存在可变状态,每个操作都会返回一个新的世界状态。现在假设我有一个联系人列表和一个单独的联系人。
我要将Dirk添加到我的地址簿中。Dirk是我的地址簿的子对象,而我的地址簿是Dirk的父对象。但由于无法同时设置两个引用,我陷入了困境。父子关系应该定义一个无限循环,使得我可以永远从父对象走到子对象再走回同一父对象。
使用JavaScript语法:
var addresses = new AddressBook();
var dirk = new Contact(addresses, 'Dirk', ...);

在第二行,我传递了没有Dirk的通讯录。 Dirk有一个父引用指向没有他的通讯录。

我怀疑答案,但我想确认一下。我是否实际上会改变状态来正确设置这个,还是有我忽略的一些技巧?

3个回答

3
如果您希望这种情况能够像JavaScript示例一样正常工作(因此您可以直接查找实际的通讯录并查看实际的子项),那么您必须使通讯录具有可变性。这甚至不是因为父项和子项的初始创建(在某些函数式编程语言中可以更轻松地管理),而是因为如果您继续添加到通讯录的其他引用,旧条目仍将保留其过时版本的通讯录。
在Clojure中,使用Atom或Ref来保存整个地址簿并在每个子项中放置指向地址簿的Atom或Ref是很诱人的,但是Clojure引用类型真正设计用于保存不可变数据,嵌套它们可能会导致问题。
更好的解决方法是给您的实体命名符号名称(关键字、数字、UUID都可以),并将它们存储在某个映射中。使用单个Atom可能如下所示:
(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 [...] ...))

在从咖啡馆开车回家的路上,我想到了你建议使用指针可能与此有关。 - Mario
1
那整个复杂/冗长的 update-in/swap/fn/fn 是不是只是写 (swap! state update-in [:address-book :my-address-book :entries] (fnil conj []) :dirk) 的一种复杂方式? - amalloy

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    
[]                                              

你可以使用受管理的可变状态以函数式的方式维护身份命名地址的内容。如果有人查看它,地址原始值仍然存在(否则将进行垃圾回收)。
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."}]

感谢您的帮助。我对Schema的理解比Clojure更好一些,但我认为好的答案可能来自Clojure社区。感谢您的参与。 :) - Mario

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上的 Tying the knot
如果一种语言缺乏惰性求值,你可以自己实现它:如果一个值还没有被请求,计算它,存储它并返回它。下一次,只需返回之前计算过的内容。当然,你需要可变性来实现它,但是可变状态被隐藏在软件组件内部,如果计算具有引用透明性,则可变性永远不会泄漏出去。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接