【译】Brave Clojure 第十章:Clojure哲学体系:Atom, Refs, Vars与抱抱僵尸

本文是我对Clojure书籍 CLOJURE FOR THE BRAVE AND TRUE第十章Clojure Metaphysics: Atoms, Refs, Vars, and Cuddle Zombies 做的翻译。翻译形式,中英对照,英文引用跟着中文翻译。如有错误,在所难免,欢迎指正。

其他章的翻译在这里

译文开始。


The Three Concurrency Goblins are all spawned from the same pit of evil: shared access to mutable state. You can see this in the reference cell discussion in Chapter 9. When two threads make uncoordinated changes to the reference cell, the result is unpredictable.

三个并发哥布林都产生于同样的邪恶陷阱:共享可更改状态的访问权。在第9章的引用单元的讨论中可以看到。当两个线程对一个引用单元进行不协调地修改的时候,结果是不可预料的。

Rich Hickey designed Clojure to specifically address the problems that develop from shared access to mutable state. In fact, Clojure embodies a very clear conception of state that makes it inherently safer for concurrency than most popular programming languages. It’s safe all the way down to its meta-freakin-physics.

Rich Hickey对Clojure进行了特别设计,以解决共享可更改状态的访问权带来的问题。事实上,Clojure体现了一种非常清晰的状态概念,使Clojure在并发上天生就比大多数流行的编程语言安全。它的一切都是安全的,连同它的哲学体系也是安全的。

In this chapter, you’ll learn about Clojure’s underlying metaphysics, as compared to the metaphysics of typical object-oriented (OO) languages. Learning this philosophy will prepare you to handle Clojure’s remaining concurrency tools, the atom, ref, and var reference types. (Clojure has one additional reference type, agents, which this book doesn’t cover.) Each of these types enables you to safely perform state-modifying operations concurrently. You’ll also learn about easy ways to make your program more efficient without introducing state at all.

这章将学习Clojure的哲学体系,并与其他面相对象语言的哲学体系进行比较。这将使你做好准备,运用Clojure的并发工具,atom, ref, 和 var 引用类型。(Clojure还有一个引用类型,agents,本书没有讲解。)每个类型都能让你安全地并发执行状态修改操作。这章我们还会学习一些方法,使你完全不用引入状态,从而更高效地编程。

Metaphysics attempts to answer two basic questions in the broadest possible terms:

哲学体系从尽可能广泛的方面,尝试回答两个基本问题:

  • What is there?
  • What is it like?
  • 有什么?
  • 像什么?

To draw out the differences between Clojure and OO languages, I’ll explain two different ways of modeling a cuddle zombie. Unlike a regular zombie, a cuddle zombie does not want to devour your brains. It only wants to spoon you and maybe smell your neck. That makes its undead, shuffling, decaying state all the more tragic. How could you try to kill something that only wants love? Who’s the real monster here?

为了说明Clojure与OO语言的区别,我将解释两种不同的方法,用于建模抱抱僵尸。与普通僵尸不同,抱抱僵尸不想吞吃你的大脑。它只想爱抚你,也许会闻闻你的脖子。这使它的不死,摇晃着行走,正在腐烂的状态更加悲惨。你怎么能尝试杀死只想要爱的东西?谁才是真正的怪物?

Object-Oriented Metaphysics

面向对象哲学体系

OO metaphysics treats the cuddle zombie as an object that exists in the world. The object has properties that may change over time, but it’s still treated as a single, constant object. If that seems like a totally obvious, uncontroversial approach to zombie metaphysics, you probably haven’t spent hours in an intro philosophy class arguing about what it means for a chair to exist and what really makes it a chair in the first place.

面相对象哲学体系把抱抱僵尸当作一个世界上存在的对象。这个对象有可能随着时间变化的属性,但它仍然被看作一个单一,持久的对象。如果这看起来像一个完全明显,毫无争议的僵尸哲学体系,那么你可能没有在哲学介绍课里花费过几个小时,讨论一个椅子存在意味着什么,和是什么第一次使它真正存在。

The tricky part is that the cuddle zombie is always changing. Its body slowly deteriorates. Its undying hunger for cuddles grows fiercer with time. In OO terms, we would say that the cuddle zombie is an object with mutable state and that its state is ever fluctuating. But no matter how much the zombie changes, we still identify it as the same zombie. Here’s how you might model and interact with a cuddle zombie in Ruby:

棘手的是抱抱僵尸一直在变化。它的身体慢慢腐烂。它对拥抱的渴望随着时间越来越强烈。用面相对象的术语,我们应该说抱抱僵尸是一个带有可变状态的对象,并且它的状态一直在改变。但无论它怎么变,它仍然是同一个僵尸。下面是如何用Ruby建模并与一个抱抱僵尸:

10-1. Modeling cuddle zombie behavior with Ruby

10-1. 用Ruby建模抱抱僵尸的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CuddleZombie
# attr_accessor is just a shorthand way for creating getters and
# setters for the listed instance variables
attr_accessor :cuddle_hunger_level, :percent_deteriorated

def initialize(cuddle_hunger_level = 1, percent_deteriorated = 0)
self.cuddle_hunger_level = cuddle_hunger_level
self.percent_deteriorated = percent_deteriorated
end
end

fred = CuddleZombie.new(2, 3)
fred.cuddle_hunger_level # => 2
fred.percent_deteriorated # => 3

fred.cuddle_hunger_level = 3
fred.cuddle_hunger_level # => 3

In this example, you create a cuddle zombie, fred, with two attributes: cuddle_hunger_level and percent_deteriorated. fred starts out with a cuddle_hunger_level of just 2, but you can change it to whatever you want and it’s still good ol’ Fred, the same cuddle zombie. In this case, you changed its cuddle_hunger_level to 3.

这个例子中,创建了一个抱抱僵尸,fred,有两个属性:cuddle_hunger_level,和percent_deterioratedfred开始的cuddle_hunger_level是2,但任何时候,只要你愿意,你都能改变这个属性,而它还是我们的老朋友Fred,同一个抱抱僵尸。这个例子里,你把cuddle_hunger_level改成了3。

cuddle-zombie

You can see that this object is just a fancy reference cell. It’s subject to the same nondeterministic results in a multithreaded environment. For example, if two threads try to increment Fred’s hunger level with something like fred.cuddle_hunger_level = fred.cuddle_hunger_level + 1, one of the increments could be lost, just like in the example with two threads writing to X in “The Three Goblins: Reference Cells, Mutual Exclusion, and Dwarven Berserkers” on page 193.

可以看到,这个对象只是一个神奇的引用单元。它同样受制于多线程环境下的不确定性。比如,如果有两个线程尝试增加弗雷德的渴望级别,用类似这样的代码,fred.cuddle_hunger_level = fred.cuddle_hunger_level + 1,其中一个增加会丢失,就像193页的三个哥布林的例子一样,那个例子里有两个进程对X进行写操作,点击查看

Even if you’re only performing reads on a separate thread, the program will still be nondeterministic. For example, suppose you’re conducting research on cuddle zombie behavior. You want to log a zombie’s hunger level whenever it reaches 50 percent deterioration, but you want to do this on another thread to increase performance, using code like that in Listing 10-1:

即使你只在独立线程上进行读操作,程序依然是不确定的。例如,假设你正在进行抱抱僵尸的行为研究。当一个抱抱僵尸的渴望级别到达百分之五十的时候,你希望记录下来,但为了提高性能,你想在另一个线程上干这件事,使用像列表10-1这样的代码:

This Ruby code isn’t safe for concurrent execution.

这个Ruby代码对于并发执行是不安全的。

1
2
3
if fred.percent_deteriorated >= 50
Thread.new { database_logger.log(fred.cuddle_hunger_level) }
end

The problem is that another thread could change fred before the write actually takes place.

问题在于,写操作真正发生前,另一个线程可能改变弗雷德。

For example, Figure 10-1 shows two threads executing from top to bottom. In this situation, it would be correct to write 5 to the database, but 10 gets written instead.

例如,图10-1演示了从上至下执行的两个线程。这个情况里,数据库里写入5应该是对的,但写入的是10。

Figure 10-1: Logging inconsistent cuddle zombie data

图 10-1: 记录不一致的抱抱僵尸数据

fred-read

This would be unfortunate. You don’t want your data to be inconsistent when you’re trying to recover from the cuddle zombie apocalypse. However, there’s no way to retain the state of an object at a specific moment in time.

这很不幸。当尝试从抱抱僵尸灾变恢复的时候,你不想要不一致的数据。但没有办法保留一个对象某一个特定时刻的状态。

Additionally, in order to change the cuddle_hunger_level and percent_deteriorated simultaneously, you must be extra careful. Otherwise, it’s possible for fred to be viewed in an inconsistent state, because another thread might read the fred object in between the two changes that you intend to be simultaneous, like so:

另外,如果想要同时改变cuddle_hunger_levelpercent_deteriorated,你必须特别小心。否则有可能使弗雷德的状态看起来不一致,因为另一个进程可能在这两个修改之间读取弗雷德对象,像这样:

1
2
3
4
fred.cuddle_hunger_level = fred.cuddle_hunger_level + 1
# At this time, another thread could read fred's attributes and
# "perceive" fred in an inconsistent state unless you use a mutex
fred.percent_deteriorated = fred.percent_deteriorated + 1

This is another version of the mutual exclusion problem. In object-oriented programming (OOP), you can manually address this problem with a mutex, which ensures that only one thread can access a resource (in this case, the fred object) at a time for the duration of the mutex.

