Monads(Haskell)的主要目的

时间:2018-06-28 09:21:19

标签: haskell monads

从我的阅读中我了解到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

那么这种类型差异的好处是什么?是错误处理吗?

3 个答案:

答案 0 :(得分:17)

将Monads视为 things (名词)是混乱的根源。 Monad更像形容词。您不会问“蓝色”或“薄”有什么用。您会发现一些有用的东西,例如一本蓝皮书或一支细笔,然后注意到一些图案-有些东西是蓝色的,有些是细的,有些都不是。

与单子相似。要了解单子,您首先应该对单子的事物有一些经验:MaybeEitherReaderState。了解它们的工作原理,>>=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 <- mm已运行并成功。
  • Reader r:可能使用某些“全局”数据r的计算。
    • return a:不需要全局的计算。
    • a <- mm已运行,可能使用了全局方法,并产生了a
  • State s:具有内部状态的计算,例如可用于它们的读/写可变变量。
    • return a:保持该状态不变的计算。
    • a <- mm已运行,可能使用/修改了状态,并产生了a
  • IO:在现实世界中可能进行某些输入/输出交互的计算。
    • return a:实际上不会执行IO的IO计算。
    • a <- mm已运行,可能是通过与文件,用户,网络等的交互而产生的,并产生了a

上面列出的内容,以及语法分析,将使您有效地使用任何monad都有很长的路要走。我也遗漏了一些东西。首先,a <- m并不是绑定(>> =)的全部故事。举例来说,也许我的解释没有解释在失败的计算中该怎么做-中止链的其余部分。其次,我也无视单子法则,无论如何我都无法解释。但是它们的目的主要是确保return就像对上下文不执行任何操作,例如IO返回不会发送小信号,State返回不会触摸状态,等等。

编辑。由于我无法很好地内嵌评论的答案,因此我将在此处解决。 commaParse是类型为commaParse :: MyUndefinedMonadicParserType ()的虚构解析器组合器的概念示例。我可以通过例如

来实现此解析器
import Text.Read

commaParse :: ReadPrec ()
commaParse = do
  ',' <- get
  return ()

其中get :: ReadPrec CharText.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。