【翻译】Working with type in a context

发现《Get Programming with Haskell》这本书中对 Functor,Applicative,Monad 的概念的引入非常直观有趣,在这里进行一波翻译。我是 Haskell 初学者,且英语水平也不高,所以难免拉垮,望读者海涵并给予意见。文章最后粘贴了英文原文的图片。

在文章中,我将 type 翻译作类型,type class 翻译作类型类,但作者有时会把 Maybe,IO 等称作类型,令人感叹迷惑,总之忠于原文。我一些我拿不准的地方则把英文原文也贴上了。


在这个单元里,你将关注 Haskell 的三个最具威力,但同时也最迷惑人的类型类:FunctorApplicativeMonad。这些类型类名字有趣,但其目的却相对的明确。它们中的每一个都建立在前一个之上,并提供你在诸如IO等上下文中进行操作的能力。在单元 4 里,你大量使用了Monad类型类以操作IO。在这个单元里,你将更深刻地理解其工作原理。为更好地感受这些抽象的类型类的行为,(在这里)你将把类型当作形状来看待。

理解函数的一种方式是认为其将一种类型转换成另一种类型。让我们把两个类型可视化为两个形状,一个圆和一个正方形,就如图 1 所示。

这些形状可以代表任意两个类型,比如IntDoubleStringTextNameFirstName以及其他。当你试图将一个圆转换成一个正方形的时候,你就在使用函数。你可以把函数可视化为两个形状间的一种连接(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 IntIO DoubleMaybe StringMaybe TextMaybe NameMaybe FirstName等的类型。因为这些类型是处于一定的上下文中的,你不能用你的原有的连接去进行转换。当前,在本书中你曾依赖过那些输入和输出都处在同样的上下文中的函数。而为对上下文中的类型进行转换,你需要一个类似图 5 的连接。

这个连接代表那些类型签名形如(Maybe Int -> Maybe Double)(IO String -> IO Text)(IO Name -> IO FirstName)的函数。通过该连接,你很容易对上下文中的类型进行转换,就如图 6 所示。

这看上去像是一个完美的解决方案,但是这里有个问题。让我们看下面这个函数halve,它的类型是Int -> Double,其行为就如我们所期望的,对半分(halve)输入参数Int

1
2
halve :: Int -> Double
halve n = fromIntegral n / 2.0

这个函数很直白,但假设你想对半分一个Maybe Int呢?仅用手头的工具,你必须对这个函数编写一个包装器(wrapper)以使它能够对Maybe类型起作用。

1
2
3
halveMaybe :: Maybe Int -> Maybe Double
halveMaybe Nothing = Nothing
halveMaybe (Just n) = Just (halve n)

在这个例子里,写一个简单的包装器并非难事。但若是对一大片的现存的a -> b函数,想要使用它们中的任意一个操作Maybe类型都需要编写几乎同样的包装器。更糟糕的是你无法编写对IO类型的包装器!

译者:为什么无法编写IO的包装器?你需要对IO类型的实例进行解构并获取它的值,再重新构造它,而解构这一步是无法做到的——这意味着 Haskell 将会提供诸如IO Int -> Int这样签名的函数,这是不安全的——你不能保证函数是纯函数了!假设你又有一个函数Int -> IO Int(这是容易做到的,通过return之类),你就可以将两个函数组合,使其具有Int -> Int之类的函数签名,但是在内部做 dirty work。当然,Haskell 的确提供了这样的 unsafe 函数就是了。

于是,我们的FunctorApplicativeMonad来到了!你可以认为这些类型类是适配器(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中,你关心转换你的基本的IntDouble(的函数),使它能够适配以工作在上下文的类型中。这是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)意味着这个函数本身可能不存在。这(函数被包裹在上下文中)听起来奇怪,但它很有可能发生在对MaybeIO的偏调用中。图 9 描述了这一有趣的情形。

然后还有最后一种可能的函数和上下文中类型不匹配的情形。这种情况是参数不在上下文中,而结果在上下文中。这种情形比你想象中的更加普遍。如Map.lookupputStrLn的类型签名都是这样。这个问题被Monad类型类解决,见图 10。

当你结合使用这三个类型类,只要底层类型匹配,你能把所有函数应用到诸如MaybeIO等上下文。这可是件了不起的事——你可以在上下文中应用任何你想做的计算,并能够不同的上下文中重用大量的现存代码。



本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!