monad 的主要目的是减轻使用计算上下文的负担。
以解析为例。在解析中,我们尝试将字符串转换为数据。 Parser 上下文正在将字符串转换为数据。
在解析时,我们可能会尝试将字符串读取为整数。如果字符串是“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 实例让用户忘记那些噪音,只专注于当前字符串的下一部分。
在许多常见的编程情况下,程序所做的细节可以通过一元上下文建模。它是一个一般概念。知道某事是 monad 可以让您知道如何组合 monadic 函数,即在 do 块内。但是你仍然需要知道上下文的细节是什么,正如罗马在他的回答中所强调的那样。
monad 接口有两个方法,return 和(>>=)。这些决定了上下文。我喜欢用 do 表示法来思考,所以我在下面再解释几个例子,将纯值放入上下文中,return,并在 do 块中将其从上下文中取出,@987654329 @
-
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返回不发送missles,State返回不触及状态等
编辑。由于我不能很好地内联评论的答案,我将在这里解决。 commaParse 是一个虚构的解析器组合器的概念示例,类型为 commaParse :: MyUndefinedMonadicParserType ()。我可以通过例如
来实现这个解析器
import Text.Read
commaParse :: ReadPrec ()
commaParse = do
',' <- get
return ()
其中get :: ReadPrec Char 在Text.ParserCombinators.ReadPrec 中定义,并从正在解析的字符串中获取下一个字符。我利用ReadPrec 有一个MonadFail 实例这一事实,并使用一元绑定作为',' 的模式匹配。如果绑定的字符不是逗号,则解析字符串中的下一个字符不是逗号,解析失败。
问题的下一部分很重要,因为它强调了单子解析器的微妙魔力,“它从哪里获得输入?”输入是我所说的一元上下文的一部分。从某种意义上说,解析器只知道它会在那里,并且库提供了访问它的原语。
详细说明:编写原始的intCommaInt = do 块我的想法是,“在解析的这一点上,我期望一个整数(具有有效整数表示的字符串),我称之为'i'。下一个有一个逗号(它返回一个(),不需要将它绑定到一个变量)。接下来应该是另一个整数。好的,解析成功,返回两个整数。注意,我不需要考虑类似的事情。“抓住我正在解析的当前字符串,传递剩余的字符串。”那些无聊的东西由解析器的定义处理。我对上下文的了解是解析器将处理字符串的下一部分,无论如何就是这样。
当然,最终还是需要提供字符串。一种方法是标准的“运行”单子模式:
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"。
所以,如果我们让(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。