内容简介:【译】Brave Clojure 第十三章:用Multimethod,Protocol和Record创建并扩展抽象
本文是我对Clojure书籍 CLOJURE FOR THE BRAVE AND TRUE 第十二章 Creating and Extending Abstractions with Multimethods, Protocols, and Records 做的翻译。翻译形式,中英对照,英文引用跟着中文翻译。如有错误,在所难免,欢迎指正。
译文开始。
Take a minute to contemplate how great it is to be one of Mother Nature’s top-of-the-line products: a human. As a human, you get to gossip on social media, play Dungeons and Dragons, and wear hats. Perhaps more important, you get to think and communicate in terms of abstractions.
用一分钟仔细思考:身为大自然最好的产品:人,有多棒。你可以在社交网络上闲聊,玩龙与地下城,戴帽子。也许更重要的是,你开始从抽象的角度思考和交流。
The ability to think in terms of abstractions is truly one of the best human features. It lets you circumvent your cognitive limits by tying together disparate details into a neat conceptual package that you can hold in your working memory. Instead of having to think the clunky thought “squeezable honking red ball nose adornment,” you only need the concept “clown nose.”
抽象思考能力真的是人类最棒的一个特征。它把许多不同的细节合并成一个整齐的、你的工作记忆能够持有的概念,从而使你避免了认知限制。你只需要知道”小丑鼻子”这个概念,而不需要这个笨重的想法:“可压缩的,可出声的红色球状鼻子用的装饰物”。
In Clojure, an abstraction
is a collection of operations, and data types
implement abstractions. For example, the seq abstraction consists of operations like first
and rest
, and the vector data type is an implementation of that abstraction; it responds to all of the seq operations. A specific vector like [:seltzer :water]
is an instance
of that data type.
Clojure的 抽象
是操作的集合,并且 数据类型
实现抽象。例如,seq抽象由 first
, rest
这样的操作组成,同时vector数据类型实现了这个抽象,vector对所有的seq操作做出响应。 [:seltzer :water]
这样的特定vector是这种数据类型的 实例
。
The more a programming language lets you think and write in terms of abstractions, the more productive you will be. For example, if you learn that a data structure is an instance of the seq abstraction, you can instantly call forth a large web of knowledge about what functions will work with the data structure. As a result, you spend time actually using the data structure instead of constantly looking up documentation on how it works. By the same token, if you extend a data structure to work with the seq abstraction, you can use the extensive library of seq functions on it.
一门编程语言越多地让你面向抽象思考和编程,你就越有效率。例如,如果你知道一个数据结构是seq抽象的实例,你就能马上知道哪些函数能用于这个数据结构。结果就是你的时间都用在使用数据结构上,而不是查文档了解它如何工作。同样,如果你用seq抽象扩展一个数据结构,你就能对它使用seq函数。
In Chapter 4, you learned that Clojure is written in terms of abstractions. This is powerful because in Clojure you can focus on what you can actually do with data structures and not worry about the nitty-gritty of implementation. This chapter introduces you to the world of creating and implementing your own abstractions. You’ll learn the basics of multimethods, protocols, and records.
你在第4章知道了Clojure是面向抽象编程的。这很强大,因为你能把注意力集中在用数据结构能干什么,同时不用关心实现细节。这章介绍如何创建和实现自己的抽象,将学习multimethods, protocols和records的基础知识。
Polymorphism
多态
The main way we achieve abstraction in Clojure is by associating an operation name with more than one algorithm. This technique is called polymorphism
. For example, the algorithm for performing conj
on a list is different from the one for vectors, but we unify them under the same name to indicate that they implement the same concept, namely, add an element to this data structure
.
Clojure里获得抽象的主要方法是关联一个操作名到多个算法上。这个技术叫 多态
。比如,list上执行 conj
的算法与vector不同,但我们统一用一个名字指明他们实现了同样的概念,即 往这个数据结构添加成员
。
Because Clojure relies on Java’s standard library for many of its data types, a little Java is used in this chapter. For example, Clojure strings are just Java strings, instances of the Java class java.lang.String
. To define your own data types in Java, you use classes. Clojure provides additional type constructs: records and types. This book only covers records.
因为Clojure的很多数据类型依靠 Java 标准库,所以这章会用到一点Java。例如Clojure字符串就是Java字符串,Java类 java.lang.String
的实例。在Java里用类定义自己的数据类型。Clojure提供了额外的类型结构: record和type。这本书只讲解record。
Before we learn about records, though, let’s look at multimethods, our first tool for defining polymorphic behavior.
学习record前,先看看multimethod, 第一个定义多态行为的工具。
Multimethods
Multimethods
Multimethods give you a direct, flexible way to introduce polymorphism into your code. Using multimethods, you associate a name with multiple implementations by defining a dispatching function , which produces dispatching values that are used to determine which method to use. The dispatching function is like the host at a restaurant. The host will ask you questions like “Do you have a reservation?” and “Party size?” and then seat you accordingly. Similarly, when you call a multimethod, the dispatching function will interrogate the arguments and send them to the right method, as this example shows:
Multimethods 是一种直接,灵活的引入多态的方法。使用Multimethods可以关联一个名字到多个实现:定义一个返回 调度值 的 调度函数 ,靠 调度值 决定使用哪个 方法 。调度函数就像饭店的老板。会问你诸如“是否预定了座位?”和”聚会人数?”等问题,然后相应地为你安排座位。类似地,调度函数会询问参数,并发送给正确的方法,如下例所示:
(ns were-creatures)
➊ (defmulti full-moon-behavior (fn [were-creature] (:were-type were-creature)))
➋ (defmethod full-moon-behavior :wolf
[were-creature]
(str (:name were-creature) " will howl and murder"))
➌ (defmethod full-moon-behavior :simmons
[were-creature]
(str (:name were-creature) " will encourage people and sweat to the oldies"))
(full-moon-behavior {:were-type :wolf
➍ :name "Rachel from next door"})
; => "Rachel from next door will howl and murder"
(full-moon-behavior {:name "Andy the baker"
➎ :were-type :simmons})
; => "Andy the baker will encourage people and sweat to the oldies"
This multimethod shows how you might define the full moon behavior of different kinds of were-creatures. Everyone knows that a werewolf turns into a wolf and runs around howling and murdering people. A lesser-known species of were-creature, the were-Simmons, turns into Richard Simmons, power perm and all, and runs around encouraging people to be their best and sweat to the oldies. You do not want to get bitten by either, lest you turn into one.
这个multimethod演示了如何定义不同种类的狼人的月圆行为。
We create the multimethod at ➊. This tells Clojure, “Hey, create a new multimethod named full-moon-behavior
. Whenever someone calls full-moon-behavior
, run the dispatching function (fn [were-creature] (:were-type were-creature))
on the arguments. Use the result of that function, aka the dispatching value, to decide which specific method to use!”
在➊处,创建了名字是 full-moon-behavior
的multimethod。只要调用 full-moon-behavior
,就运行调度函数 (fn [were-creature] (:were-type were-creature))
。用结果,即调度值决定使用哪个方法。
Next, we define two methods, one for when the value returned by the dispatching function is :wolf
at ➋, and one for when it’s :simmons
at ➌. Method definitions look a lot like function definitions, but the major difference is that the method name is immediately followed by the dispatch value. :wolf
and :simmons
are both dispatch values. This is different from a dispatching value, which is what the dispatching function returns. The full dispatch sequence goes like this:
接着,我们定义了两个方法,➋处的用于调度函数返回 :wolf
时,➌处的用于调度函数返回 :simmons
时。方法定义很像函数定义,但主要差别是方法名后面是调度值。 :wolf
和 :simons
都是调度值。完整调度过程是这样的:
-
The form
(full-moon-behavior {:were-type :wolf :name "Rachel from next door"})
is evaluated. -
full-moon-behavior
’s dispatching function runs, returning:wolf
as the dispatching value. -
Clojure compares the dispatching value
:wolf
to the dispatch values of all the methods defined forfull-moon-behavior
. The dispatch values are:wolf
and:simmons
. -
Because the dispatching value
:wolf
is equal to the dispatch value:wolf
, the algorithm for:wolf
runs.
-
形式
(full-moon-behavior {:were-type :wolf :name "Rachel from next door"})
被求值。 -
full-moon-behavior
的调度函数运行,返回:wolf
作为调度值。 -
Clojure用
:wolf
与full-moon-behavior
的所有方法的调度值比较。包括:wolf
和:simmons
。 -
因为
:wolf
匹配,所以运行:wolf
对应的逻辑。
Don’t let the terminology trip you up! The main idea is that the dispatching function returns some value, and this value is used to determine which method definition to use.
别让术语困扰你。主要思想就是调度函数返回一个值,用这个值确定使用哪个方法。
Back to our example! Next we call the method twice. At ➍, the dispatching function returns the value :wolf
and the corresponding method is used, informing you that "Rachel from next door will howl and murder"
. At ➏, the function behaves similarly, except :simmons
is the dispatching value.
回到例子,在➍调度函数返回值 :wolf
,在➎处调度函数返回值 :simmons
,分别运行对应的逻辑。
You can define a method with nil
as the dispatch value:
可以定义调度值是 nil
的方法:
(defmethod full-moon-behavior nil
[were-creature]
(str (:name were-creature) " will stay at home and eat ice cream"))
(full-moon-behavior {:were-type nil
:name "Martin the nurse"})
; => "Martin the nurse will stay at home and eat ice cream"
When you call full-moon-behavior
this time, the argument you give it has nil
for its :were-type
, so the method corresponding to nil
gets evaluated and you’re informed that "Martin the nurse will stay at home and eat ice cream"
.
这次运行是,参数的 :were-type
的值是 nil
,所以运行对应的逻辑。
You can also define a default method to use if no other methods match by specifying :default
as the dispatch value. In this example, the :were-type
of the argument given doesn’t match any of the previously defined methods, so the default method is used:
也可以定义调度值是 :default
的方法,作为默认方法。调度函数没有匹配值时候,使用这个方法:
(defmethod full-moon-behavior :default
[were-creature]
(str (:name were-creature) " will stay up all night fantasy footballing"))
(full-moon-behavior {:were-type :office-worker
:name "Jimmy from sales"})
; => "Jimmy from sales will stay up all night fantasy footballing"
One cool thing about multimethods is that you can always add new methods. If you publish a library that includes the were-creatures
namespace, other people can continue extending the multimethod to handle new dispatch values. This example shows that you’re creating your own random namespace and including the were-creatures
namespace, and then defining another method for the full-moon-behavior
multimethod:
multimethod的一个优点是总是可以添加新方法。如果你公布了一个库,包含 were-creatures
命名空间,其他人可以继续扩展这个multimethod以处理新的调度值。下面的例子演示了创建自己的命名空间,并包含了 were-creatures
命名空间,然后定义了另一个 full-moon-behavior
multimethod的方法:
(ns random-namespace
(:require [were-creatures]))
(defmethod were-creatures/full-moon-behavior :bill-murray
[were-creature]
(str (:name were-creature) " will be the most likeable celebrity"))
(were-creatures/full-moon-behavior {:name "Laura the intern"
:were-type :bill-murray})
; => "Laura the intern will be the most likeable celebrity"
Your dispatching function can return arbitrary values using any or all of its arguments. The next example defines a multimethod that takes two arguments and returns a vector containing the type of each argument. It also defines an implementation of that method, which will be called when each argument is a string:
调度函数可以使用任意或全部参数,返回任意值,如下所示:
(ns user)
(defmulti types (fn [x y] [(class x) (class y)]))
(defmethod types [java.lang.String java.lang.String]
[x y]
"Two strings!")
(types "String 1" "String 2")
; => "Two strings!"
Incidentally, this is why they’re called multimethods: they allow dispatch on multiple arguments. I haven’t used this feature very often, but I could see it being used in a role-playing game to write methods that are dispatched according to, say, a mage’s major school of magic and his magic specialization. Either way, it’s better to have it and not need it than need it and not have it.
顺便提一下,这就是叫做multimethods的原因:允许你在多个参数上调度。我不常使用这个特性,但我知道它能用于角色扮演游戏,用这个特性可以写出根据法师的主要魔法流派和魔法专精进行调度的方法。有但不需要比需要但没有强。
Note Multimethods also allow hierarchical dispatching. Clojure lets you build custom hierarchies, which I won’t cover, but you can learn about them by reading the documentation .
注意Multimethods也允许层级调度。Clojure允许你建立自定义层级,我不会讲解,但你可以阅读 文档 。
Protocols
协议
Approximately 93.58 percent of the time, you’ll want to dispatch to methods according to an argument’s type. For example, count
needs to use a different method for vectors than it does for maps or for lists. Although it’s possible to perform type dispatch with multimethods, protocols
are optimized for type dispatch. They’re more efficient than multimethods, and Clojure makes it easy for you to succinctly specify protocol implementations.
大多数情况下,你想根据参数类型分配方法。比如,对于vector,map,list, count
需要使用不同的方法。尽管可以用multimethods执行类型调度, 协议
是更优选择。它比multimethods更高效,并且Clojure让你能很容易地简洁地指明协议实现。
A multimethod is just one polymorphic operation, whereas a protocol is a collection of one or more polymorphic operations. Protocol operations are called methods, just like multimethod operations. Unlike multimethods, which perform dispatch on arbitrary values returned by a dispatching function, protocol methods are dispatched based on the type of the first argument, as shown in this example:
一个multimethod是一个多态操作,而一个协议是一个或多个多态操作的 集合 。协议和multimethod的操作都叫method,但multimethod根据调度函数的返回值决定使用哪个方法,而协议根据第一个参数的类型,如下所示:
(ns data-psychology)
➊(defprotocol ➋Psychodynamics
➌"Plumb the inner depths of your data types"
➍(thoughts [x] "The data type's innermost thoughts")
➎(feelings-about [x] [x y] "Feelings about self or other"))
First, there’s defprotocol
at ➊. This takes a name, Psychodynamics
➋, and an optional docstring, "Plumb the inner depths of your data types"
➌. Next are the method signatures. A method signature
consists of a name, an argument specification, and an optional docstring. The first method signature is named thoughts
➍ and can take only one argument. The second is named feelings-about
➎ and can take one or two arguments. Protocols do have one limitation: the methods can’t have rest arguments. So a line like the following isn’t allowed:
➊的 defprotocol
定义一个协议。➋处的 Psychodynamics
是协议名,➌处是可选的文档字符串。后面是方法签名,由方法名字,参数和一个可选的文档字符串组成。➍处和➎处的方法签名分别接收一个和一个或两个参数。协议有个限制,方法不能有rest参数。所以不允许下面这样:
(feelings-about [x] [x & others])
By defining a protocol, you’re defining an abstraction, but you haven’t yet defined how that abstraction is implemented. It’s like you’re reserving names for behavior (in this example, you’re reserving thoughts
and feelings-about
), but you haven’t defined what exactly the behavior should be. If you were to evaluate (thoughts "blorb")
, you would get an exception that reads, “No implementation of method: thoughts of protocol: data-psychology/Psychodynamics found for class: java.lang.String.” Protocols dispatch on the first argument’s type, so when you call (thoughts "blorb")
, Clojure tries to look up the implementation of the thoughts
method for strings, and fails.
定义完一个协议,就定义了一个抽象,但还没有定义如何实现这个抽象。就好像保留了行为的名字(这个例子里的 thoughts
和 feelings-about
),但还没有确切定义行为是什么。如果你求值 (thoughts "blorb")
,会得到异常,说”No implementation of method: thoughts of protocol: data-psychology/Psychodynamics found for class: java.lang.String.”。协议根据第一个参数的类型调度,所以调用 (thoughts "blorb")
时,会查找字符串的 thoughts
方法,然后失败了。
You can fix this sorry state of affairs by extending
the string data type to implement the Psychodynamics
protocol:
你可以 扩展
字符串数据类型,实现 Psychodynamics
协议:
➊ (extend-type java.lang.String
➋ Psychodynamics
➌ (thoughts [x] (str x " thinks, 'Truly, the character defines the data type'"))
➍ (feelings-about
([x] (str x " is longing for a simpler way of life"))
([x y] (str x " is envious of " y "'s simpler way of life"))))
(thoughts "blorb")
➎ ; => "blorb thinks, 'Truly, the character defines the data type'"
(feelings-about "schmorb")
; => "schmorb is longing for a simpler way of life"
(feelings-about "schmorb" 2)
; => "schmorb is envious of 2's simpler way of life"
extend-type
is followed by the name of the class or type you want to extend and the protocol you want it to support—in this case, you specify the class java.lang.String at ➊ and the protocol you want it to support, Psychodynamics, at ➋. After that, you provide an implementation for both the thoughts method at ➌ and the feelings-about method at ➍. If you’re extending a type to implement a protocol, you have to implement every method in the protocol or Clojure will throw an exception. In this case, you can’t implement just thoughts or just feelings; you have to implement both.
extend-type
后面是要扩展的类或类型,然后是要支持的协议。然后是协议的所有方法的实现。你必须实现协议的每个方法,否则会报错。
Notice that these method implementations don’t begin with defmethod like multimethods do. In fact, they look similar to function definitions, except without defn
. To define a method implementation, you write a form that starts with the method’s name, like thoughts, then supply a vector of parameters and the method’s body. These methods also allow arity overloading, just like functions, and you define multiple-arity method implementations similarly to multiple-arity functions. You can see this in the feelings-about implementation at ➍.
方法定义与函数定义类似,除了没有 defn
。与函数定义类似,方法也可以有多个参数个数,如➍处所示。
After you’ve extended the java.lang.String
type to implement the Psychodynamics
protocol, Clojure knows how to dispatch the call (thoughts "blorb")
, and you get the string "blorb thinks, 'Truly, the character defines the data type'"
at ➎.
扩展 java.lang.String
,实现 Psychodynamics
协议后,就可以调用 (thoughts "blorb")
,如➎处所示。
What if you want to provide a default implementation, like you did with multimethods? To do that, you can extend java.lang.Object
. This works because every type in Java (and hence, Clojure) is a descendant of java.lang.Object
. If that doesn’t quite make sense (perhaps because you’re not familiar with object-oriented programming), don’t worry about it—just know that it works. Here’s how you would use this technique to provide a default implementation for the Psychodynamics
protocol:
如果想提供默认实现,你可以扩展 java.lang.Object
。因为每个Java类型都是其后代:
(extend-type java.lang.Object
Psychodynamics
(thoughts [x] "Maybe the Internet is just a vector for toxoplasmosis")
(feelings-about
([x] "meh")
([x y] (str "meh about " y))))
(thoughts 3)
; => "Maybe the Internet is just a vector for toxoplasmosis"
(feelings-about 3)
; => "meh"
(feelings-about 3 "blorb")
; => "meh about blorb"
Because we haven’t defined a Psychodynamics
implementation for numbers, Clojure dispatches calls to thoughts
and feelings-about
to the implementation defined for java.lang.Object
.
可以看到,默认实现对数字起作用了。
Instead of making multiple calls to extend-type
to extend multiple types, you can use extend-protocol
, which lets you define protocol implementations for multiple types at once. Here’s how you’d define the preceding protocol implementations:
extend-type
一次扩展一个类型,想要一次扩展多个类型,可以使用 extend-protocol
,前面的协议可以这么定义:
(extend-protocol Psychodynamics
java.lang.String
(thoughts [x] "Truly, the character defines the data type")
(feelings-about
([x] "longing for a simpler way of life")
([x y] (str "envious of " y "'s simpler way of life")))
java.lang.Object
(thoughts [x] "Maybe the Internet is just a vector for toxoplasmosis")
(feelings-about
([x] "meh")
([x y] (str "meh about " y))))
You might find this technique more convenient than using extend-type
. Then again, you might not. How does extend-type
make you feel? How about extend-protocol
? Come sit down on this couch and tell me all about it.
可能你发现这样更方便,或感觉 extend-type
更方便。
It’s important to note that a protocol’s methods “belong” to the namespace that they’re defined in. In these examples, the fully qualified names of the Psychodynamics
methods are data-psychology/thoughts
and data-psychology/feelings-about
. If you have an object-oriented background, this might seem weird because methods belong to data types in OOP. But don’t freak out! It’s just another way that Clojure gives primacy to abstractions. One consequence of this fact is that, if you want two different protocols to include methods with the same name, you’ll need to put the protocols in different namespaces.
注意,协议的方法属于协议定义的命名空间。这个例子里,完全限定的 Psychodynamics
方法是 data-psychology/thoughts
和 data-psychology/feelings-about
。如果你有OO背景,这可能看着很怪,因为在OOP里,方法属于数据类型。但别激动,这只是Clojure把抽象放在首要地位的一个方法。其结果是如果你需要不同的协议包含同名的方法,你需要把这些协议放在不同的命名空间里。
Records
记录
Clojure allows you to create records, which are custom, maplike data types. They’re maplike in that they associate keys with values, you can look up their values the same way you can with maps, and they’re immutable like maps. They’re different in that you specify fields for records. Fields are slots for data; using them is like specifying which keys a data structure should have. Records are also different from maps in that you can extend them to implement protocols.
Clojure允许你创建记录,一种自定义的,类似map的数据类型。记录与map相似的地方是:也是键值关联,可以用与map相同的方法查找值,也是不可变的。不同之处是指定记录的字段。字段是存放数据的槽,使用字段就像指定数据结构应该有哪些key。另一个不同是可以扩展记录以实现协议。
To create a record, you use defrecord
to specify its name and fields:
要创建记录,用 defrecord
指定名字和字段:
(ns were-records)
(defrecord WereWolf [name title])
This record’s name is WereWolf
, and its two fields are name
and title
. You can create an instance of this record in three ways:
这个记录名字是 WereWolf
,有两个字段 name
和 title
。创建记录的实例有三种方法:
➊ (WereWolf. "David" "London Tourist")
; => #were_records.WereWolf{:name "David", :title "London Tourist"}
➋ (->WereWolf "Jacob" "Lead Shirt Discarder")
; => #were_records.WereWolf{:name "Jacob", :title "Lead Shirt Discarder"}
➌ (map->WereWolf {:name "Lucian" :title "CEO of Melodrama"})
; => #were_records.WereWolf{:name "Lucian", :title "CEO of Melodrama"}
At ➊, we create an instance the same way we’d create a Java object, using the class instantiation interop call. ( Interop refers to the ability to interact with native Java constructs within Clojure.) Notice that the arguments must follow the same order as the field definition. This works because records are actually Java classes under the covers.
在➊处,我们用类实例化互操作创建了一个实例,与创建Java对象的方法一样。( 互操作 指在Clojure内与源生Java结构交互的能力。)注意参数顺序必须与字段定义顺序相同。这种方法有效是因为在内部记录实际上是Java类。
The instance at ➋ looks nearly identical to the one at ➊, but the key difference is that ->WereWolf
is a function. When you create a record, the factory functions ->RecordName
and map->RecordName
are created automatically. At ➌, map->WereWolf
takes a map as an argument with keywords that correspond to the record type’s fields and returns a record.
➋处与➊处的关键区别是 ->WereWolf
是个函数。创建一个记录时,工厂函数 ->RecordName
和 map->RecordName
被自动创建。在➌处, map->WereWolf
接受一个map作为参数,key对应记录类型的字段。
If you want to use a record type in another namespace, you’ll have to import it, just like you did with the Java classes in Chapter 12. Be careful to replace all dashes in the namespace with underscores. This brief example shows how you’d import the WereWolf
record type in another namespace:
如果想在另一个命名空间使用一个记录,必须引入,就像在12章里引入Java一样。需要要把横线换成下划线。下面的例子演示如何在另一个命名空间引入 WereWolf
:
(ns monster-mash
(:import [were_records WereWolf]))
(WereWolf. "David" "London Tourist")
; => #were_records.WereWolf{:name "David", :title "London Tourist"}
Notice that were_records
has an underscore, not a dash.
注意 were_records
里的下划线。
You can look up record values in the same way you look up map values, and you can also use Java field access interop:
查找记录值时,可以使用map的方法,也可以使用Java字段访问互操作:
(def jacob (->WereWolf "Jacob" "Lead Shirt Discarder"))
➊ (.name jacob)
; => "Jacob"
➋ (:name jacob)
; => "Jacob"
➌ (get jacob :name)
; => "Jacob"
The first example, (.name jacob)
at ➊, uses Java interop, and the examples at ➋ and ➌ access :name
the same way you would with a map.
➊处的方法使用了Java互操作,➋ 和 ➌ 处使用了map的方法。
When testing for equality, Clojure will check that all fields are equal and that the two comparands have the same type:
检查相等时,Clojure会检查所有字段相等而且两个比较者是同一种类型:
➊ (= jacob (->WereWolf "Jacob" "Lead Shirt Discarder"))
; => true
➋ (= jacob (WereWolf. "David" "London Tourist"))
; => false
➌ (= jacob {:name "Jacob" :title "Lead Shirt Discarder"})
; => false
The test at ➊ returns true
because jacob
and the newly created record are of the same type and their fields are equal. The test at ➋ returns false
because the fields aren’t equal. The final test at ➌ returns false
because the two comparands don’t have the same type: jacob
is a WereWolf
record, and the other argument is a map.
➊处相等时因为类型和字段都相同。➋处不等是因为字段不同。➌处不同是因为类型不同。
Any function you can use on a map, you can also use on a record:
任何可以用于map的函数也可以用于记录:
(assoc jacob :title "Lead Third Wheel")
; => #were_records.WereWolf{:name "Jacob", :title "Lead Third Wheel"}
However, if you dissoc
a field, the result’s type will be a plain ol’ Clojure map; it won’t have the same data type as the original record:
但如果 dissoc
一个字段,结果会变成一个普通Clojure map,与原来的记录类型不同:
(dissoc jacob :title)
; => {:name "Jacob"} <- that's not a were_records.WereWolf
This matters for at least two reasons: first, accessing map values is slower than accessing record values, so watch out if you’re building a high-performance program. Second, when you create a new record type, you can extend it to implement a protocol, similar to how you extended a type using extend-type
earlier. If you dissoc
a record and then try to call a protocol method on the result, the record’s protocol method won’t be called.
类型不同很重要的原因有两个:首先,访问map值比访问记录值慢,所以写高性能程序时候要留心。第二,创建一个新记录时,可以扩展它以实现一个协议,与用 extend-type
扩展一个类型类似。如果你 dissoc
一个记录然后在结果上调用协议方法,方法不会被调用。
Here’s how you would extend a protocol when defining a record:
下面是定义记录时如何扩展协议:
➊ (defprotocol WereCreature
➋ (full-moon-behavior [x]))
➌ (defrecord WereWolf [name title]
WereCreature
(full-moon-behavior [x]
(str name " will howl and murder")))
(full-moon-behavior (map->WereWolf {:name "Lucian" :title "CEO of Melodrama"}))
; => "Lucian will howl and murder"
We’ve created a new protocol, WereCreature
➊, with one method, full-moon-behavior
➋. At ➌, defrecord
implements WereCreature
for WereWolf
. The most interesting part of the full-moon-behavior
implementation is that you have access to name
. You also have access to title
and any other fields that might be defined for your record. You can also extend records using extend-type
and extend-protocol
.
➊➋处创建了有一个方法的协议。➌处定义了 WereWolf
记录并实现了 WereCreature
协议。注意, full-moon-behavior
可以访问记录的所有字段。也可以用 extend-type
和 extend-protocol
扩展记录。
When should you use records, and when should you use maps? In general, you should consider using records if you find yourself creating maps with the same fields over and over. This tells you that that set of data represents information in your application’s domain, and your code will communicate its purpose better if you provide a name based on the concept you’re trying to model. Not only that, but record access is more performant than map access, so your program will become a bit more efficient. Finally, if you want to use protocols, you’ll need to create a record.
什么时候应该用记录呢?当你发现自己一次又一次用map创建同样的字段,这说明这是应用程序特定领域的信息,用记录表示合适,因为更清晰。而且访问记录比访问map性能更高。另外如果想使用协议,需要使用记录。
Further Study
进一步学习
Clojure offers other tools for working with abstractions and data types. These tools, which I consider advanced, include deftype
, reify
, and proxy
. If you’re interested in learning more, check out the documentation on data types at http://clojure.org/reference/datatypes
.
Clojure提供了其他抽象和数据类型工具。我认为这些是高级工具,包括 deftype
, reify
和 proxy
。如果你感兴趣,查看文档 http://clojure.org/reference/datatypes
。
Summary
总结
One of Clojure’s design principles is to write to abstractions. In this chapter, you learned how to define your own abstractions using multimethods and prototypes. These constructs provide polymorphism, allowing the same operation to behave differently based on the arguments it’s given. You also learned how to create and use your own associative data types with defrecord
and how to extend records to implement protocols.
Clojure的一个设计原则是面向抽象编程。这章学习了如何用multimethods和protocols定义自己的抽象。这些结构提供了多态,允许同样的操作基于参数行为不同。还学习了如何用 defrecord
创建自己的关联数据类型,和如何扩展记录实现protocols。
When I first started learning Clojure, I was pretty shy about using multimethods, protocols, and records. However, they are used often in Clojure libraries, so it’s good to know how they work. Once you get the hang of them, they’ll help you write cleaner code.
我刚开始学习Clojure时,很少使用multimethods, protocols, 和 records,他们经常用于Clojure库,所以最好知道他们用法。一旦学会了如何使用,它们会帮助你写出更清晰的代码。
Exercises
练习
- Extend the full-moon-behavior multimethod to add behavior for your own kind of were-creature.
- Create a WereSimmons record type, and then extend the WereCreature protocol.
- Create your own protocol, and then extend it using extend-type and extend-protocol.
- Create a role-playing game that implements behavior using multiple dispatch.
译文结束。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- (全栈学习实践)三、创建php、添加扩展,其他Dockerfile编写
- Microsoft开源VS Code扩展Web Template Studio,可轻松创建全栈Web应用
- iOS App创建证书,添加Appid,创建配置文件流程
- 【php 扩展开发】扩展生成器
- 喧喧发布 1.6.0 版本,扩展机制增强,支持服务器扩展
- RabbitMQ集群创建
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。