【翻译】Working with type in a context
发现《Get Programming with Haskell》这本书中对 Functor,Applicative,Monad 的概念的引入非常直观有趣,在这里进行一波翻译。我是 Haskell 初学者,且英语水平也不高,所以难免拉垮,望读者海涵并给予意见。文章最后粘贴了英文原文的图片。
在文章中,我将 type 翻译作类型,type class 翻译作类型类,但作者有时会把 Maybe,IO 等称作类型,令人感叹迷惑,总之忠于原文。我一些我拿不准的地方则把英文原文也贴上了。
在这个单元里,你将关注 Haskell 的三个最具威力,但同时也最迷惑人的类型类:Functor
,Applicative
和Monad
。这些类型类名字有趣,但其目的却相对的明确。它们中的每一个都建立在前一个之上,并提供你在诸如IO
等上下文中进行操作的能力。在单元 4 里,你大量使用了Monad
类型类以操作IO
。在这个单元里,你将更深刻地理解其工作原理。为更好地感受这些抽象的类型类的行为,(在这里)你将把类型当作形状来看待。
理解函数的一种方式是认为其将一种类型转换成另一种类型。让我们把两个类型可视化为两个形状,一个圆和一个正方形,就如图 1 所示。
这些形状可以代表任意两个类型,比如Int
和Double
,String
和Text
,Name
和FirstName
以及其他。当你试图将一个圆转换成一个正方形的时候,你就在使用函数。你可以把函数可视化为两个形状间的一种连接(connector),如图 2 所示。
译者:显然,这两个形状也可以代表同一个类型。
这个连接可以代表任何从一个类型到另一个类型的函数。它可以代表(Int -> Double)
,(String -> Text)
,(Name -> FirstName)
,诸如此类。当你试图应用一个转换时,你可以可视化地将连接器置于初始值(在当前的情形下,是一个圆形)以及期望值(一个正方形)之间;见图 3。
当每个形状都正确匹配,你就能完成你所期望的转换。
在这个单元,你将关注如何操作处于上下文(context)中的类型。你已经见过的两个关于上下文中的类型的最好的例子是Maybe
类型和IO
类型。Maybe
类型代表这样一种上下文,即其中的值可能不存在;IO
类型代表着这样一种上下文,即其中的值将同 I/O 交互(the value has interacted with I/O)。放到我们的可视化语言中,你可以想象上下文中的类型将像图 4 这样表述。
这些形状可以代表诸如IO Int
,IO Double
,Maybe String
,Maybe Text
,Maybe Name
,Maybe FirstName
等的类型。因为这些类型是处于一定的上下文中的,你不能用你的原有的连接去进行转换。当前,在本书中你曾依赖过那些输入和输出都处在同样的上下文中的函数。而为对上下文中的类型进行转换,你需要一个类似图 5 的连接。
这个连接代表那些类型签名形如(Maybe Int -> Maybe Double)
,(IO String -> IO Text)
和(IO Name -> IO FirstName)
的函数。通过该连接,你很容易对上下文中的类型进行转换,就如图 6 所示。
这看上去像是一个完美的解决方案,但是这里有个问题。让我们看下面这个函数halve
,它的类型是Int -> Double
,其行为就如我们所期望的,对半分(halve)输入参数Int
。
1 |
|
这个函数很直白,但假设你想对半分一个Maybe Int
呢?仅用手头的工具,你必须对这个函数编写一个包装器(wrapper)以使它能够对Maybe
类型起作用。
1 |
|
在这个例子里,写一个简单的包装器并非难事。但若是对一大片的现存的a -> b
函数,想要使用它们中的任意一个操作Maybe
类型都需要编写几乎同样的包装器。更糟糕的是你无法编写对IO
类型的包装器!
译者:为什么无法编写
IO
的包装器?你需要对IO
类型的实例进行解构并获取它的值,再重新构造它,而解构这一步是无法做到的——这意味着 Haskell 将会提供诸如IO Int -> Int
这样签名的函数,这是不安全的——你不能保证函数是纯函数了!假设你又有一个函数Int -> IO Int
(这是容易做到的,通过return
之类),你就可以将两个函数组合,使其具有Int -> Int
之类的函数签名,但是在内部做 dirty work。当然,Haskell 的确提供了这样的 unsafe 函数就是了。
于是,我们的Functor
,Applicative
和Monad
来到了!你可以认为这些类型类是适配器(adapter),它们允许你在底层(underlying)类型(圆和正方形)相同的情况下使用不同的连接(You can think of these type classes as adapters that allow you to work with different connectors so long as the underlying types (circle and square) are the same)。比如在halve
中,你关心转换你的基本的Int
到Double
(的函数),使它能够适配以工作在上下文的类型中。这是Functor
类型类的工作,如图 7。
译者:图中文字为:
Functor
类型类能够解决上下文中的类型和连接不匹配的问题。这也就是说
Functor
能够使类型a -> b
的函数将(Functor f) => f a
类型转化为(Functor f) => f b
。换句话说,Functor
能够将a -> b
转化成(Functor f) => f a -> f b
。如果你曾了解过
Functor
的方法(是这么叫吗?)fmap
,查看它的签名fmap :: (Functor f) => (a -> b) -> f a -> f b
,就容易发现上面的“换句话说”两边的描述其实就是对fmap
的不同诠释。
(Functor
能解决一种类型不匹配问题),但仍有其它三种类型不匹配问题。Applicative
能解决其中两种。其中第一种情况是连接的第一部分在上下文中,而结果不在,如图 8。
另一种情形则是整个函数都在上下文中。比如函数Maybe (Int -> Double)
意味着这个函数本身可能不存在。这(函数被包裹在上下文中)听起来奇怪,但它很有可能发生在对Maybe
和IO
的偏调用中。图 9 描述了这一有趣的情形。
然后还有最后一种可能的函数和上下文中类型不匹配的情形。这种情况是参数不在上下文中,而结果在上下文中。这种情形比你想象中的更加普遍。如Map.lookup
和putStrLn
的类型签名都是这样。这个问题被Monad
类型类解决,见图 10。
当你结合使用这三个类型类,只要底层类型匹配,你能把所有函数应用到诸如Maybe
,IO
等上下文。这可是件了不起的事——你可以在上下文中应用任何你想做的计算,并能够不同的上下文中重用大量的现存代码。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!