这是另一个版本的互斥问题。在面向对象编程里,可以用互斥手工解决这个问题,互斥确保了在互斥持续过程中,一次只有一个线程能访问一个资源(这个例子里,是fred对象)。

The fact that objects are never stable doesn’t stop us from treating them as the fundamental building blocks of programs. In fact, this is considered an advantage of OOP. It doesn’t matter how the state changes; you can still interact with a stable interface and everything will work as it should. This conforms to our intuitive sense of the world. A piece of wax is still the same piece of wax even if its properties change: if I change its color, melt it, and pour it on the face of my enemy, I’d still think of it as the same wax object I started with.

对象永远是不稳定的,这个事实并未阻止我们把对象作为编程的基础结构。事实上,这被看作面向对象编程的一个优点。不管状态如何改变,你始终与稳定的接口交互,并且一切都按预想工作。这符合我们对世界的直觉。即使一块腊的属性改变了,它始终还是那块腊:如果我改变了它的颜色,把它融化,并倒在敌人的脸上,我仍然认为它是开始那个腊对象。

Also, in OOP, objects do things. They act on each other, changing state as the program runs. Again, this conforms to our intuitive sense of the world: change is the result of objects acting on each other. A Person object pushes on a Door object and enters a House object.

面向对象编程里,对象也做事情。他们互相作用,随着程序运行改变状态。这又一次符合我们对世界的直觉:改变是对象相互作用的结果。一个人对象推一个门对象,并且进入了一个房子对象。

Clojure Metaphysics

Clojure哲学体系

In Clojure metaphysics, we would say that we never encounter the same cuddle zombie twice. The cuddle zombie is not a discrete thing that exists in the world independent of its mutations: it’s actually a succession of values.

在Clojure的哲学体系里,我们永远不会说遇到同一个抱抱僵尸两次。那个抱抱僵尸不是世界上的独立于其改变的具体事物:它实际上是一系列的值。

The term value is used often by Clojurists, and its specific meaning might differ from what you’re used to. Values are atomic in the sense that they form a single irreducible unit or component in a larger system; they’re indivisible, unchanging, stable entities. Numbers are values: it wouldn’t make sense for the number 15 to mutate into another number. When you add or subtract from 15, you don’t change the number 15; you just wind up with a different number. Clojure’s data structures are also values because they’re immutable. When you use assoc on a map, you don’t modify the original map; instead, you derive a new map.

值这个术语经常被Clojure程序员使用,并且其特定含义可能与你习惯的不同。值构成了更大系统中的单一的不可削减的单元,它们是不可分割的,不变的,稳定的实体。从这个意义上说,值是原子的。数字是值:数字15改变成另一个数字是无意义的。对15进行加或减时,不是改变数字15,只是最后得到了另一个数字。Clojure的数据结构也是值,因为它们是不可变的。当你对一个map使用assco时候,你没有改变原来的map,相反,你得到了一个新map。

So a value doesn’t change, but you can apply a process to a value to produce a new value. For example, say we start with a value F1, and then we apply the Cuddle Zombie process to F1 to produce the value F2. The process then gets applied to the value F2 to produce the value F3, and so on.

值不改变,但你可以对值应用一个过程用以产生一个新值。比如,开始有个值F1,然后对F1应用Cuddle Zombie过程,产生了值F2。然后这个过程又应用于F2产生了值F3,诸如此类。

This leads to a different conception of identity. Instead of understanding identity as inherent to a changing object, as in OO metaphysics, Clojure metaphysics construes identity as something we humans impose on a succession of unchanging values produced by a process over time. We use names to designate identities. The name Fred is a handy way to refer to a series of individual states F1, F2, F3, and so on. From this viewpoint, there’s no such thing as mutable state. Instead, state means the value of an identity at a point in time.

这导致了不同的身份概念。不像面向对象哲学那样,认为身份对于变化的对象是固有的,Clojure哲学认为身份是我们强加在连续不变的值上的东西,这些值是一个过程随着时间流逝产生的。我们用名称标出身份。使用名称Fred可以方便地引用一系列个别的状态,F1, F2, F3 等等。从这个角度看,没有可变状态那样的东西。相反,状态意味着身份在某个时间点上的值。

Rich Hickey has used the analogy of phone numbers to explain state. Alan’s phone number has changed 10 times, but we will always call these numbers by the same name, Alan’s phone number. Alan’s phone number five years ago is a different value than Alan’s phone number today, and both are two states of Alan’s phone number identity.

Rich Hickey曾经用电话号码的类比解释状态。阿兰的电话号码 变过10次,但我们总是用同样的名字阿兰的电话号码 叫这些号码。五年前的阿兰的电话号码与今天的阿兰的电话号码是不同的值,它们都是阿兰的电话号码这个身份的状态。

This makes sense when you consider that in your programs you are dealing with information about the world. Rather than saying that information has changed, you would say you’ve received new information. At 12:00 pm on Friday, Fred the Cuddle Zombie was in a state of 50 percent decay. At 1:00 pm, he was 60 percent decayed. These are both facts that you can process, and the introduction of a new fact does not invalidate a previous fact. Even though Fred’s decay increased from 50 percent to 60 percent, it’s still true that at 12:00 pm he was in a state of 50 percent decay.

考虑到你的程序处理的是世界上的信息,这是有意义的。不是信息在改变,而是接受到了新信息。周五晚上12点,抱抱僵尸弗雷德的腐烂度是50%。夜里一点,是60%。这是可以处理的两个事实,并且新事实的引入没有使前一个无效。尽管弗雷德的腐烂度从50%增加到60%,但他在12点时候的腐烂度为50%仍然是事实。

Figure 10-2 shows how you might visualize values, process, identity, and state.

图10-2图形化地演示了值,过程,身份和状态。

Figure 10-2: Values, process, identity, and state

fpm

These values don’t act on each other, and they can’t be changed. They can’t do anything. Change only occurs when a) a process generates a new value and b) we choose to associate the identity with the new value.

这些值不互相影响,也不会改变。它们什么都不做。变化只出现于:a) 过程产生了新值。 b) 我们把这个身份与一个新值关联起来。

To handle this sort of change, Clojure uses reference types. Reference types let you manage identities in Clojure. Using them, you can name an identity and retrieve its state. Let’s look at the simplest of these, the atom.

Clojure使用引用类型处理这类变化。使用引用类型可以管理身份。用引用类型可以命名一个身份,可以获取它的状态。让我们看看最简单的引用类型:原子

Atoms

原子

Clojure’s atom reference type allows you to endow a succession of related values with an identity. Here’s how you create one:

Clojure的原子引用类型让你为连续相关值提供一个身份。创建方法:

1
2
(def fred (atom {:cuddle-hunger-level 0
:percent-deteriorated 0}))

This creates a new atom and binds it to the name fred. This atom refers to the value {:cuddle-hunger-level 0 :percent-deteriorated 0}, and you would say that that’s its current state.

这段代码创建了一个原子并把它与名字fred绑定。这个原子指向的值是{:cuddle-hunger-level 0 :percent-deteriorated 0},这也是它当前的状态。

To get an atom’s current state, you dereference it. Here’s Fred’s current state:

取值可以得到原子当前状态。Fred的当前状态:

1
2
@fred
; => {:cuddle-hunger-level 0, :percent-deteriorated 0}

Unlike futures, delays, and promises, dereferencing an atom (or any other reference type) will never block. When you dereference futures, delays, and promises, it’s like you’re saying “I need a value now, and I will wait until I get it,” so it makes sense that the operation would block. However, when you dereference a reference type, it’s like you’re saying “give me the value I’m currently referring to,” so it makes sense that the operation doesn’t block, because it doesn’t have to wait for anything.

不像未来,延期和承诺(参考这里),对原子(或任何其他引用类型)取值永远不会阻塞。当对未来,延期和承诺取值时,就像在说:我现在需要一个值,我会等着直到得到它。所以这些操作会阻塞是有意义的。但当对引用类型取值时,就像在说:给我当前指向的值。所以这个操作不阻塞是有道理的,因为这个操作不用等待任何东西。

In the Ruby example in Listing 10-1, we saw how object data could change while you try to log it on a separate thread. There’s no danger of that happening when using atoms to manage state, because each state is immutable. Here’s how you could log a zombie’s state with println:

在列表10-1的Ruby例子里可以看到,当你尝试在独立线程记录对象数据时,它有可能改变。用原子管理状态时候,不会有这种危险,因为每个状态是不可变的。这是如何用print记录僵尸状态:

1
2
3
(let [zombie-state @fred]
(if (>= (:percent-deteriorated zombie-state) 50)
(future (println (:cuddle-hunger-level zombie-state)))))

The problem with the Ruby example in Listing 10-1 was that it took two steps to read the zombie’s two attributes, and some other thread could have changed those attributes in between the two steps. However, by using atoms to refer to immutable data structures, you only have to perform one read, and the data structure returned won’t get altered by another thread.

列表10-1的Ruby例子的问题在于:读取僵尸的两个属性花了两步,这两步之间,其他线程可能已经改变了这些属性。但用原子引用不可变数据结构,你只需要执行一次读取,并且返回的数据结构不会被另一个线程改变。

To update the atom so that it refers to a new state, you use swap!. This might seem contradictory, because I said that atomic values are unchanging. Indeed, they are! But now we’re working with the atom reference type, a construct that refers to atomic values. The atomic values don’t change, but the reference type can be updated and assigned a new value.

