视图#1和#2通常是不正确的。
- 任何类型的数据类型都
* -> *
可以充当标签,而monad远不止于此。
- (除
IO
monad之外)monad中的计算并非不纯。它们只是代表我们认为具有副作用的计算,但它们是纯净的。
这两种误解都源于对IO
monad的关注,这实际上有点特殊。
我将尝试详细阐述#3,如果可能的话,不要进入类别理论。
标准计算
可以将函数式编程语言中的所有计算视为具有源类型和目标类型的函数:f :: a -> b
。如果一个函数有多个参数,则可以通过curring将其转换为一个参数的函数(另请参见Haskell wiki)。如果我们只有一个值x :: a
(带有0个参数的函数),则可以将其转换为带有单位类型参数的函数:(\_ -> x) :: () -> a
。
通过使用.
运算符编写此类函数,我们可以从简单的程序构建更复杂的程序。例如,如果我们有f :: a -> b
和g :: b -> c
我们得到g . f :: a -> c
。请注意,这也适用于转换后的值:如果有x :: a
并将其转换为表示形式,则将获得f . ((\_ -> x) :: () -> a) :: () -> b
。
此表示具有一些非常重要的属性,即:
- 我们有一个非常特殊的功能- 每种类型的标识功能。它是 关于以下内容的标识元素:与和都相等。
id :: a -> a
a
.
f
f . id
id . f
- 函数组合算子
.
是关联的。
单子计算
假设我们要选择某种特殊类别的计算并进行处理,其结果所包含的内容不只是单个返回值。我们不想指定“更多”的含义,我们想让事情尽可能地笼统。最普遍的方式来表示“更多的东西”是代表它作为一种功能-一种m
类型的* -> *
(即它的一种类型转换为另一种)。因此,对于要使用的每种计算类别,我们将有一些类型函数m :: * -> *
。(在Haskell,m
是[]
,IO
,Maybe
,等等)以及类别将包含的类型的所有功能a -> m b
。
现在,我们希望以与基本情况相同的方式使用此类中的功能。我们希望能够组合这些功能,我们希望组合具有关联性,并且我们希望拥有一个身份。我们需要:
- 要有一个运算符(让我们称其为
<=<
),该运算符将功能f :: a -> m b
和内容组合g :: b -> m c
为g <=< f :: a -> m c
。并且,它必须是关联的。
- 要为每种类型具有某些标识功能,我们称之为
return
。我们还希望f <=< return
与相同f
且相同return <=< f
。
任何m :: * -> *
对我们有这样的功能return
,并<=<
称为单子。就像在基本情况下一样,它允许我们从简单的计算创建复杂的计算,但是现在返回值的类型由转换m
。
(实际上,我在这里略微滥用了类别一词。从类别理论的意义上来说,只有在我们知道其结构符合这些定律后,我们才能将其称为类别。)
哈斯克尔的单子
在Haskell(和其他功能语言)中,我们主要处理值,而不处理类型的函数() -> a
。因此<=<
,我们没有为每个monad定义,而是定义了一个function (>>=) :: m a -> (a -> m b) -> m b
。这样的替代定义是等效的,我们可以>>=
使用来表示<=<
,反之亦然(尝试作为练习,或参阅参考资料)。现在,该原理已经不那么明显了,但是仍然保持不变:我们的结果始终是类型m a
,我们组成类型的函数a -> m b
。
对于我们创建的每个monad,我们都不要忘记检查return
并<=<
具有我们需要的属性:关联性和左/右身份。使用表达return
和>>=
他们被称为单子法律。
一个例子-列表
如果选择m
是[]
,我们将得到类型为type的函数的类别a -> [b]
。此类函数表示非确定性计算,其结果可能是一个或多个值,但也可能没有值。这产生了所谓的列表单子。的组成f :: a -> [b]
和g :: b -> [c]
工作原理如下:g <=< f :: a -> [c]
的方法来计算类型的所有可能的结果[b]
,适用g
于每个人,并收集在一个列表中的所有结果。用Haskell表示
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
或使用 >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
请注意,在此示例中,返回类型是,[a]
所以它们可能不包含type的任何值a
。实际上,对于monad并没有这样的要求,即返回类型应具有此类值。有些单子总是有(如IO
或State
),但有些不一样,[]
或Maybe
。
IO monad
正如我提到的,IO
monad有点特殊。类型的值IO a
意味着类型的值a
通过与该程序的环境交互构成。因此(与所有其他monad不同),我们无法IO a
使用某些纯构造来描述type的值。这IO
只是区分与环境交互的计算的标签或标签。这是(唯一的情况)视图#1和#2是正确的。
对于IO
monad:
- 的组合物
f :: a -> IO b
和g :: b -> IO c
方法:计算f
与所述环境相互作用,并且然后计算g
一个使用值,并计算结果与环境相互作用。
return
只需将IO
“标签” 添加到值中(我们只需通过保持环境完整就可以“计算”结果)。
- 编译器保证单子定律(关联性,同一性)。
一些注意事项:
- 由于单子计算的结果类型始终为
m a
,因此无法从IO
单子中“退出” 。含义是:一旦计算与环境交互,就无法从中构造计算。
- 当函数式程序员不知道如何以纯净的方式制作东西时,他可以(作为最后的手段)通过在
IO
monad中进行一些有状态的计算来对任务进行编程。这就是为什么IO
通常被称为程序员的罪孽箱的原因。
- 请注意,在不纯净的世界中(从函数编程的意义上来说),读取值也会改变环境(例如消耗用户的输入)。这就是为什么像这样的函数
getChar
必须具有的结果类型的原因IO something
。