单子(Monad)的主要目的
单子(Monad)的主要目的
根据我的阅读,我了解到Monad主要用于以下方面:\n-函数组合\n通过将一个函数的输出类型与另一个函数的输入类型相匹配。\n我认为这是一篇非常好的文章:\nhttp://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html\n它用盒子/包装器的概念解释了Monad。但是我不明白这些包装器的用途是什么?除了组合之外,包装器还有什么好处?\n另外,IO Monad是一个常见的例子。\nname <- getLine -- name的类型是String,getLine的类型是IO String
\n那么这种类型差异有什么好处呢?是用于错误处理吗?
Monads的主要目的是减轻在计算上下文中工作的负担。以解析为例。在解析中,我们试图将字符串转换为数据。而解析器上下文是将字符串转换为数据。
在解析中,我们可以尝试将一个字符串读取为整数。如果字符串是“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,因为它更复杂,你可以猜到它应该做什么。我让逗号解析返回一个无聊的(),以便两个函数具有相似的接口,暗示它们可能是相同类型的monadic事物。
现在,要组合上面的两个naive解析器,我们将前一个解析的输出连接到后一个解析的输入,如果解析成功。但每次我们想要解析一个东西然后解析另一个东西时,都需要这样做。monad实例允许用户忘记这个琐碎的事情,只需专注于当前字符串的下一部分。
有很多常见的编程情况可以通过monadic上下文来建模。这是一个通用的概念。知道某个东西是一个monad让你知道如何组合monadic函数,即在do块中。但你仍然需要知道上下文的具体细节,正如Roman在他的回答中强调的那样。
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并不是bind (>>=)的全部内容。例如,对于我的maybe解释,并没有解释在计算失败时应该做什么-中止整个链。其次,我还忽略了monad定律,我无法解释它们。但它们的目的主要是确保return就像对上下文什么都没做一样,例如IO return不会发送导弹,State return不会触碰状态等等。
编辑。由于我无法在评论中很好地嵌入答案,所以我在这里回答这个问题。commaParse是一个假设的用于虚构解析器组合器的例子,类型为commaParse :: MyUndefinedMonadicParserType ()。我可以通过以下方式实现这个解析器:
import Text.Read
commaParse :: ReadPrec ()
commaParse = do
',' <- get
return ()
其中,get :: ReadPrec Char在Text.ParserCombinators.ReadPrec中定义,并从正在解析的字符串中获取下一个字符。我利用了ReadPrec具有MonadFail实例的事实,并将monadic绑定用作对“,”的模式匹配。如果绑定的字符不是逗号,则解析失败,因为解析字符串的下一个字符不是逗号。
问题的下一部分很重要,因为它强调了monadic解析器的微妙之处:“它从哪里获取输入?”输入是我一直提到的monadic上下文的一部分。从某种意义上说,解析器只是知道它将在那里,并且库提供了访问它的原始操作。
具体地说,在编写原始的intCommaInt = do块时,我的思考方式大致是:“在解析的这一点上,我期望一个整数(一个具有有效整数表示的字符串),我将其称为'i'。接下来是一个逗号(它返回一个 (),不需要将其绑定到变量)。接下来应该是另一个整数。好的,解析成功,返回这两个整数。请注意,我不需要考虑这样的事情:“获取我正在解析的当前字符串,将其传递给剩余的字符串。”这些无聊的事情由解析器的定义处理。我的上下文知识是解析器将在字符串的下一部分上工作,无论那是什么。
但是当然,字符串最终需要提供。一种方法是标准的“运行”monad模式:
x = runMonad monadicComputation inputData
在我们的例子中,大致如下:
case readPrec_to_S intCommaInt 0 inputString of
[] -> --失败的解析值
((i,j),remainingString):moreParses -> --使用i,j等的其他东西
上述是一种标准模式,其中monad表示需要输入的某种计算机。然而,对于特定的ReadPrec,运行是通过标准的Read类型类完成的,只需调用read "a string to parse"即可。
因此,如果我们将(Int,Int)作为Read的成员,并定义如下:
class Read (Int,Int) where
readPrec = intCommaInt
然后我们可以调用类似以下的代码,它们都使用底层的Read实例:
read "1,1" :: (Int,Int) --成功,现在可以使用整数对工作。
read "a,b" :: (Int,Int) --运行时失败
readMaybe "a,b" :: Maybe (Int,Int) -- 返回 (Just (1,1))
readMaybe "1,1" :: Maybe (Int,Int) -- 返回 Nothing
但是,Read类已经为(Int,Int)实现了,所以我们不能直接编写该类。相反,我们可能定义一个新类型:
newtype IntCommaInt = IntCommaInt (Int,Int)
并根据它定义我们的解析器/ReadPrec。
问题的出现的原因是I/O操作是不纯的,例如读取文件的内容,在不同的时间点可以得到不同的结果。读取当前系统时间每次都会得到不同的结果。生成随机数每次调用都会得到不同的结果。这些操作显然依赖于除了它们的函数参数之外的东西,即某种状态。在IO monad的情况下,这种状态甚至存在于Haskell程序之外!
可以将monad视为函数调用的“额外参数”。因此,IO monad中的每个函数还会获得一个“包含”程序外部整个世界的参数。
你可能会想知道为什么这很重要。一个原因是优化可以改变程序执行的顺序,只要语义保持不变。计算表达式1 + 4 - 5
,无论你先做加法还是减法都没有关系。但是如果每个值都是文件中的行,那么按顺序读取它们就很重要:(readInt f) + (readInt f) - (readInt f)
。函数readInt
每次都得到相同的参数,所以如果它是纯的,你将从这三个调用中得到相同的结果。现在不是这样,因为它从外部世界中读取,所以执行readInt
调用的顺序变得重要起来。
因此,可以将IO monad视为一种序列化机制。在monad中的两个操作将始终按照相同的顺序执行,因为在与外部世界交互时顺序变得重要!
当你开始在monad之外工作时,monad的实际价值才会显现出来。你的纯程序可以传递“封装”在monad中的值,并在以后将值提取出来。回顾优化,这使得优化器可以优化纯代码同时保持monad的语义意义。
在你的例子1 + 4 - 5中,为什么值是文件中的行与否很重要呢?计算应该是相同的,不是吗?
想象一下:(readInt f) + (readInt f) - (readInt f)
。函数readInt
每次都得到相同的参数,所以如果它是纯净的,你会在这三个调用中得到相同的结果。现在不是这样,因为它从外部世界中读取,所以执行readInt
调用的顺序变得重要起来。
是的,但是f每次可能是不同的值吗?我的意思是我可以从文件中读取三个整数a、b、c然后做a + b - c。
如果你确切地按照我的例子来做,而且f
每次都是相同的文件句柄,你会期望在所有三个调用中得到相同的值。
理解纯度可能会有所帮助,如果你考虑方法和函数之间的区别。函数是从输入到输出的映射;给定相同的输入,函数将始终产生相同的输出。由于从文件中读取需要IO monad(涉及副作用),来自f的调用可能不会产生相同的结果,因为文件可能在调用之间发生了更改。
谢谢,现在更清楚了...但是你说文件可能发生变化,如果文件发生了变化,那么输入就不是相同的了,从逻辑上讲函数应该返回另一个值/结果。我漏掉了什么?
这就是重点!由于外部状态,你的函数突然返回了另一个值!你可以将monad视为函数的一个参数,在这种情况下,你没有漏掉任何东西。
IO Monad是纯的。纯函数在相同的参数下总是给出相同的结果。在IO monad的情况下,额外的参数是整个世界的状态。如果你有一个IO monad,并且能够给出完全相同的整个世界状态,那么结果应该是相同的。
Monad的主要目的是什么(Haskell)?
Monad的主要目的是为了解决一种混淆的源。Monad更像是形容词而不是名词。你不会问“蓝色”或“瘦”有什么用。你会找到一些有用的东西,比如一本蓝色的书或一支细的笔,然后你会注意到一些模式——有些东西是蓝色的,有些是细的,有些既不是蓝色也不是细的。
同样,对于Monad来说也是一样的。为了理解Monad,你应该首先获得一些有关Monad的实例的经验:Maybe、Either、Reader、State。了解它们的工作原理,了解>>=
和return
对它们的作用以及它们的用途,但也了解如何在不使用Monad类的情况下与这些类型一起工作(因此不要从IO开始)。然后你将会注意到这些不同类型之间的共同点,并理解它们为什么遵循一个被称为Monad的共同模式。
Monad只是一种对不同类型的有用接口,但在熟悉这些类型之前,你无法欣赏它,就像如果你从来没有见过任何蓝色的东西,你无法欣赏“蓝色”这个词一样。