使用swap!更新原子,使它指向一个新状态。由于我曾经说过原子值是不变的,这看起来可能有点矛盾。原子确实是不变的!但现在操作的是原子引用类型 ,一个指向原子值的结构。原子值不变,但引用类型可以更新,并被赋予一个新值。

swap! receives an atom and a function as arguments. It applies the function to the atom’s current state to produce a new value, and then it updates the atom to refer to this new value. The new value is also returned. Here’s how you might increase Fred’s cuddle hunger level by one:

swap!接受一个原子和一个函数作为参数。对原子的当前状态应用这个函数,产生一个新值,然后更新这个原子指向这个新值。并返回这个新值。下面是如何把Fred的拥抱渴望等级加一:

1
2
3
4
(swap! fred
(fn [current-state]
(merge-with + current-state {:cuddle-hunger-level 1})))
; => {:cuddle-hunger-level 1, :percent-deteriorated 0}

Dereferencing fred will return the new state:

fred取值会返回新状态:

1
2
@fred
; => {:cuddle-hunger-level 1, :percent-deteriorated 0}

Unlike Ruby, it’s not possible for fred to be in an inconsistent state, because you can update the hunger level and deterioration percentage at the same time, like this:

不像Ruby,fred不可能处于不一致状态,因为你同时更新渴望级别和腐烂百分比,像这样:

1
2
3
4
5
(swap! fred
(fn [current-state]
(merge-with + current-state {:cuddle-hunger-level 1
:percent-deteriorated 1})))
; => {:cuddle-hunger-level 2, :percent-deteriorated 1}

This code passes swap! a function that takes only one argument, current-state. You can also pass swap! a function that takes multiple arguments. For example, you could create a function that takes two arguments, a zombie state and the amount by which to increase its cuddle hunger level:

这段代码传给swap!一个参数是current-state的函数。这个函数也可以接受多个参数。例如,僵尸状态和渴望拥抱等级增加的数量:

1
2
3
(defn increase-cuddle-hunger-level
[zombie-state increase-by]
(merge-with + zombie-state {:cuddle-hunger-level increase-by}))

Let’s test increase-cuddle-hunger-level out real quick on a zombie state.

测试一下。

1
2
(increase-cuddle-hunger-level @fred 10)
; => {:cuddle-hunger-level 12, :percent-deteriorated 1}

Note that this code doesn’t actually update fred, because we’re not using swap! We’re just making a normal function call to increase-cuddle-hunger-level, which returns a result.

注意这段代码实际上没有更新fred,因为我们没有使用swap!,只是对increase-cuddle-hunger-level做了一次普通函数调用,这个函数返回了一个结果。

Now call swap! with the additional arguments, and @fred will be updated, like this:

现在带着附加参数调用swap!,@fred将会更新,像这样:

1
2
3
4
5
(swap! fred increase-cuddle-hunger-level 10)
; => {:cuddle-hunger-level 12, :percent-deteriorated 1}

@fred
; => {:cuddle-hunger-level 12, :percent-deteriorated 1}

Or you could express the whole thing using Clojure’s built-in functions. The update-in function takes three arguments: a collection, a vector for identifying which value to update, and a function to update that value. It can also take additional arguments that get passed to the update function. Here are a couple of examples:

你也可以用Clojure的内置函数完成整个事情。update-in函数接受三个参数:一个集合,一个vector用于指明更新哪个值,和一个函数用于更新那个值。也可以接受更多参数传给那个更新函数。这是几个例子:

1
2
3
4
5
(update-in {:a {:b 3}} [:a :b] inc)
; => {:a {:b 4}}

(update-in {:a {:b 3}} [:a :b] + 10)
; => {:a {:b 13}}

In the first example, you’re updating the map {:a {:b 3}}. Clojure uses the vector [:a :b] to traverse the nested maps; :a yields the nested map {:b 3}, and :b yields the value 3. Clojure applies the inc function to 3 and returns a new map with 3 replaced by 4. The second example is similar. The only difference is that you’re using the addition function and you’re supplying 10 as an additional argument; Clojure ends up calling (+ 3 10).

第一个例子要更新map{:a {:b 3}}。用vector[:a :b]遍历这个map,并得到值3。对3应用inc函数得到4,并替换map里的3。第二个例子类似,只是用了+函数和一个附加参数10,调用的是(+ 3 10)

Here’s how you can use the update-in function to change Fred’s state:

如何使用update-in函数修改Fred的状态:

1
2
(swap! fred update-in [:cuddle-hunger-level] + 10)
; => {:cuddle-hunger-level 22, :percent-deteriorated 1}

By using atoms, you can retain past state. You can dereference an atom to retrieve State 1, and then update the atom, creating State 2, and still make use of State 1:

使用原子可以保留过去的状态。可以对原子取值获得状态1,然后更新原子,创建状态2,并继续使用状态1:

1
2
3
4
5
6
7
(let [num (atom 1)
s1 @num]
(swap! num inc)
(println "State 1:" s1)
(println "Current state:" @num))
; => State 1: 1
; => Current state: 2

This code creates an atom named num, retrieves its state, updates its state, and then prints its past state and its current state, showing that I wasn’t trying to trick you when I said you can retain past state, and therefore you can trust me with all manner of things—including your true name, which I promise to utter only to save you from mortal danger.

这段代码创建了一个名字是num的原子,取得了它的值,更新了它的状态,然后打印它的过去和当前状态。

This is all interesting and fun, but what happens if two separate threads call (swap! fred increase-cuddle-hunger-level 1)? Is it possible for one of the increments to get lost the way it did in the Ruby example at Listing 10-1?

这很有趣,但如果两个独立线程调用(swap! fred increase-cuddle-hunger-level 1)会怎么样呢?其中一个增加会不会像列表10-1的Ruby例子那样丢失呢?

The answer is no! swap! implements compare-and-set semantics, meaning it does the following internally:

答案是不会!swap!实现了比较并设置语意,意味着它内部做了下列事情:

  1. It reads the current state of the atom.
  2. It then applies the update function to that state.
  3. Next, it checks whether the value it read in step 1 is identical to the atom’s current value.
  4. If it is, then swap! updates the atom to refer to the result of step 2.
  5. If it isn’t, then swap! retries, going through the process again with step 1.
  1. 读取原子的当前值。
  2. 对这个状态应用更新函数。
  3. 检查原子当前值与步骤1读到的值是否完全一样
  4. 如果一样,swap!更新原子指向步骤2的结果
  5. 如果不一样,swap!从步骤1开始重新尝试

This process ensures that no swaps will ever get lost.

这个过程确保了swap不会丢失。

One detail to note about swap! is that atom updates happen synchronously; they will block their thread. For example, if your update function calls Thread/sleep 1000 for some reason, the thread will block for at least a second while swap! completes.

一个需要注意的swap!的细节是原子更新是同步的,会阻塞线程。例如,如果更新函数调用了Thread/sleep 1000,swap!完成时,这个线程至少会阻塞一秒。

Sometimes you’ll want to update an atom without checking its current value. For example, you might develop a serum that sets a cuddle zombie’s hunger level and deterioration back to zero. For those cases, you can use the reset! function:

有时候更新原子时候不想检查它的当前值。例如,你可能研究出一种血清,可以把抱抱僵尸的渴望级别和腐烂级别都变成零。这种情况下,可以使用reset!函数:

1
2
(reset! fred {:cuddle-hunger-level 0
:percent-deteriorated 0})

And that covers all the core functionality of atoms! To recap: atoms implement Clojure’s concept of state. They allow you to endow a series of immutable values with an identity. They offer a solution to the reference cell and mutual exclusion problems through their compare-and-set semantics. They also allow you to work with past states without fear of them mutating in place.

这就是原子的所有核心功能!总结一下:原子实现了Clojre的状态概念。它允许给一系列不可变的值赋予一个身份。通过比较并设置语意,它为引用单元和互斥问题提供了一个解决方案。它让你能够使用过去的状态而不用害怕它们被修改。

In addition to these core features, atoms also share two features with the other reference types. You can attach both watches and validators to atoms. Let’s look at those now.

除了这些核心功能,原子跟其他引用类型一样,有两个功能。你可以对原子附加监视校验。让我们看一下。

Watches and Validators

监视与校验

Watches allow you to be super creepy and check in on your reference types’ every move. Validators allow you to be super controlling and restrict what states are allowable. Both watches and validators are plain ol’ functions.

监视让你很紧张,因为监视让你能看到引用类型的每步动作。校验让你很有控制力,因为校验限制了什么状态是许可的。监视和校验都是普通函数。

Watches

监视

A watch is a function that takes four arguments: a key, the reference being watched, its previous state, and its new state. You can register any number of watches with a reference type.

监视 是个函数,接受四个参数:一个key,被监视的引用,之前的状态,新状态。一个引用类型可以注册任意个监视。

Let’s say that a zombie’s shuffle speed (measured in shuffles per hour, or SPH) is dependent on its hunger level and deterioration. Here’s how you’d calculate it, multiplying the cuddle hunger level by how whole it is:

比如说僵尸的移动速度(每小时移动步数,SPH)依赖它的渴望级别和腐烂级别。下面是如何计算的代码,渴望等级与完整度相乘:

1
2
3
4
(defn shuffle-speed
[zombie]
(* (:cuddle-hunger-level zombie)
(- 100 (:percent-deteriorated zombie))))

