查看单子的不同方法


29

在学习Haskell的过程中,我遇到了很多教程,试图解释什么是monad,以及为什么monad在Haskell中很重要。他们每个人都使用类比,因此更容易理解其含义。归根结底,我对单子是3种不同的看法:

查看1:将Monad作为标签

有时我认为将monad作为特定类型的标签。例如,一个类型的函数:

myfunction :: IO Int

myfunction是一个函数,无论何时执行它都会产生一个Int值。结果的类型不是Int,而是IO Int。因此,IO是Int值的标签,警告用户知道Int值是执行IO操作的过程的结果。

因此,此Int值已被标记为来自具有IO的过程的值,因此该值为“脏”。您的过程不再是纯粹的。

视图2:莫纳德(Monad)是可能发生令人讨厌的事情的私人空间。

在所有过程都是纯且严格的系统中,有时您需要产生副作用。因此,monad只是一个很小的空间,允许您做讨厌的副作用。在这个空间中,您可以逃脱纯净的世界,走不纯净的事物,进行过程,然后返回有价值的东西。

图3:Monad的范畴论

我不完全理解这种观点。monad只是同一类别或子类别的函子。例如,您具有Int值,并且作为IO Int子类,它们是在IO过程之后生成的Int值。

这些观点正确吗?哪个更准确?


5
#2并不是一般的monad。实际上,它几乎仅限于IO,而不是有用的视图(请参阅什么不是Monad)。同样,“严格”通常用于命名Haskell 拥有的属性(即严格评估)。顺便说一下,Monads也不会改变(再次,请参阅什么不是Monad)。

3
从技术上讲,只有第三个是正确的。Monad是endofunctor,因为它定义了特殊操作-升级和绑定。Monad数量众多-列出Monad是在Monad背后获得直觉的完美示例。readS设施更好。令人惊讶的是,monad可用作隐式地以纯功能语言对状态进行线程化的工具。这不是monad的定义属性:碰巧的是,状态线程可以用它们的术语来实现。IO同样适用。
permeakra 2012年

Common Lisp作为语言的一部分有其自己的编译器。Haskell有Monads。
尼斯

Answers:


33

视图#1和#2通常是不正确的。

  1. 任何类型的数据类型都* -> *可以充当标签,而monad远不止于此。
  2. (除IOmonad之外)monad中的计算并非不纯。它们只是代表我们认为具有副作用的计算,但它们是纯净的。

这两种误解都源于对IOmonad的关注,这实际上有点特殊。

我将尝试详细阐述#3,如果可能的话,不要进入类别理论。


标准计算

可以将函数式编程语言中的所有计算视为具有源类型和目标类型的函数:f :: a -> b。如果一个函数有多个参数,则可以通过curring将其转换为一个参数的函数(另请参见Haskell wiki)。如果我们只有一个值x :: a(带有0个参数的函数),则可以将其转换为带有单位类型参数的函数:(\_ -> x) :: () -> a

通过使用.运算符编写此类函数,我们可以从简单的程序构建更复杂的程序。例如,如果我们有f :: a -> bg :: b -> c我们得到g . f :: a -> c。请注意,这也适用于转换后的值:如果有x :: a并将其转换为表示形式,则将获得f . ((\_ -> x) :: () -> a) :: () -> b

此表示具有一些非常重要的属性,即:

  • 我们有一个非常特殊的功能- 每种类型的标识功能。它是 关于以下内容的标识元素:与和都相等。id :: a -> aa.ff . idid . f
  • 函数组合算子.关联的

单子计算

假设我们要选择某种特殊类别的计算并进行处理,其结果所包含的内容不只是单个返回值。我们不想指定“更多”的含义,我们想让事情尽可能地笼统。最普遍的方式来表示“更多的东西”是代表它作为一种功能-一种m类型的* -> *(即它的一种类型转换为另一种)。因此,对于要使用的每种计算类别,我们将有一些类型函数m :: * -> *。(在Haskell,m[]IOMaybe,等等)以及类别将包含的类型的所有功能a -> m b

现在,我们希望以与基本情况相同的方式使用此类中的功能。我们希望能够组合这些功能,我们希望组合具有关联性,并且我们希望拥有一个身份。我们需要:

  • 要有一个运算符(让我们称其为<=<),该运算符将功能f :: a -> m b和内容组合g :: b -> m cg <=< 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并没有这样的要求,即返回类型应具有此类值。有些单子总是有(如IOState),但有些不一样,[]Maybe

