从我的阅读中我了解到Monad主要用于:
-功能组成 通过将一种功能输出类型与另一种功能输入类型进行匹配。
我认为这是一篇非常好的文章:
http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
它用盒子/包装的概念解释了Monads。但是我不明白这些包装纸是做什么用的?包装纸除了成分之外还有什么好处?
IO Monad也很常见。
name <- getLine -- name has the type String and getLine IO String
那么这种类型差异的好处是什么?是错误处理吗?
答案 0 :(得分:17)
将Monads视为 things (名词)是混乱的根源。 Monad更像形容词。您不会问“蓝色”或“薄”有什么用。您会发现一些有用的东西,例如一本蓝皮书或一支细笔,然后注意到一些图案-有些东西是蓝色的,有些是细的,有些都不是。
与单子相似。要了解单子,您首先应该对是单子的事物有一些经验:Maybe
,Either
,Reader
,State
。了解它们的工作原理,>>=
和return
对它们的作用以及它们的有用性,以及使用Monad类如何不使用这些类型的方法。 (因此,请不要从IO开始。)然后,您将准备注意到这些不同类型之间的共性,并欣赏它们为何遵循一种称为Monad的通用模式。
Monad只是各种类型的有用界面,但是您必须先熟悉类型本身,然后才能欣赏它,就像如果您从未见过任何蓝色时就无法欣赏“蓝色”一词一样东西。
答案 1 :(得分:3)
I / O不纯。例如,读取文件的内容可以在不同的时间点给出不同的结果。读取当前系统时间始终会得出不同的结果。生成随机数会为每个呼叫提供不同的结果。显然(或显然),这些类型的操作依赖于其功能参数以外的其他东西。某种状态。对于IO monad,此状态甚至位于Haskell程序之外!
您可以将monad视为函数调用的“额外参数”。因此,IO monad中的每个函数还会获得一个“包含”程序外部的参数。
您可能想知道为什么这很重要。原因之一是,只要语义保持不变,优化就可以更改程序的执行顺序。要计算表达式1 + 4 - 5
,先进行加法还是减法都没有关系。但是,如果每个值都是文件中的行,则读取它们的顺序并不重要:(readInt f) + (readInt f) - (readInt f)
。函数readInt
每次都会获得相同的参数,因此,如果函数是纯函数,则所有三个调用都将获得相同的结果。现在,您不必这样做了,因为它是从外部读取的,然后按执行readInt
调用的顺序变得很重要。
因此,您可以将IO monad视为序列化机制。 monad中的两个操作将始终以相同的顺序进行,因为与外界交谈时顺序很重要!
单子的实际价值是在您开始在单子之外工作时得出的。您的纯程序可以传递在monad中“装箱”的值,然后再取出该值。回顾优化,这可以使优化器在保持monad语义的同时优化纯代码。
答案 2 :(得分:3)
monad的主要目的是减轻使用计算上下文的负担。
以解析为例。在解析中,我们尝试将字符串转换为数据。而且解析器上下文正在将字符串转换为数据。
在解析中,我们可能尝试将字符串读取为整数。如果字符串为“ 123”,我们可能会成功,但是对于字符串“ 3,6”,可能会失败。因此,失败是解析上下文的一部分。解析的另一部分是处理我们正在解析的字符串的当前位置,该位置也包含在“上下文”中。因此,如果我们要解析一个整数,然后是一个逗号,然后解析另一个整数,我们的monad可以帮助我们解析上述“ 3,6”,例如:
intCommaInt = do
i <- intParse
commaParse
j <- intParse
return (i,j)
解析器monad的定义将需要处理正在解析的字符串的某些内部状态,以便第一个intParse将使用“ 3”,并将其余的字符串“,6”传递给其余部分解析器的monad通过允许用户忽略传递未解析的字符串来提供帮助。
要欣赏这一点,请想象编写一些函数,这些函数手动传递经过解析的字符串。
commaParseNaive :: String -> Maybe (String,())
commaParseNaive (',':str) = Just (str,())
commaParseNaive _ = Nothing
intParseNaive :: String -> Maybe (String,Int)
intParseNaive str = ...
请注意:我没有实施intParseNaive
,因为它更复杂,而且
您可以猜测它应该做什么。我使逗号解析返回一个无聊的(),以便两个函数都具有相似的接口,暗示它们可能是同一类型的单子对象。
现在要构成上面的两个朴素的解析器,我们将前一个解析的输出连接到后续解析的输入-如果解析成功。但是我们每次都想解析一件事然后再解析另一件事。 monad实例使用户忘记了这种噪音,而只专注于当前字符串的下一部分。
在许多常见的编程情况下,可以通过一元上下文来模拟程序的具体功能。它是一个一般的概念。知道某物是单子,可以让您知道如何组合单子函数,即在do
块内。但是,正如罗马在他的回答中强调的那样,您仍然需要了解上下文的具体内容。
monad接口有两种方法,return
和(>>=)
。这些决定了上下文。我喜欢用do
表示法来思考,因此我在下面解释了一些其他示例,涉及将纯值return
放入上下文中,并在do a <- (monadicExpression :: m a)
Maybe
:计算失败。
return a
:可靠且始终产生a
a <- m
:m
已运行并成功。Reader r
:可能使用某些“全局”数据r
的计算。
return a
:不需要全局的计算。a <- m
:m
已运行,可能使用了全局方法,并产生了a
State s
:具有内部状态的计算,例如可用于它们的读/写可变变量。
return a
:保持该状态不变的计算。a <- m
:m
已运行,可能使用/修改了状态,并产生了a
IO
:在现实世界中可能进行某些输入/输出交互的计算。
return a
:实际上不会执行IO的IO计算。a <- m
:m
已运行,可能是通过与文件,用户,网络等的交互而产生的,并产生了a
。上面列出的内容,以及语法分析,将使您有效地使用任何monad都有很长的路要走。我也遗漏了一些东西。首先,a <- m
并不是绑定(>> =)的全部故事。举例来说,也许我的解释没有解释在失败的计算中该怎么做-中止链的其余部分。其次,我也无视单子法则,无论如何我都无法解释。但是它们的目的主要是确保return
就像对上下文不执行任何操作,例如IO返回不会发送小信号,State返回不会触摸状态,等等。
编辑。由于我无法很好地内嵌评论的答案,因此我将在此处解决。 commaParse
是类型为commaParse :: MyUndefinedMonadicParserType ()
的虚构解析器组合器的概念示例。我可以通过例如
import Text.Read
commaParse :: ReadPrec ()
commaParse = do
',' <- get
return ()
其中get :: ReadPrec Char
在Text.ParserCombinators.ReadPrec
中定义,并从要分析的字符串中获取下一个字符。我利用ReadPrec
有一个MonadFail
实例的事实,并使用monadic绑定作为对','
的模式匹配。如果绑定的字符不是逗号,则解析的字符串中的下一个字符不是逗号,并且解析失败。
问题的下一部分很重要,因为它强调了Monadic解析器的细微魔术,“它从哪里得到他的输入?”输入是我一直所说的单子上下文的一部分。从某种意义上说,解析器只知道它将存在,并且库提供了访问它的基元。
要详细说明:写原始的intCommaInt = do
块,我的想法是,“在解析的这一点上,我期望一个整数(具有有效整数表示形式的字符串),我将其称为'i' 。接下来是一个逗号(返回()
,不需要将其绑定到变量。)接下来应该是另一个整数。好,解析成功,返回这两个整数。注意,我不需要考虑诸如此类的事情:“获取我正在解析的当前字符串,将其余的字符串传递给它。”无聊的事情由解析器的定义处理。我对上下文的了解是解析器将在解析器的下一部分上工作字符串,不管是什么。
但是,当然,最终需要提供字符串。一种方法是标准的“运行” monad模式:
x = runMonad monadicComputation inputData
在我们的情况下,类似
case readPrec_to_S intCommaInt 0 inputString of
[] -> --failed parse value
((i,j),remainingString):moreParses -> --something using i,j etc.
以上是标准模式,其中monad代表某种类型的需要输入的计算机。但是,特别是对于ReadPrec
,运行是通过标准Read
类型的类并仅调用read "a string to parse"
完成的。
因此,如果我们要通过{p>使(Int,Int)
成为Read
的成员,
class Read (Int,Int) where
readPrec = intCommaInt
然后我们可以调用类似以下的内容,它们都将使用基础的Read
实例。
read "1,1" :: (Int,Int) --Success, now can work with int pairs.
read "a,b" :: (Int,Int) --Fails at runtime
readMaybe "a,b" :: Maybe (Int,Int) -- Returns (Just (1,1))
readMaybe "1,1" :: Maybe (Int,Int) -- Returns Nothing
但是,读取的类已经具有(Int,Int)的实现,因此我们不能直接编写该类。相反,我们可能会定义一个新类型,
newtype IntCommaInt = IntCommaInt (Int,Int)
并根据其定义解析器/ ReadPrec。