Let’s also say that you want to be alerted whenever a zombie’s shuffle speed reaches the dangerous level of 5,000 SPH. Otherwise, you want to be told that everything’s okay. Here’s a watch function you could use to print a warning message if the SPH is above 5,000 and print an all’s-well message otherwise:

假设僵尸的移动速度到达5000SPH这个危险级别时候,你需要报警。没到达时候需要告知一切ok。下面是这个监视函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
(defn shuffle-alert
[key watched old-state new-state]
(let [sph (shuffle-speed new-state)]
(if (> sph 5000)
(do
(println "Run, you fool!")
(println "The zombie's SPH is now " sph)
(println "This message brought to your courtesy of " key))
(do
(println "All's well with " key)
(println "Cuddle hunger: " (:cuddle-hunger-level new-state))
(println "Percent deteriorated: " (:percent-deteriorated new-state))
(println "SPH: " sph)))))

Watch functions take four arguments: a key that you can use for reporting, the atom being watched, the state of the atom before its update, and the state of the atom after its update. This watch function calculates the shuffle speed of the new state and prints a warning message if it’s too high and an all’s-well message when the shuffle speed is safe, as mentioned above. In both sets of messages, the key is used to let you know the source of the message.

这个监视函数接受四个参数:用于报告的key,被监视的原子,原子更新前的状态,原子更新后的状态。监视函数用新状态计算出僵尸的移动速度,并做出相应报告。报告里的key使你知道信息来源。

You can attach this function to fred with add-watch. The general form of add-watch is (add-watch ref key watch-fn). In this example, we’re resetting fred’s state, adding the shuffle-alert watch function, and then updating fred’s state a couple of times to trigger shuffle-alert:

add-watch可以把这个函数附加到fred上。add-watch的通用形式是(add-watch ref key watch-fn)。这个例子里,重置了fred的状态,加上了shuffle-alert监视函数,然后更新了几次fred的状态,以触发shuffle-alert:

1
2
3
4
5
6
7
8
9
10
11
12
13
(reset! fred {:cuddle-hunger-level 22
:percent-deteriorated 2})
(add-watch fred :fred-shuffle-alert shuffle-alert)
(swap! fred update-in [:percent-deteriorated] + 1)
; => All's well with :fred-shuffle-alert
; => Cuddle hunger: 22
; => Percent deteriorated: 3
; => SPH: 2134

(swap! fred update-in [:cuddle-hunger-level] + 30)
; => Run, you fool!
; => The zombie's SPH is now 5044
; => This message brought to your courtesy of :fred-shuffle-alert

This example watch function didn’t use watched or old-state, but they’re there for you if the need arises. Now let’s cover validators.

这个监视函数例子没有使用watchedold-state,如果需要,可以使用。现在我们来看看验证。

Validators

验证

Validators let you specify what states are allowable for a reference. For example, here’s a validator that you could use to ensure that a zombie’s :percent-deteriorated is between 0 and 100:

验证 用来标明引用的许可的状态。例如,这个验证用来确保僵尸的:percent-deteriorated在0和100之间:

1
2
3
4
(defn percent-deteriorated-validator
[{:keys [percent-deteriorated]}]
(and (>= percent-deteriorated 0)
(<= percent-deteriorated 100)))

As you can see, the validator takes only one argument. When you add a validator to a reference, the reference is modified so that, whenever it’s updated, it will call this validator with the value returned from the update function as its argument. If the validator fails by returning false or throwing an exception, the reference won’t change to point to the new value.

正如你看到的,验证只接受一个参数。把验证添加到引用时,引用被修改成:无论何时这个引用被更新,它都会用更新函数返回的值作为参数调用这个验证函数。如果验证函数失败,即返回false或抛出异常,这个引用不会改成指向那个新值。

You can attach a validator during atom creation:

原子创建时可以附加验证:

1
2
3
4
5
6
(def bobby
(atom
{:cuddle-hunger-level 0 :percent-deteriorated 0}
:validator percent-deteriorated-validator))
(swap! bobby update-in [:percent-deteriorated] + 200)
; This throws "Invalid reference state"

In this example, percent-deteriorated-validator returned false and the atom update failed.

上面的例子里,percent-deteriorated-validator返回false,并且这个原子更新失败。

You can throw an exception to get a more descriptive error message:

为了让错误描述更清楚,可以抛出异常:

1
2
3
4
5
6
7
8
9
10
11
(defn percent-deteriorated-validator
[{:keys [percent-deteriorated]}]
(or (and (>= percent-deteriorated 0)
(<= percent-deteriorated 100))
(throw (IllegalStateException. "That's not mathy!"))))
(def bobby
(atom
{:cuddle-hunger-level 0 :percent-deteriorated 0}
:validator percent-deteriorated-validator))
(swap! bobby update-in [:percent-deteriorated] + 200)
; This throws "IllegalStateException: That's not mathy!"

Pretty great! Now let’s look at refs.

非常棒!现在看看引用。

Atoms are ideal for managing the state of independent identities. Sometimes, though, we need to express that an event should update the state of more than one identity simultaneously. Refs are the perfect tool for this scenario.

用原子管理独立的身份很理想。但有时需要同时更新不止一个身份的状态。这个场合使用引用很完美。

A classic example of this is recording sock gnome transactions. As we all know, sock gnomes take a single sock from every clothes dryer around the world. They use these socks to incubate their young. In return for this “gift,” sock gnomes protect your home from El Chupacabra. If you haven’t been visited by El Chupacabra lately, you have sock gnomes to thank.

一个经典例子是记录袜子矮人事务。我们都知道,袜子矮人从遍布世界的烘干机拿走一只袜子,用来养育他们的小孩。作为回报,他们保护你的家庭,免受吸血鬼的入侵。如果你还没有被吸血鬼拜访过,你应该感谢袜子矮人。

sock-gnome

To model sock transfers, we need to express that a dryer has lost a sock and a gnome has gained a sock simultaneously. One moment the sock belongs to the dryer; the next it belongs to the gnome. The sock should never appear to belong to both the dryer and the gnome, nor should it appear to belong to neither.

为了对袜子转换建模,我们要同时表示烘干机少了一只袜子和矮人获得了一只袜子。某个时刻袜子属于烘干机,下一时刻它属于矮人。袜子永远不会同时属于烘干机和矮人,也不会谁都不属于。

Modeling Sock Transfers

建模袜子转换

You can model this sock transfer with refs. Refs allow you to update the state of multiple identities using transaction semantics. These transactions have three features:

可以用引用建模袜子转换。引用可以按事务语意更新多个身份。这些事务有三个特征:

  • They are atomic, meaning that all refs are updated or none of them are.
  • They are consistent, meaning that the refs always appear to have valid states. A sock will always belong to a dryer or a gnome, but never both or neither.
  • They are isolated, meaning that transactions behave as if they executed serially; if two threads are simultaneously running transactions that alter the same ref, one transaction will retry. This is similar to the compare-and-set semantics of atoms.
  • 它们是原子的,意味着所有引用或者全更新,或者全不更新。
  • 它们是一致的,意味着这些引用的状态总是有效的。一只袜子总是属于一个烘干机或一个矮人,永远不会同时属于或都不属于。
  • 它们是隔离的,意味着所有事务就像是按顺序执行一样;如果两个线程同时运行更改同一个引用的事务,一个线程将重试。这与原子的比较并设置语意类似。

You might recognize these as the A, C, and I in the ACID properties of database transactions. You can think of refs as giving you the same concurrency safety as database transactions, only with in-memory data.

你可能认出这是数据库事务的ACID里的A,C,I。你可以认为引用与数据库事务的安全并发一样,只不过是对内存数据操作。

Clojure uses software transactional memory (STM) to implement this behavior. STM is very cool, but when you’re starting with Clojure, you don’t need to know much about it; you just need to know how to use it, which is what this section shows you.

Clojure用软件事务内存(STM) 实现这个行为。STM非常酷,但刚开始学习Clojure时,不需要对它了解太多,只需要知道怎么用它,这节就会演示给你。

Let’s start transferring some socks! First, you’ll need to code up some sock- and gnome-creation technology. The following code defines some sock varieties, then defines a couple of helper functions: sock-count will be used to help keep track of how many of each kind of sock belongs to either a gnome or a dryer, and generate-sock-gnome creates a fresh, sockless gnome:

让我们开始转换袜子!首先,需要一些创建袜子和矮人的代码。下面的代码定义了一些袜子品种,然后定义了几个辅助函数:sock-count用于记录烘干机和矮人的每种袜子有多少,generate-sock-gnome创建一个袜子矮人:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(def sock-varieties
#{"darned" "argyle" "wool" "horsehair" "mulleted"
"passive-aggressive" "striped" "polka-dotted"
"athletic" "business" "power" "invisible" "gollumed"})

(defn sock-count
[sock-variety count]
{:variety sock-variety
:count count})