IO monad

正如我提到的,IOmonad有点特殊。类型的值IO a意味着类型的值a通过与该程序的环境交互构成。因此(与所有其他monad不同),我们无法IO a使用某些纯构造来描述type的值。这IO只是区分与环境交互的计算的标签或标签。这是(唯一的情况)视图#1和#2是正确的。

对于IOmonad:

  • 的组合物f :: a -> IO bg :: b -> IO c方法:计算f与所述环境相互作用,并且然后计算g一个使用值,并计算结果与环境相互作用。
  • return只需将IO“标签” 添加到值中(我们只需通过保持环境完整就可以“计算”结果)。
  • 编译器保证单子定律(关联性,同一性)。

一些注意事项:

  1. 由于单子计算的结果类型始终为m a,因此无法从IO单子中“退出” 。含义是:一旦计算与环境交互,就无法从中构造计算。
  2. 当函数式程序员不知道如何以纯净的方式制作东西时,他可以(作为最后的手段)通过在IOmonad中进行一些有状态的计算来对任务进行编程。这就是为什么IO通常被称为程序员的罪孽箱的原因
  3. 请注意,在不纯净的世界中(从函数编程的意义上来说),读取值也会改变环境(例如消耗用户的输入)。这就是为什么像这样的函数getChar必须具有的结果类型的原因IO something

3
好答案。IO从语言的角度来看,我没有特殊的语义。这并不特殊,它的行为类似于其他任何代码。仅运行时库实现是特殊的。另外,还有一种特殊的转义方式(unsafePerformIO)。我认为这很重要,因为人们经常将其IO视为特殊的语言元素或声明性标记。它不是。
usr

2
@usr好点。我要补充一点,unsafePerformIO确实是不安全的,应仅由专家使用。它允许您破坏所有内容,例如,您可以创建一个coerce :: a -> b转换任意两种类型的函数(在大多数情况下会导致程序崩溃)。见这个例子 -你甚至可以在功能转换成Int
切赫Pudlák

另一个“特殊魔术” monad是ST,它允许您声明对内存的引用,您可以根据自己的喜好对其进行读写(尽管仅在monad中),然后可以通过调用来提取结果runST :: (forall s. GHC.ST.ST s a) -> a
sara

5

查看1:将Monad作为标签

“因此,此Int值已被标记为来自具有IO的进程的值,因此该值为“ dirty”。

通常,“ IO Int”不是Int值(尽管在某些情况下可能是“ return 3”)。这是一个输出一些Int值的过程。此“过程”的不同执行可能会产生不同的Int值。

monad m是一种嵌入式(命令式)“编程语言”:在该语言内可以定义一些“过程”。单调值(类型为ma)是此“编程语言”中的一种程序,输出一个类型a的值。

例如:

foo :: IO Int

是一些输出Int类型值的过程。

然后:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

是输出两个(可能不同)Ints的过程。

每个此类“语言”都支持某些操作:

  • 可以将两个过程(ma和mb)“串联”:您可以创建一个较大的过程(ma >> mb),该过程由第一个过程然后是第二个过程组成;

  • 此外,第一个输出(a)可能会影响第二个输出(ma >> = \ a-> ...);

  • 一个过程(返回x)可能会产生一些恒定值(x)。

不同的嵌入式编程语言在支持的种类方面有所不同,例如:

  • 产生随机值;
  • “分叉”([monad]);
  • 异常(抛出/捕获)(The Either e monad);
  • 明确的继续/ callcc支持;
  • 向其他“代理”发送/接收消息;
  • 创建,设置和读取变量(对于该编程语言而言是本地的)(ST monad)。

1

不要将monadic类型与monad类混淆。

一元类型(即一种类型是monad类的实例)将解决一个特定的问题(原则上,每个一元类型都解决一个不同的问题):状态,随机,也许是IO。它们都是带有上下文的类型(您称之为“标签”,但这并不是使它们成为单子的原因)。

对于所有这些对象,都需要“通过选择链接操作”(一个操作取决于前一个的结果)。monad类在这里起作用:让您的类型(解决给定的问题)成为monad类的实例,并解决链接问题。

请参阅monad类能解决什么?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.