Type Classes: Things I wish someone had explained about functional programming
类型类:我希望有人能解释的关于函数式编程的事情
This is part three of a four-part series: Things I wish someone had explained to me about functional programming.
这是四部分系列的第三部分:我希望有人能向我解释的关于函数式编程的事情。
- Part 1: Faulty Assumptions
第 1 部分:错误的假设 - Part 2: Algebraic Structures
第 2 部分:代数结构 - Part 3: Type classes 第 3 部分:类型类
- Part 4: Algebraic Data Types
第 4 部分:代数数据类型
In the last article we discussed algebraic structures. They’re super abstract, which can make them difficult to get into. But they’re also powerful. So powerful, it’s surprising more people aren’t writing about algebraic structures everywhere. And there’s reasons for that. Sometimes people write about one algebraic structure as if they represented all structures. Monads, for example. Sometimes it’s because people don’t know what they’re called. But most often, it’s because people write about type classes instead. So, let’s talk about type classes.
在上一篇文章中,我们讨论了代数结构。它们非常抽象,这可能使得人们难以理解。但它们也很强大。如此强大,以至于令人惊讶的是,更多的人没有在各处撰写关于代数结构的内容。这是有原因的。有时人们会将一种代数结构写成好像它代表了所有结构。例如单子。有时是因为人们不知道它们被称为什么。但最常见的情况是,人们更倾向于写关于类型类的内容。因此,让我们来谈谈类型类。
Typeclasses vs. algebraic structures
类型类 vs. 代数结构
Type classes are not the same thing as algebraic structures. But you’ll find many people use the terms interchangeably. And that can be confusing. It confused me for a long time. For example, the Haskell community has a popular reference on algebraic structures. It’s called ‘Typeclassopedia.’ Why do people talk about type classes when they mean algebraic structures? The reason is, type classes are used to implement algebraic structures. They’re a language feature, rather than a mathematical concept. In a languages with type classes, you’ll find they’re not used for much else. So you can understand why people might be a little loose with terminology.
类型类与代数结构并不是同一回事。但你会发现许多人将这两个术语互换使用。这可能会让人困惑。这让我困惑了很长时间。例如,Haskell 社区有一个关于代数结构的热门参考资料。它被称为‘Typeclassopedia’。为什么人们在谈论代数结构时却提到类型类?原因是,类型类用于实现代数结构。它们是一种语言特性,而不是数学概念。在具有类型类的语言中,你会发现它们并没有用于其他目的。因此,你可以理解为什么人们在术语上可能会有些松散。
It’s even more confusing if you come from a JavaScript background. JavaScript doesn’t have built-in language support for type-classes. That makes it clunky to use them (though not impossible). In the JavaScript world, we tend to talk about algebraic structures instead. And that’s OK. But let’s assume you’re serious about learning functional programming. At some point you’ll run out of good JavaScript tutorials. Eventually, you’ll need to learn from people writing about other languages. When you get there, it will help a lot to understand type classes.
如果你来自 JavaScript 背景,这就更令人困惑了。JavaScript 没有内置的语言支持类型类。这使得使用它们变得笨拙(尽管不是不可能)。在 JavaScript 世界中,我们倾向于谈论代数结构。这没问题。但假设你认真想学习函数式编程。在某个时刻,你会用尽好的 JavaScript 教程。最终,你需要向编写其他语言的人学习。当你到达那里时,理解类型类将大有帮助。
What is a type class then?
那么,什么是类型类?
What’s a type class? In short, type classes are a way of doing polymorphism. And they happen to be most convenient for building algebraic structures. But to get a good feel for why they exist, let’s do a thought experiment. It’s a little round-about, but we’ll get there. Bear with me.
什么是类型类?简而言之,类型类是一种实现多态的方法。它们在构建代数结构时最为方便。但为了更好地理解它们存在的原因,让我们进行一个思想实验。这有点绕,但我们会到达目的地。请耐心等待。
To start, think back to our trusty functor structure. What if (in an alternate universe) we didn’t have the built-in .map()
method for arrays? Good old Array.prototype.map
ceased to exist. It would be inconvenient. But not for long. It wouldn’t be hard to get our .map()
method back. We could write our own:
首先,回想一下我们可靠的函子结构。如果在一个平行宇宙中,我们没有内置的 .map()
数组方法呢?老旧的 Array.prototype.map
不复存在。这会很不方便。但不会太久。我们可以很容易地找回我们的 .map()
方法。我们可以自己编写:
Array.prototype.map = function map(f) {
const out = [];
for (let x of this) {
out.push(f(x));
}
return out;
};
Not too hard, was it? And now, let’s look at another functor. Here’s a .map()
method for Maybe:
这并不难,对吧?现在,让我们看看另一个函子。这是一个 Maybe 的 .map()
方法:
Maybe.prototype.map = function(f) {
if (this.isNothing()) {
return Maybe.of(null);
}
return Maybe.of(f(this.__value));
};
So far, nothing radical going on here. But let’s take this thought experiment a little further. Imagine we wanted to use functions instead of methods to make functors. As in, we’d like to create functors like Maybe and Array, but not use methods at all. Plain functions. No this
. (This is not an unreasonable idea at all, by the way).
到目前为止,这里没有什么激进的事情。但让我们把这个思想实验进一步推进。想象一下,我们想用函数而不是方法来制作函子。也就是说,我们想创建像 Maybe 和 Array 这样的函子,但完全不使用方法。普通函数。没有 this
。(顺便说一下,这并不是一个不合理的想法)。
Could we do it? Well, yes. Of course we could. All we do is take this
or this.__value
and make it a parameter. And so our two map functions might look like so:
我们能做到吗?当然可以。我们只需将 this
或 this.__value
作为参数传入。因此,我们的两个 map 函数可能看起来是这样的:
// Map for arrays.
function map(f, xs) {
const out = [];
for (let x of xs) {
out.push(f(x));
}
return out;
};
// Map for Maybe.
function map(f, x) {
if (x.isNothing()) {
return x;
}
return Maybe.of(f(x.__value));
};
Except, now we have a problem. This code above won’t work. JavaScript won’t let us have two functions called map
in the same scope. One will overwrite the other. Instead, we either use methods, or rename our functions. For example:
但是,现在我们遇到了一个问题。上面的代码无法工作。JavaScript 不允许我们在同一作用域中有两个名为 map
的函数。一个会覆盖另一个。相反,我们要么使用方法,要么重命名我们的函数。例如:
// Map for arrays.
function arrayMap(f, xs) {
const out = [];
for (let x of xs) {
out.push(f(x));
}
return out;
};
// Map for Maybe.
function maybeMap(f, x) {
if (x.isNothing()) {
return x;
}
return Maybe.of(f(x.__value));
};
If you’re used to JavaScript, this makes sense. You can’t have two functions with the same name in the same scope. But in a language like Haskell, it’s different.
如果你习惯了 JavaScript,这就很有意义。你不能在同一作用域中有两个同名的函数。但在像 Haskell 这样的语言中,情况就不同了。
Why? Because of types. Haskell has a ‘static’ type system. JavaScript has a ‘dynamic’ type system. In JavaScript, there’s no way for the computer to tell that map
for array is different from map
for Maybe. But in Haskell, the type signatures for those two functions are different. They might look something like this:
为什么?因为类型。Haskell 有一个‘静态’类型系统。JavaScript 有一个‘动态’类型系统。在 JavaScript 中,计算机无法判断 map
对于数组与 map
对于 Maybe 是不同的。但在 Haskell 中,这两个函数的类型签名是不同的。它们可能看起来像这样:
-- Type signature of map for arrays/lists.
map :: (a -> b) -> [a] -> [b]
-- Type signature of map for Maybe
map :: (a -> b) -> Maybe a -> Maybe b
Two different type signatures. Because the types are different, Haskell’s compiler can figure out which map
to call. It can look at the arguments, figure out their types, and call the correct version. And so the two versions of map
can exist side-by-side. (Unlike in JavaScript).
两个不同的类型签名。因为类型不同,Haskell 的编译器可以确定调用哪个 map
。它可以查看参数,确定它们的类型,并调用正确的版本。因此这两个版本的 map
可以并存。(与 JavaScript 不同。)
Languages with this feature use it to create algebraic structures. We can say, for example, “I’m going to create a new instance of Functor. Here’s its map
function.” In code, it might look like this: 1
具有此功能的语言使用它来创建代数结构。我们可以说,例如,“我将创建一个新的 Functor 实例。这里是它的 map
函数。”在代码中,它可能看起来像这样: 1
instance Functor List where
map :: (a -> b) -> [a] -> [b]
map f xs = foldl (\x arr -> arr ++ [f x]) [] xs
And we could declare Maybe a functor too:
我们也可以声明 Maybe 是一个函子:
instance Functor Maybe where
map :: (a -> b) -> Maybe a -> Maybe b
map f (Just a) = Just f a
map _ Nothing = Nothing
Don’t worry if all that Haskell is gobbledygook. All it means is we can define different versions of map
for different types. This language feature is built into Haskell. And it lets us declare a name for these things-that-can-be-mapped. In this case, Functor.
Don’t worry if all that Haskell is gobbledygook. All it means is we can define different versions of map
for different types. This language feature is built into Haskell. And it lets us declare a name for these things-that-can-be-mapped. In this case, Functor. 不用担心这些 Haskell 术语。如果你看到的都是胡言乱语。它的意思是我们可以为不同的类型定义不同版本的 map
。这个语言特性是内置于 Haskell 中的。它让我们可以为这些可以映射的东西声明一个名称。在这种情况下,Functor。
Languages providing this feature call this thing-you-can-create-an-instance-of, a type class. And type classes are often used to create algebraic structures. But that’s not the only thing you can do with them. What type classes do is enable a specific kind of polymorphism. That is, they let us use the same ‘function’ with different types. Even if we don’t know up front what those types might be. And that happens to be a convenient way to define algebraic structures.
Languages providing this feature call this thing-you-can-create-an-instance-of, a type class. And type classes are often used to create algebraic structures. But that’s not the only thing you can do with them. What type classes do is enable a specific kind of polymorphism. That is, they let us use the same ‘function’ with different types. Even if we don’t know up front what those types might be. And that happens to be a convenient way to define algebraic structures. 提供此特性的语言称这种可以创建实例的东西为类型类。类型类通常用于创建代数结构。但这并不是你可以用它们做的唯一事情。类型类的作用是启用特定类型的多态性。也就是说,它们让我们可以使用相同的“函数”与不同的类型。即使我们事先不知道这些类型可能是什么。这恰好是定义代数结构的一种方便方式。
Now, if you’re paying careful attention, you may have noticed that keyword instance
. It’s in both the Haskell code blocks above. And you may well wonder: An instance of what? How do we declare a new type class? In Haskell, the definition for functor looks something like this:2
Now, if you’re paying careful attention, you may have noticed that keyword instance
. It’s in both the Haskell code blocks above. And you may well wonder: An instance of what? How do we declare a new type class? In Haskell, the definition for functor looks something like this: 2 现在,如果你仔细观察,你可能已经注意到那个关键字 instance
。它出现在上面的两个 Haskell 代码块中。你可能会想:一个什么的实例?我们如何声明一个新的类型类?在 Haskell 中,Functor 的定义看起来像这样:2
class Functor f where
map :: (a -> b) -> f a -> f b
This code says we’re creating a new type class called ‘Functor’. And we use the shortcut f
to refer to it in type definitions. For something to qualify as a functor, it must have a map
function. And that map
function must follow the given type signature. That is, map
takes two parameters. The first is a function that takes something of type a
and returns something of type b
. The second is a functor of type f
with something of type a
‘inside’ it.3 Given these, map
must return another functor of the same type f
with something of type b
‘inside’ it.
这段代码表示我们正在创建一个名为 ‘Functor’ 的新类型类。我们使用快捷方式 f
在类型定义中引用它。要使某物符合函子(functor)的资格,它必须具有一个 map
函数。并且该 map
函数必须遵循给定的类型签名。也就是说, map
接受两个参数。第一个是一个接受类型为 a
的某物并返回类型为 b
的某物的函数。第二个是类型为 f
的函子,其中有类型为 a
的某物 ‘在里面’。 3 鉴于这些, map
必须返回另一个相同类型 f
的函子,其中有类型为 b
的某物 ‘在里面’。
Whew. The code is much easier to read than the explanation. Here’s a shorter way to say it: This is a type class called functor. It has a map
function. It does what you’d expect map
to do.
呼。代码比解释更容易阅读。这是更简短的说法:这是一个叫做函子的类型类。它有一个 map
函数。它做你期望的 map
要做的事情。
Again, don’t worry if all that Haskell code doesn’t make sense. The important thing to understand is that it’s about polymorphism. This particular kind is called parametric polymorphism. Type classes let us have many functions with the same name. That is, so long as those functions handle different types. In practice, it allows us to think of all those map functions as if it were one single function. And the Functor
definition makes sure that they all do logically similar tasks.
再说一次,如果所有这些 Haskell 代码没有意义也不要担心。重要的是要理解这与多态性有关。这种特定的类型称为参数多态性。类型类让我们可以有许多同名的函数。也就是说,只要这些函数处理不同的类型。在实践中,它让我们可以将所有这些 map 函数视为一个单一的函数。而 Functor
定义确保它们都执行逻辑上相似的任务。
Type classes and JavaScript
类型类和 JavaScript
JavaScript doesn’t have type classes. At least, it has no built-in language support for them. It is possible to create type classes in JavaScript. You can see an example in this type class implementation based on Sanctuary. If you look closely, you’ll notice that we have to do a bunch of the work to declare them. This is work that the compiler would do for us in a language like Haskell. For example, we’re required to write a predicate function for each type class instance. That predicate determines whether a value can work with the type class we define. In other languages, the compiler would take care of that. Most of the time though, a library author does that work, not the end user. So it’s not as tedious as it might seem.
JavaScript 没有类型类。至少,它没有内置的语言支持。可以在 JavaScript 中创建类型类。您可以在这个基于 Sanctuary 的类型类实现中看到一个例子。如果您仔细观察,您会注意到我们必须做很多工作来声明它们。这是编译器在 Haskell 等语言中为我们完成的工作。例如,我们需要为每个类型类实例编写一个谓词函数。该谓词确定一个值是否可以与我们定义的类型类一起使用。在其他语言中,编译器会处理这个问题。不过,大多数时候,库的作者会做这项工作,而不是最终用户。因此,这并不像看起来那么繁琐。
In practice, almost nobody uses type classes in JavaScript. Which makes me sad. I do wish they were more popular. But for now, the reality is that type classes aren’t practical for most code bases. But all is not lost. We still have polymorphism, even if it’s not parametric polymorphism. Instead of type classes, we use prototypical inheritance. This lets us pass around a bunch of methods along with a value. As a result, we can write a map function (as opposed to a method) that works like this:
在实践中,几乎没有人使用 JavaScript 中的类型类。这让我感到难过。我确实希望它们更受欢迎。但目前的现实是,类型类对大多数代码库来说并不实用。但并非一切都失去。我们仍然拥有多态性,即使它不是参数多态性。我们使用原型继承,而不是类型类。这使我们能够将一堆方法与一个值一起传递。因此,我们可以编写一个像这样的映射函数(而不是方法):
const map = (f, x) => x.map(f);
As long as x
has a .map()
method that obeys the functor laws, this will work just fine. And we achieve much the same thing as type classes. This is what makes libraries like Ramda, Sanctuary and Crocks so powerful. It’s also another reason why that Fantasy Land specification is so important. It gives us all that wonderful polymorphic goodness.
只要 x
有一个遵循函子法则的 .map()
方法,这就可以正常工作。我们实现的效果与类型类非常相似。这就是为什么像 Ramda、Sanctuary 和 Crocks 这样的库如此强大的原因。这也是 Fantasy Land 规范如此重要的另一个原因。它为我们提供了所有那些美妙的多态性特性。
That said, type classes have their advantages. For example, Haskell can refuse to compile if it knows we haven’t defined map
somewhere. JavaScript though, doesn’t know until it runs the code (often in production).
话虽如此,类型类有其优点。例如,如果 Haskell 知道我们没有在某处定义 map
,它可以拒绝编译。然而,JavaScript 直到运行代码(通常是在生产环境中)才知道。
Is this article a waste of time?
这篇文章是浪费时间吗?
Well, it is a waste of time if you’re looking for quick tips to write better JavaScript code. This article won’t help you with that. But this series isn’t about quick practical tips. It’s about helping you help yourself. My aim is to help people avoid the traps I fell into. One of those traps was not understanding type classes. And not understanding how they’re different from algebraic structures. It’s my hope that this will help you understand what others are talking and writing about as you explore.
如果你在寻找快速技巧来编写更好的 JavaScript 代码,那么这确实是浪费时间。这篇文章不会帮助你实现这一点。但这个系列并不是关于快速实用技巧的。它是关于帮助你自助。我的目标是帮助人们避免我曾陷入的陷阱。其中一个陷阱就是不理解类型类,以及不理解它们与代数结构的不同。我希望这能帮助你理解其他人在讨论和写作时所说的内容。
So, we’ve got a handle on algebraic structures and type classes. But the confusing terminology doesn’t stop there. You might think that algebraic data types is another name for algebraic structures. I did. But no. They’re something different again. Algebraic data types will be the topic of the next article.
所以,我们已经掌握了代数结构和类型类。但令人困惑的术语并没有就此停止。你可能会认为代数数据类型是代数结构的另一种名称。我曾这样认为。但不是。它们是完全不同的东西。代数数据类型将是下一篇文章的主题。
Enormous thanks to Jethro Larson, Joel McCracken and Kurt Milam for reviewing an earlier draft of this entire series. I really appreciate the feedback and suggestions.
非常感谢 Jethro Larson、Joel McCracken 和 Kurt Milam 审阅了这一系列的早期草稿。我非常感谢他们的反馈和建议。