Monads(Haskell)的主要目的[重复]
Posted
技术标签:
【中文标题】Monads(Haskell)的主要目的[重复]【英文标题】:The main purpose of Monads (Haskell) [duplicate] 【发布时间】:2018-12-07 07:35:34 【问题描述】:根据我的阅读,我了解到 Monad 主要用于:
-功能组合 通过将一个函数输出类型匹配到另一个函数输入类型。
我认为这是一篇非常好的文章:
http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
它用盒子/包装器的概念来解释 Monad。但我不明白这些包装器是做什么用的? Wrapper 除了Composition之外还有什么好处?
IO Monad 也是一个常见的例子。
name <- getLine -- name has the type String and getLine IO String
那么这种类型差异有什么好处呢?是错误处理吗?
【问题讨论】:
好处是可以嵌入副作用。 【参考方案1】:将单子视为事物(名词)会造成混淆。单子更像是形容词。你不会问“蓝色”或“瘦”有什么用。您会找到一些有用的东西,例如一本蓝皮书或一支细笔,然后您会注意到一些图案 - 有些东西是蓝色的,有些很薄,有些都不是。
与单子类似。要了解 monad,您应该首先对 是 monad 的事物有所了解:Maybe
、Either
、Reader
、State
。了解它们的工作原理、>>=
和 return
为它们做什么以及它们如何有用,以及如何在不使用 Monad 类的情况下使用这些类型。 (因此,不要从 IO 开始。)然后您将准备好注意到这些不同类型之间的共同点,并理解为什么它们遵循称为 Monad 的通用模式。
Monad 只是一个适用于各种类型的有用界面,但除非您熟悉类型本身,否则您无法欣赏它,就像您从未见过任何蓝色就无法欣赏“蓝色”这个词东西。
【讨论】:
【参考方案2】: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 的语义。
【讨论】:
在您的示例 1 + 4 - 5 中,为什么值是否是文件中的行很重要?计算是一样的,不是吗? 想象一下:(readInt f) + (readInt f) - (readInt f)
。函数readInt
每次都获取相同的参数,因此如果它是纯函数,您将从所有三个调用中得到相同的结果。现在你不这样做了,因为它从外部世界读取,然后执行readInt
调用的顺序变得很重要。
是的,但 f 每次都可能是不同的值?我的意思是我可以从一个文件中读取三个整数 a,b,c 然后执行 a + b - c
如果您考虑方法和函数之间的区别,这可能有助于理解纯度。函数是从输入到输出的映射;给定相同的输入,函数将始终产生相同的输出。由于从文件读取需要 IO monad(输入副作用),因此来自 f 的调用可能不会产生相同的结果,因为文件可能在调用之间发生了变化。 @simplesystems
IO Monad 是纯粹的。纯函数总是用相同的参数给出相同的结果。在 IO monad 的情况下,额外的参数是整个世界的状态。如果你有一个 IO monad,并且你能够给出完全相同的整个世界状态,那么结果应该是一样的。【参考方案3】:
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。
【讨论】:
首先感谢您的精彩解释!在您的第一个示例中,函数 commaParse 是如何工作的?它从哪里得到他的意见?它用它做什么?以上是关于Monads(Haskell)的主要目的[重复]的主要内容,如果未能解决你的问题,请参考以下文章