(defn generate-sock-gnome
"Create an initial sock gnome state with no socks"
[name]
{:name name
:socks #{}})

Now you can create your actual refs. The gnome will have 0 socks. The dryer, on the other hand, will have a set of sock pairs generated from the set of sock varieties. Here are our refs:

现在可以创建引用了。矮人没有袜子。烘干机有很多双袜子,按袜子品牌集合生成。这就是我们的引用:

1
2
3
(def sock-gnome (ref (generate-sock-gnome "Barumpharumph")))
(def dryer (ref {:name "LG 1337"
:socks (set (map #(sock-count % 2) sock-varieties))}))

You can dereference refs just like you can dereference atoms. In this example, the order of your socks will probably be different because we’re using an unordered set:

同原子一样,你可以对引用取值。例子里袜子的顺序可能与你的不一样,因为使用的是无序集合:

1
2
3
4
5
6
7
8
(:socks @dryer)
; => #{{:variety "passive-aggressive", :count 2} {:variety "power", :count 2}
{:variety "athletic", :count 2} {:variety "business", :count 2}
{:variety "argyle", :count 2} {:variety "horsehair", :count 2}
{:variety "gollumed", :count 2} {:variety "darned", :count 2}
{:variety "polka-dotted", :count 2} {:variety "wool", :count 2}
{:variety "mulleted", :count 2} {:variety "striped", :count 2}
{:variety "invisible", :count 2}}

Now everything’s in place to perform the transfer. We’ll want to modify the sock-gnome ref to show that it has gained a sock and modify the dryer ref to show that it’s lost a sock. You modify refs using alter, and you must use alter within a transaction. dosync initiates a transaction and defines its extent; you put all transaction operations in its body. Here we use these tools to define a steal-sock function, and then call it on our two refs:

现在执行转换的所有东西都已就绪。我们想修改sock-gnome引用,表示它得到了一只袜子,并且修改dryer引用,表示它失去了一只袜子。用alter修改引用,而且alter必须放在事务内。dosync开始一个事务,并定义其内容,所有事务操作都放在dosync主体内。我们用这些工具定义steal-sock函数,然后对两个引用调用这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
(defn steal-sock
[gnome dryer]
(dosync
(when-let [pair (some #(if (= (:count %) 2) %) (:socks @dryer))]
(let [updated-count (sock-count (:variety pair) 1)]
(alter gnome update-in [:socks] conj updated-count)
(alter dryer update-in [:socks] disj pair)
(alter dryer update-in [:socks] conj updated-count)))))
(steal-sock sock-gnome dryer)

(:socks @sock-gnome)
; => #{{:variety "passive-aggressive", :count 1}}

Now the gnome has one passive-aggressive sock, and the dryer has one less (your gnome may have stolen a different sock because the socks are stored in an unordered set). Let’s make sure all passive-aggressive socks are accounted for:

现在矮人有一只passive-aggressive袜子,同时烘干机少了一只(因为袜子使用的是无序集合,你的矮人可能偷的是不同的袜子)。让我们确认一下双方的passive-aggressive袜子都改变了:

1
2
3
4
5
6
(defn similar-socks
[target-sock sock-set]
(filter #(= (:variety %) (:variety target-sock)) sock-set))

(similar-socks (first (:socks @sock-gnome)) (:socks @dryer))
; => ({:variety "passive-aggressive", :count 1})

There are a couple of details to note here: when you alter a ref, the change isn’t immediately visible outside of the current transaction. This is what lets you call alter on the dryer twice within a transaction without worry­ing about whether dryer will be read in an inconsistent state. Similarly, if you alter a ref and then deref it within the same transaction, the deref will return the new state.

这有几个细节需要注意:alter一个引用时,这个修改对于当前事务外面不是立刻可见的。正是这点让你能够在事务内alter两次dryer,而不用担心dryer被不一致地读取。如果在同一事务内alter一个引用,然后再deref这个引用,deref会返回新状态。

Here’s an example to demonstrate this idea of in-transaction state:

事务内状态例子:

1
2
3
4
5
6
7
8
9
10
(def counter (ref 0))
(future
(dosync
(alter counter inc)
(println @counter)
(Thread/sleep 500)
(alter counter inc)
(println @counter)))
(Thread/sleep 250)
(println @counter)

This prints 1, 0 , and 2, in that order. First, you create a ref, counter, which holds the number 0. Then you use future to create a new thread to run a transaction on. On the transaction thread, you increment the counter and print it, and the number 1 gets printed. Meanwhile, the main thread waits 250 milliseconds and prints the counter’s value, too. However, the value of counter on the main thread is still 0—the main thread is outside of the transaction and doesn’t have access to the transaction’s state. It’s like the transaction has its own private area for trying out changes to the state, and the rest of the world can’t know about them until the transaction is done. This is further illustrated in the transaction code: after it prints the first time, it increments the counter again from 1 to 2 and prints the result, 2.

这段代码按1,0,2的顺序打印。首先创建引用counter,值是0。然后用future创建新线程运行事务。事务线程上计数器加一并打印。同时主线程等待250毫秒并打印计数器的值。但主线程上计数器的值仍然是0,主线程在事务外面,无权访问事务状态。就好像事务有自己的私有区域用于尝试修改状态,而这个区域外的世界直到事务完成才会知道这个修改。事务代码进一步演示了这点:第一次打印后,把计数器从1增加到2,并打印结果,2。

The transaction will try to commit its changes only when it ends. The commit works similarly to the compare-and-set semantics of atoms. Each ref is checked to see whether it’s changed since you first tried to alter it. If any of the refs have changed, then none of the refs is updated and the transaction is retried. For example, if Transaction A and Transaction B are both attempted at the same time and events occur in the following order, Transaction A will be retried:

只有完成时,事务才尝试提交修改。提交的工作方式与原子的比较并设置语意类似。如果有任何引用被修改了,那么所有引用都不会更新,同时事务将重试。例如,如果事务A和事务B在同一时间发生,并且事件按下列顺序发生,事务A会重试:

  1. Transaction A: alter gnome
  2. Transaction B: alter gnome
  3. Transaction B: alter dryer
  4. Transaction B: alter dryer
  5. Transaction B: commit—successfully updates gnome and dryer
  6. Transaction A: alter dryer
  7. Transaction A: alter dryer
  8. Transaction A: commit—fails because dryer and gnome have changed; retries.
  1. 事务A: 修改矮人
  2. 事务B: 修改矮人
  3. 事务B: 修改烘干机
  4. 事务B: 修改烘干机
  5. 事务B: 提交成功,更新矮人和烘干机
  6. 事务A: 修改烘干机
  7. 事务A: 修改烘干机
  8. 事务A: 提交失败,因为矮人和烘干机已经改变;重试

And there you have it! Safe, easy, concurrent coordination of state changes. But that’s not all! Refs have one more trick up their suspiciously long sleeve: commute.

给你!这这个安全,容易,并发的状态改变协调工具!但这还不是全部!在出奇的长袖里,引用还有一个把戏:commute

commute

交换

commute allows you to update a ref’s state within a transaction, just like alter. However, its behavior at commit time is completely different. Here’s how alter behaves:

alter一样,用commute可以在事务内更新引用的状态。但它提交时候的行为完全不同。alter的行为是这样的:

  1. Reach outside the transaction and read the ref’s current state.
  2. Compare the current state to the state the ref started with within the transaction.
  3. If the two differ, make the transaction retry.
  4. Otherwise, commit the altered ref state.
  1. 去事务外面读取引用当前状态。
  2. 把这个状态与事务内引用开始时候的状态比较。
  3. 如果不同,重试这个事务。
  4. 否则提交修改后的状态。

commute, on the other hand, behaves like this at commit time:

commute提交时候的的行为:

  1. Reach outside the transaction and read the ref’s current state.
  2. Run the commute function again using the current state.
  3. Commit the result.
  1. 去事务外面读取引用当前状态。
  2. 用这个当前状态再次运行交换函数。
  3. 提交这个结果。

As you can see, commute doesn’t ever force a transaction retry. This can help improve performance, but it’s important that you only use commute when you’re sure that it’s not possible for your refs to end up in an invalid state. Let’s look at examples of safe and unsafe uses of commute.

如你所见,commute不会强制事务重试。这有助于提升性能,但只有当你确认引用的最终状态是有效的,你才能使用commute。这点非常重要。来看一下安全和不安全使用commute的例子。

Here’s an example of a safe use. The sleep-print-update function returns the updated state but also sleeps the specified number of milliseconds so we can force transaction overlap. It prints the state that it’s attempting to update so we can gain insight into what’s going on:

这是个安全使用的例子。sleep-print-update函数返回更新后的状态,为了强制事务重叠,这个函数睡眠指定的时间。为了我们理解内部发生了什么,这个函数也打印了它正在修改的状态:

1
2
3
4
5
6
7
8
9
(defn sleep-print-update
[sleep-time thread-name update-fn]
(fn [state]
(Thread/sleep sleep-time)
(println (str thread-name ": " state))
(update-fn state)))
(def counter (ref 0))
(future (dosync (commute counter (sleep-print-update 100 "Thread A" inc))))
(future (dosync (commute counter (sleep-print-update 150 "Thread B" inc))))

Here’s a timeline of what prints:

这是代码打印的时间线:

1
2
3
4
Thread A: 0 | 100ms
Thread B: 0 | 150ms
Thread A: 0 | 200ms
Thread B: 1 | 300ms

Notice that the last printed line reads Thread B: 1. That means that sleep-print-update receives 1 as the argument for state the second time it runs. That makes sense, because Thread A has committed its result by that point. If you dereference counter after the transactions run, you’ll see that the value is 2.

注意最后一行打印的是Thread B: 1。这意味着sleep-print-update第二次运行时,接受的状态参数是1。这是对的,因为这时候线程A已经提交了结果。如果这个事务结束后你对counter取值,你会看到值是2

[译者增加]

1
2
@counter
; => 2

Now, here’s an example of unsafe commuting:

这是一个不安全的交换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(def receiver-a (ref #{}))
(def receiver-b (ref #{}))
(def giver (ref #{1}))
(do (future (dosync (let [gift (first @giver)]
(Thread/sleep 10)
(commute receiver-a conj gift)
(commute giver disj gift))))
(future (dosync (let [gift (first @giver)]
(Thread/sleep 50)
(commute receiver-b conj gift)
(commute giver disj gift)))))

@receiver-a
; => #{1}

@receiver-b
; => #{1}

@giver
; => #{}

The 1 was given to both receiver-a and receiver-b, and you’ve ended up with two instances of 1, which isn’t valid for your program. What’s different about this example is that the functions that are applied, essentially #(conj % gift) and #(disj % gift), are derived from the state of giver. Once giver changes, the derived functions produce an invalid state, but commute doesn’t care that the resulting state is invalid and commits the result anyway. The lesson here is that although commute can help speed up your programs, you have to be judicious about when to use it.

1同时给了receiver-areceiver-b,最后有两个1,对于程序这是不合法的。这个例子的差别在于应用的函数,本质上说,#(conj % gift)#(disj % gift) 是从giver的状态得到的。一旦giver改变,得到的函数就会产生不合法的状态,但commute不关心这个并提交了结果。教训是虽然commute有助于加快程序速度,但必须考虑好再使用。

[译者增加,对比使用alter的结果]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(def receiver-a (ref #{}))
(def receiver-b (ref #{}))
(def giver (ref #{1}))
(do (future (dosync (let [gift (first @giver)]
(Thread/sleep 10)
(alter receiver-a conj gift)
(alter giver disj gift))))
(future (dosync (let [gift (first @giver)]
(Thread/sleep 50)
(alter receiver-b conj gift)
(alter giver disj gift)))))

@receiver-a
; => #{1}

@receiver-b
; => #{nil}

@giver
; => #{}

Now you’re ready to start using refs safely and sanely. Refs have a few more nuances that I won’t cover here, but if you’re curious about them, you can research the ensure function and the phenomenon write skew.

现在你已经准备好安全并明智地使用引用了。引用还有几个我没有讲到的细节,如果感兴趣,你可以研究ensure函数和write skew现象。

On to the final reference type that this book covers: vars.

本书最后讲解的引用类型是变量

Vars

变量

You’ve already learned a bit about vars in Chapter 6. To recap briefly, vars are associations between symbols and objects. You create new vars with def.

第六章你对变量已经有了一些了解。简要概括一下,变量是符号与对象之间的关联。用def创建新变量。

Although vars aren’t used to manage state in the same way as atoms and refs, they do have a couple of concurrency tricks: you can dynamically bind them, and you can alter their roots. Let’s look at dynamic binding first.

虽然变量不像原子和引用那样用于管理状态,但它确实有几个并发花招:你可以动态绑定变量,你可以修改变量的根。先看看动态绑定。

Dynamic Binding

动态绑定

When I first introduced def, I implored you to treat it as if it’s defining a constant. It turns out that vars are a bit more flexible than that: you can create a dynamic var whose binding can be changed. Dynamic vars can be useful for creating a global name that should refer to different values in different contexts.

第一次介绍def,我请求你就当它定义了一个常亮。实际上变量更灵活:你可以创建能改变绑定的动态变量。需要不同情况指向不同值的全局名字时,可以用动态变量。

Creating and Binding Dynamic Vars

创建和绑定动态变量

First, create a dynamic var:

首先创建动态变量:

1
(def ^:dynamic *notification-address* "dobby@elf.org")

Notice two important details here. First, you use ^:dynamic to signal to Clojure that a var is dynamic. Second, the var’s name is enclosed by asterisks. Lispers call these earmuffs, which is adorable. Clojure requires you to enclose the names of dynamic vars in earmuffs. This helps signal the var’s dynamicaltude to other programmers.

注意两个要点。首先,用^:dynamic告诉Clojure这个变量是动态的。然后,变量名用星号包围。Lisp程序员管这个叫耳套,Clojure要求动态变量名字包含在耳套里。这有助于告诉其他程序员,这个变量是动态的。

Unlike regular vars, you can temporarily change the value of dynamic vars by using binding:

与普通变量不同,用binding可以临时改变动态变量的值:

1
2
3
(binding [*notification-address* "test@elf.org"]
*notification-address*)
; => "test@elf.org"

You can also stack bindings ( just like you can with let):

也可以嵌套绑定(像let一样):

1
2
3
4
5
6
7
8
(binding [*notification-address* "tester-1@elf.org"]
(println *notification-address*)
(binding [*notification-address* "tester-2@elf.org"]
(println *notification-address*))
(println *notification-address*))
; => tester-1@elf.org
; => tester-2@elf.org
; => tester-1@elf.org

Now that you know how to dynamically bind a var, let’s look at a real-world application.

知道了如何动态绑定一个变量,来看一个真实应用程序。

Dynamic Var Uses

动态变量使用

Let’s say you have a function that sends a notification email. In this example, we’ll just return a string but pretend that the function actually sends the email:

假设有个发送通知邮件的函数。这个例子里,只返回了字符串,表示发送了邮件:

1
2
3
4
5
6
(defn notify
[message]
(str "TO: " *notification-address* "\n"
"MESSAGE: " message))
(notify "I fell.")
; => "TO: dobby@elf.org\nMESSAGE: I fell."

What if you want to test this function without spamming Dobby every time your specs run? Here comes binding to the rescue:

如果不想每次测试这个函数时候都发邮件给Dobby,该怎么办呢?这时候binding就有用了:

1
2
3
(binding [*notification-address* "test@elf.org"]
(notify "test!"))
; => "TO: test@elf.org\nMESSAGE: test!"

Of course, you could have just defined notify to take an email address as an argument. In fact, that’s often the right choice. Why would you want to use dynamic vars instead?

当然,你可以定义notify,让它接受一个邮件地址参数。实际上这经常是正确的选择。那么我们到底为什么使用动态变量呢?

Dynamic vars are most often used to name a resource that one or more functions target. In this example, you can view the email address as a resource that you write to. In fact, Clojure comes with a ton of built-in dynamic vars for this purpose. *out*, for example, represents the standard output for print operations. In your program, you could re-bind *out* so that print statements write to a file, like so:

动态变量最经常的用处是:命名一个或多个函数的目标资源。上面的例子里,你可以把邮件地址看成写入的资源。事实上,Clojure有大量用于这个目的的动态变量。比如*out*表示打印操作的标准输出。你可以重新绑定*out*,让打印目标变成一个文件,像这样:

1
2
3
4
5
6
7
8
(binding [*out* (clojure.java.io/writer "print-output")]
(println "A man who carries a cat by the tail learns
something he can learn in no other way.
-- Mark Twain"))
(slurp "print-output")
; => A man who carries a cat by the tail learns
something he can learn in no other way.
-- Mark Twain

This is much less burdensome than passing an output destination to every invocation of println. Dynamic vars are a great way to specify a common resource while retaining the flexibility to change it on an ad hoc basis.

这比每次调用println时候都传给它输出目省事多了。想要指定通用资源,又能在特定情况改变这个资源,动态变量是个很棒的方法。

Dynamic vars are also used for configuration. For example, the built-in var *print-length* allows you to specify how many items in a collection Clojure should print:

动态变量也可以用于配置。比如,动态变量*print-length*可以指定打印多少集合里的成员:

1
2
3
4
5
6
(println ["Print" "all" "the" "things!"])
; => [Print all the things!]

(binding [*print-length* 1]
(println ["Print" "just" "one!"]))
; => [Print ...]

Finally, it’s possible to set! dynamic vars that have been bound. Whereas the examples you’ve seen so far allow you to convey information in to a function without having to pass in the information as an argument, set! allows you convey information out of a function without having to return it as an argument.

最后,可以用set!设置一个已经绑定的动态变量。就像上面看到的例子让你无需给函数传参,就能把信息传进函数,set!使函数无需返回结果,就能把信息传到函数外面。

For example, let’s say you’re a telepath, but your mind-reading powers are a bit delayed. You can read people’s thoughts only after the moment when it would have been useful for you to know them. Don’t feel too bad, though; you’re still a telepath, which is awesome. Anyway, say you’re trying to cross a bridge guarded by a troll who will eat you if you don’t answer his riddle. His riddle is “What number between 1 and 2 am I thinking of?” In the event that the troll devours you, you can at least die knowing what the troll was actually thinking.

例如,假设你是个心灵感应者,但你的读心术有点慢。你只能在某一时刻读到对方的想法,这个时刻就是知道这个想法时对你已经没用了。不要觉得太糟糕,你仍然是心灵感应者,这很了不起。比如说你正要通过一个食人魔把守的桥,如果你没有解开食人魔的迷题,它就会吃了你。它的谜题是”我现在想的数字是1还是2?”。当食人魔吃你的时候,你至少可以知道它实际想的是什么?

troll

In this example, you create the dynamic var *troll-thought* to convey the troll’s thought out of the troll-riddle function:

这个例子中,在函数troll-riddle外面创建动态变量*troll-thought*,传达食人魔的想法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(def ^:dynamic *troll-thought* nil)
(defn troll-riddle
[your-answer]
(let [number "man meat"]
➊ (when (thread-bound? #'*troll-thought*)
➋ (set! *troll-thought* number))
(if (= number your-answer)
"TROLL: You can cross the bridge!"
"TROLL: Time to eat you, succulent human!")))

(binding [*troll-thought* nil]
(println (troll-riddle 2))
(println "SUCCULENT HUMAN: Oooooh! The answer was" *troll-thought*))

; => TROLL: Time to eat you, succulent human!
; => SUCCULENT HUMAN: Oooooh! The answer was man meat

You use the thread-bound? function at ➊ to check that the var has been bound, and if it has, you set! *troll-thought* to the troll’s thought at ➋.

➊处使用thread-bound函数,检查这个变量是否已经绑定,如果是,在➋处设置巨魔的想法set! *troll-thought*

The var returns to its original value outside of binding:

绑定外面,这个变量返回它的初始值:

1
2
*troll-thought*
; => nil

Notice that you have to pass #'*troll-thought* (including #'), not *troll-thought*, to the function thread-bound?. This is because thread-bound? takes the var itself as an argument, not the value it refers to.

注意,传给函数thread-bound?的必须是#'*troll-thought*(包含#'),而不能是*troll-thought*。这是因为thread-bound?接受的参数是变量本身,而不是它引用的值。

[译者注]
#'*troll-thought* 等于(var *troll-thought*),而后者就是获取这个动态变量对象本身,参考这里。前者是读入程序宏的一种形式,可参考第六章,Clojure炼金术:读取,求值,宏的相应部分

Per-Thread Binding

每线程绑定

One final point to note about binding: if you access a dynamically bound var from within a manually created thread, the var will evaluate to the original value. If you’re new to Clojure (and Java), this feature won’t be immediately relevant; you can probably skip this section and come back to it later.

关于绑定,最后一个要注意的是:如果你手动创建了一个线程,并从这个线程里访问一个动态绑定的变量,这个变量会求值为它的最初值。如果你刚接触Clojure(和Java),这个特性不会马上对你产生意义,你可以跳过这节,以后再看。

Ironically, this binding behavior prevents us from easily creating a fun demonstration in the REPL, because the REPL binds *out*. It’s as if all the code you run in the REPL is implicitly wrapped in something like (binding [*out* repl-printer] your-code. If you create a new thread, *out* won’t be bound to the REPL printer.

讽刺的是,这个绑定行为使我们无法在REPL里创造一个好玩的演示,因为这个REPL绑定了*out*。就好像所有运行在REPL里的代码,都隐含地被包在这样的东西里,(binding [*out* repl-printer] your-code)。如果你新建一个线程,*out*将不被绑定到新线程的REPL打印机。

The following example uses some basic Java interop. Even if it looks unfamiliar, the gist of the following code should be clear, and you’ll learn exactly what’s going on in Chapter 12.

下面的例子使用了一些基本的Java互操作。虽然看起来不太熟悉,但大意应该是清楚的,在12章将会了解具体都是什么。

This code prints output to the REPL:

这段代码向REPL打印输出:

1
2
(.write *out* "prints to repl")
; => prints to repl

The following code doesn’t print output to the REPL, because *out* is not bound to the REPL printer:

这段代码不会向REPL打印输出,因为*out*没绑定到REPL打印机:

1
(.start (Thread. #(.write *out* "prints to standard out")))

You can work around this by using this goofy code:

使用这段怪异的代码,可以避开这个问题:

1
2
3
4
(let [out *out*]
(.start
(Thread. #(binding [*out* out]
(.write *out* "prints to repl from thread")))))

Or you can use bound-fn, which carries all the current bindings to the new thread:

使用bound-fn也可以,它把当前所有绑定都传递给新线程:

1
(.start (Thread. (bound-fn [] (.write *out* "prints to repl from thread"))))

The let binding captures *out* so we can then rebind it in the child thread, which is goofy as hell. The point is that bindings don’t get passed on to manually created threads. They do, however, get passed on to futures. This is called binding conveyance. Throughout this chapter, we’ve been printing from futures without any problem, for example.

上面代码的let绑定里捕获了*out*,所以可以在子线程里重新绑定它,非常怪异。重点是绑定不会传递给手动创建的线程。但确实能传递给未来。这叫绑定输送。比如,这章里我们一直在未来里打印,而没发生任何问题。

That’s it for dynamic binding. Let’s turn our attention to the last var topic: altering var roots.

动态绑定就讨论这些。让我们把注意转到最后一个变量主题:修改变量。

Altering the Var Root

修改根变量

When you create a new var, the initial value that you supply is its root:

创建新变量时,你提供的初始值是这个变量的值:

1
(def power-source "hair")

In this example, "hair" is the root value of power-source. Clojure lets you permanently change this root value with the function alter-var-root:

这个例子里"hair"power-source的根值。Clojure允许你用alter-var-root永久改变这个根值:

1
2
3
(alter-var-root #'power-source (fn [_] "7-eleven parking lot"))
power-source
; => "7-eleven parking lot"

Just like when using swap! to update an atom or alter! to update a ref, you use alter-var-root along with a function to update the state of a var. In this case, the function is just returning a new string that bears no relation to the previous value, unlike the alter! examples where we used inc to derive a new number from the current number.

就像用swap!更新原子,或用alter!更新引用,使用alter-var-root加上一个函数更新变量的状态。前面的alter!例子里,我们用inc从当前数得出一个新数,与此不同的是,这个函数与之前的值没有关系,只是返回一个新字符串。

You’ll hardly ever want to do this. You especially don’t want to do this to perform simple variable assignment. If you did, you’d be going out of your way to create the binding as a mutable variable, which goes against Clojure’s philosophy; it’s best to use the functional programming techniques you learned in Chapter 5.

你几乎不会这么做。对于简单的变量赋值,你更不想这么做。这么做是把绑定当成可变变量,这与Clojure原则相背,最好的方式是用第5章学到的函数式编程技巧。

You can also temporarily alter a var’s root with with-redefs. This works similarly to binding except the alteration will appear in child threads. Here’s an example:

也可以用with-redefs临时更改一个变量的根值。这与binding类似,除了更改将出现在子线程里。下面是例子:

1
2
3
4
(with-redefs [*out* *out*]
(doto (Thread. #(println "with redefs allows me to show up in the REPL"))
.start
.join))

with-redefs can be used with any var, not just dynamic ones. Because it has has such far-reaching effects, you should only use it during testing. For example, you could use it to redefine a function that returns data from a network call, so that the function returns mock data without having to actually make a network request.

with-redefs可以用于任何变量,不只是动态变量。因此你应该把它只用于测试。例如,可以用它重新定义一个从网络调用返回数据的函数,让这个函数返回模拟数据,而无需进行真实的网络请求。

Now you know all about vars! Try not to hurt yourself or anyone you know with them.

变量的一切你都知道了!不要用它伤害你自己或任何人。

Stateless Concurrency and Parallelism with pmap

无状态并发与用pmap并行

So far, this chapter has focused on tools that are designed to mitigate the risks inherent in concurrent programming. You’ve learned about the dangers born of shared access to mutable state and how Clojure implements a reconceptualization of state that helps you write concurrent programs safely.

到现在为止本章的焦点都是:为减少并发编程天生带有的风险而设计出的工具。你已经了解了共享可变状态访问权带来的危险,也了解了Clojure重新实现的状态概念,用于安全地写并发程序。

Often, though, you’ll want to concurrent-ify tasks that are completely independent of each other. There is no shared access to a mutable state; therefore, there are no risks to running the tasks concurrently and you don’t have to bother with using any of the tools I’ve just been blabbing on about.

但你经常想要把互相独立的任务并发化。由于不存在对可变状态的共享访问,所以并发运行这些任务没有风险,不必费心使用前面提到的工具。

As it turns out, Clojure makes it easy for you to write code for achieving stateless concurrency. In this section, you’ll learn about pmap, which gives you concurrency performance benefits virtually for free.

事实证明,用Clojure写无状态并发代码很容易。这节将了解pmap,它为你免费提供了并发性能收益。

map is a perfect candidate for parallelization: when you use it, all you’re doing is deriving a new collection from an existing collection by applying a function to each element of the existing collection. There’s no need to maintain state; each function application is completely independent. Clojure makes it easy to perform a parallel map with pmap. With pmap, Clojure handles the running of each application of the mapping function on a separate thread.

对于并行化,map是个完美的候选人:使用它时候,全部工作就是一个集合的每个成员应用一个函数,得出一个新集合。没有维护状态的需要,每个函数调用都是完全独立的。使用pmap可以很容易地进行执行一个并行map。pmap的每个映射函数调用都在一个独立线程上进行。

To compare map and pmap, we need a lot of example data, and to generate this data, we’ll use the repeatedly function. This function takes another function as an argument and returns a lazy sequence. The elements of the lazy sequence are generated by calling the passed function, like this:

为了比较mappmap,我们需要很多样本数据,使用repeatedly函数产生这些数据。这个函数接受一个函数作为参数,并返回一个惰性序列。这个惰性序列的成员的产生方法是:调用那个传给repeatedly的函数,像这样:

1
2
3
4
5
(defn always-1
[]
1)
(take 5 (repeatedly always-1))
; => (1 1 1 1 1)

Here’s how you’d create a lazy seq of random numbers between 0 and 9:

下面是如何创建一个0到9之间的惰性序列:

1
2
(take 5 (repeatedly (partial rand-int 10)))
; => (1 5 0 3 4)

Let’s use repeatedly to create example data that consists of a sequence of 3,000 random strings, each 7,000 characters long. We’ll compare map and pmap by using them to run clojure.string/lowercase on the orc-names sequence created here:

让我们使用repeatedly创建样本数据,它是一个包含3000个随机字符串的序列,每个字符串有7000个字符。使用mappmap在这个叫做orc-names的序列上运行clojure.string/lowercase,从而进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(def alphabet-length 26)

;; Vector of chars, A-Z
(def letters (mapv (comp str char (partial + 65)) (range alphabet-length)))

(defn random-string
"Returns a random string of specified length"
[length]
(apply str (take length (repeatedly #(rand-nth letters)))))

(defn random-string-list
[list-length string-length]
(doall (take list-length (repeatedly (partial random-string string-length)))))

(def orc-names (random-string-list 3000 7000))

Because map and pmap are lazy, we have to force them to be realized. We don’t want the result to be printed to the REPL, though, because that would take forever. The dorun function does just what we need: it realizes the sequence but returns nil:

mappmap是惰性的,必须强制它们被实例化。但我们不想让结果打印到REPL,因为花的时间太长了。dorun函数正好是我们需要的:它实例化这个序列,但返回nil:

1
2
3
4
5
(time (dorun (map clojure.string/lower-case orc-names)))
; => "Elapsed time: 270.182 msecs"

(time (dorun (pmap clojure.string/lower-case orc-names)))
; => "Elapsed time: 147.562 msecs"

The serial execution with map took about 1.8 times longer than pmap, and all you had to do was add one extra letter! Your performance may be even better, depending on the number of cores your computer has; this code was run on a dual-core machine.

串行执行的map用时大约比pmap长1.8倍,而你要做的所有工作就是多加一个字母!你的性能可能更好,取决于你计算机的处理器个数,上面是双核机器的运行结果。

You might be wondering why the parallel version didn’t take exactly half as long as the serial version. After all, it should take two cores only half as much time as a single core, shouldn’t it? The reason is that there’s always some overhead involved with creating and coordinating threads. Sometimes, in fact, the time taken by this overhead can dwarf the time of each function application, and pmap can actually take longer than map. Figure 10-3 shows how you can visualize this.

你可能奇怪为什么并行运行的时间不是串行的正好一半。双核只需花费单核一半的运行时间,对吗?原因是创建和协调线程总会损耗一些时间。实际上,有时候损耗时间可能比每个函数调用的时间还长,因此,pmap可能比map用时长。图10-3展示了这个情况。

Figure 10-3: Parallelization overhead can dwarf task time, resulting in a performance decrease.

图10-3: 并行化损耗时间可能超过任务时间,引起性能下降。

pmap

We can see this effect at work if we run a function on 20,000 abbreviated orc names, each 300 characters long:

为了看到这个效应,我们把序列个数改成两万个,每个名字的长度改成300:

1
2
3
4
5
(def orc-name-abbrevs (random-string-list 20000 300))
(time (dorun (map clojure.string/lower-case orc-name-abbrevs)))
; => "Elapsed time: 78.23 msecs"
(time (dorun (pmap clojure.string/lower-case orc-name-abbrevs)))
; => "Elapsed time: 124.727 msecs"

Now pmap actually takes 1.6 times longer.

现在pmap的用时是map的1.6倍。

The solution to this problem is to increase the grain size, or the amount of work done by each parallelized task. In this case, the task is to apply the mapping function to one element of the collection. Grain size isn’t measured in any standard unit, but you’d say that the grain size of pmap is one by default. Increasing the grain size to two would mean that you’re applying the mapping function to two elements instead of one, so the thread that the task is on is doing more work. Figure 10-4 shows how an increased grain size can improve performance.

解决方法是加大颗粒度,即每次并行执行的任务数。这个例子里,每次并行执行的任务是对集合的一个成员调用映射函数。默认的颗粒度是1,如果增加到2,每个线程里就会对两个成员执行映射函数。图10-4演示了增加颗粒度能提升性能。

Figure 10-4: Visualizing grain size in relation to parallelization overhead

图10-4: 颗粒度与并行损耗的关系

ppmap

To actually accomplish this in Clojure, you can increase the grain size by making each thread apply clojure.string/lower-case to multiple elements instead of just one, using partition-all. partition-all takes a seq and divides it into seqs of the specified length:

可以用partition-all把序列分成多个指定长度的序列,用来增加颗粒度:

1
2
3
(def numbers [1 2 3 4 5 6 7 8 9 10])
(partition-all 3 numbers)
; => ((1 2 3) (4 5 6) (7 8 9) (10))

Now suppose you started out with code that looked like this:

假设开始代码是这样的:

1
(pmap inc numbers)

In this case, the grain size is one because each thread applies inc to an element.

上面的代码,颗粒度是1,因为每个线程对一个成员调用inc

Now suppose you changed the code to this:

现在把代码改成这样:

1
2
3
(pmap (fn [number-group] (doall (map inc number-group)))
(partition-all 3 numbers))
; => ((2 3 4) (5 6 7) (8 9 10) (11))

There are a few things going on here. First, you’ve now increased the grain size to three because each thread now executes three applications of the inc function instead of one. Second, notice that you have to call doall within the mapping function. This forces the lazy sequence returned by (map inc number-group) to be realized within the thread. Third, we need to ungroup the result. Here’s how we can do that:

这里发生了几件事。首先,每个线程执行3次inc调用,因而颗粒度增加到3,然后,必须调用doall,这强制进程内(map inc number-group)返回的惰性序列实例化。第三,需要合并结果。可以这么做:

1
2
3
(apply concat
(pmap (fn [number-group] (doall (map inc number-group)))
(partition-all 3 numbers)))

Using this technique, we can increase the grain size of the orc name lowercase-ification so each thread runs clojure.string/lower-case on 1,000 names instead of just one:

用这个技巧,把上面的代码颗粒度增加到1000,而不是1:

1
2
3
4
5
6
(time
(dorun
(apply concat
(pmap (fn [name] (doall (map clojure.string/lower-case name)))
(partition-all 1000 orc-name-abbrevs)))))
; => "Elapsed time: 44.677 msecs"

Once again the parallel version takes nearly half the time. Just for fun, we can generalize this technique into a function called ppmap, for partitioned pmap. It can receive more than one collection, just like map:

并行的版本又一次只花了几乎一半的时间。可以把这个方法通用化成一个函数,叫ppmap,代表partitioned pmap。它能接受不止一个集合,像map一样:

1
2
3
4
5
6
7
8
9
10
(defn ppmap
"Partitioned pmap, for grouping map ops together to make parallel
overhead worthwhile"
[grain-size f & colls]
(apply concat
(apply pmap
(fn [& pgroups] (doall (apply map f pgroups)))
(map (partial partition-all grain-size) colls))))
(time (dorun (ppmap 1000 clojure.string/lower-case orc-name-abbrevs)))
; => "Elapsed time: 44.902 msecs"

I don’t know about you, but I think this stuff is just fun. For even more fun, check out the clojure.core.reducers library (http://clojure.org/reference/reducers). This library provides alternative implementations of seq functions like map and reduce that are usually speedier than their cousins in clojure.core. The trade-off is that they’re not lazy. Overall, the clojure.core.reducers library offers a more refined and composable way of creating and using functions like ppmap.

我认为这很有趣。还有更有趣的,去看看clojure.core.reducers库。这个库提供了序列函数,如map,reduce的另一种实现,通常比clojure.core里的函数速度更快。代价就是它们不是惰性的。总体上说,clojure.core.reducers库提供了一种方法,用这种方法可以像ppmap那样创建和使用函数,但这种方法更加精致并且可组合。

Summary

总结

In this chapter, you learned more than most people know about safely handling concurrent tasks. You learned about the metaphysics that underlies Clojure’s reference types. In Clojure metaphysics, state is the value of an identity at a point in time, and identity is a handy way to refer to a succession of values produced by some process. Values are atomic in the same way numbers are atomic. They’re immutable, and this makes them safe to work with concurrently; you don’t have to worry about other threads changing them while you’re using them.

这章你学习了很多安全处理并发任务的知识。了解了Clojure引用类型后面的哲学体系。Clojure哲学体系里,状态是身份某一时间点的值,身份指某个过程产生的连续值。值同数字一样是不可分的。值是不可变的,这使并发使用它们是安全的,使用时不用担心它们被其他线程改变。

The atom reference type allows you to create an identity that you can safely update to refer to new values using swap! and reset!. The ref reference type is handy when you want to update more than one identity using transaction semantics, and you update it with alter and commute.

用原子引用类型可以创建一个身份,可以用swap!reset!可以安全地更新原子,使它指向新值。当需要使用事务语意更新超过一个身份时,使用引用非常方便,可以用altercommute更新引用。

Additionally, you learned how to increase performance by performing stateless data transformations with pmap and the core.reducers library. Woohoo!

另外,还学习了用pmap和core.reducers库执行无状态的数据转换以提高性能。哇!

Exercises

练习

  1. Create an atom with the initial value 0, use swap! to increment it a couple of times, and then dereference it.

  2. Create a function that uses futures to parallelize the task of downloading random quotes from http://www.braveclojure.com/random-quote using (slurp “http://www.braveclojure.com/random-quote“). The futures should update an atom that refers to a total word count for all quotes. The function will take the number of quotes to download as an argument and return the atom’s final value. Keep in mind that you’ll need to ensure that all futures have finished before returning the atom’s final value. Here’s how you would call it and an example result:

    1
    2
    (quote-word-count 5)
    ; => {"ochre" 8, "smoothie" 2}
  3. Create representations of two characters in a game. The first character has 15 hit points out of a total of 40. The second character has a healing potion in his inventory. Use refs and transactions to model the consumption of the healing potion and the first character healing.


译文结束。