上周,我们研究了monad如何帮助您实现Haskell开发的下一个跃进。 我们讨论了runXXXT
模式,以及如何使用其余代码中的某些monad作为通用网关。 但是有时它也有助于回到基础知识。 实际上,我花了很长时间才真正掌握如何使用几个基本的monad。 或者至少,我不了解如何将它们用作单子。
在本文中,我们将研究如何使用列表monad和函数monad。 列表和功能是任何Haskeller从一开始就学到的核心概念。 但是列表数据结构和功能应用程序也是单子! 理解它们的工作方式可以使我们更多地了解单子的工作原理。
有关monad的深入讨论,请查看我们的功能数据结构系列 !
执行语法的一般模式
使用do
语法是了解如何实际使用monad的关键之一。 绑定运算符使您很难跟踪参数的位置。 Do语法可使结构保持整洁,并允许您轻松传递结果。 让我们看看它如何与IO
,这是许多Haskellers学习的第一个monad。 这是一个示例,其中我们从文件中读取第二行:
readLineFromFile :: IO String
readLineFromFile = do
handle <- openFile “myFile.txt” ReadMode
nextLine <- hGetLine handle
secondLine <- hGetLine handle
_ <- hClose handle
return secondLine
通过牢记所有IO
函数的类型签名,我们可以开始了解do语法的一般模式。 让我们将每个表达式替换为其类型:
openFile :: FilePath -> IOMode -> IO Handle
hGetLine :: Handle -> IO String
hClose :: Handle -> IO ()
return :: a -> IO a
readLineFromFile :: IO String
readLineFromFile = do
(Handle) <- (IO Handle)
(String) <- (IO String)
(String) <- (IO String)
() <- (IO ())
IO String
do表达式中的每一行(最后一行除外)都使用赋值运算符<-
。 然后,它具有的表达IO a
在右侧,这将其分配给的值a
在左侧。 然后,最后一行的类型与该函数的最终返回值匹配。 现在重要的是要认识到我们可以将这种结构推广到任何monad:
monadicFunction :: mc
monadicFunction = do
(_ :: a) <- (_ :: ma)
(_ :: b) <- (_ :: mb)
(_ :: mc)
因此,例如,如果我们在Maybe
monad中有一个函数,则可以使用它并将其插入上面的m
:
myMaybeFunction :: a -> Maybe a
monadicMaybe :: a -> Maybe a
monadicMaybe x = do
(y :: a) <- myMaybeFunction x
(z :: a) <- myMaybeFunction y
(Just z :: Maybe a)
要记住的重要一点是,monad会捕获计算上下文。 对于IO
,上下文是计算可能与终端或网络交互。 对于Maybe
,上下文是计算可能会失败。
列表单子
现在要绘制列表单子图,我们需要知道其计算上下文。 我们可以将任何返回列表的函数视为不确定的 。 它可以具有许多不同的值。 因此,如果我们链接这些计算,则最终结果就是每个可能的组合 。 也就是说,我们的第一个计算可以返回值列表。 然后,我们想检查一下这些不同结果所得到的结果,作为对下一个函数的输入。 然后,我们将获得所有这些结果。 等等。
看到这个,让我们想象一下我们有一个游戏。 我们可以从特定数字x
开始游戏。 在每一回合中,我们可以减去1,加1或保持数字相同。 我们想知道5转后的所有可能结果以及这些可能性的分布。 因此,我们首先编写非确定性函数。 它需要一个输入并返回可能的游戏输出:
runTurn :: Int -> [Int]
runTurn x = [x - 1, x, x + 1]
这就是我们在这5回合游戏中的应用方式。 我们将添加类型签名,以便您可以看到单子结构:
runGame :: Int -> [Int]
runGame x = do
(m1 :: Int) <- (runTurn x :: [Int])
(m2 :: Int) <- (runTurn m1 :: [Int])
(m3 :: Int) <- (runTurn m2 :: [Int])
(m4 :: Int) <- (runTurn m3 :: [Int])
(m5 :: Int) <- (runTurn m4 :: [Int])
return m5
在右侧,每个表达式都具有[Int]
类型。 然后在左侧,我们将Int
输出。 因此, m
表达式中的每一个表示我们将从runTurn
获得的众多解决方案runTurn
。 然后,我们运行其余功能,假设我们仅使用其中之一。 但实际上,由于list monad如何定义其绑定运算符,我们将全部运行它们。 这种精神上的跳跃有些棘手。 而且,当我们进行列表计算时,只坚持使用where
表达式通常更直观。 但是看到这样的模式突然出现在意外的地方很酷。
功能单子
函数monad是我一段时间以来一直难以理解的另一个函数。 在某些方面,它与Reader
monad相同。 它封装了可以传递给不同函数的单个参数的上下文。 但这与Reader
定义方式不同。 当我尝试使用该定义时,对我而言并没有多大意义:
instance Monad ((->) r) where
return x = \_ -> x
h >>= f = \w -> f (hw) w
return
定义是有意义的。 我们将有一个函数,该函数接受一些参数,忽略该参数,并将值作为输出。 绑定运算符稍微复杂一点。 当我们将两个函数绑定在一起时,我们将获得一个带有一些参数w
的新函数。 我们将该参数应用于第一个函数( (hw)
)。 然后,我们将得出结果,并将其应用于f
,然后再再次应用参数w
。 很难遵循。
但是让我们在do语法的上下文中考虑一下。 右侧的每个表达式都是一个将我们的类型作为唯一参数的函数。
myFunctionMonad :: a -> (x, y, z)
myFunctionMonad = do
x <- :: a -> b
y <- :: a -> c
z <- :: a -> d
return (x, y, z)
现在让我们想象一下,我们将传递一个Int
并使用一些可以接受Int
不同函数。 这是我们将得到的:
myFunctionMonad :: Int -> (Int, Int, String)
myFunctionMonad = do
x <- (1 +)
y <- (2 *)
z <- show
return (x, y, z)
现在我们有了有效的do语法! 那么当我们运行此功能时会发生什么呢? 我们将在同一输入上调用不同的函数。
>> myFunctionMonad 3
(4, 6, "3")
>> myFunctionMonad (-1)
(0, -2, "-1")
当我们在第一个例子中通过3中,我们在第二行上加1它在第一行上,乘以它是2, show
它在第三行上。 而且我们在没有明确说明参数的情况下完成了所有这些工作! 棘手的是,所有函数都必须将输入参数作为最后一个参数。 因此,您可能需要进行一些参数翻转。
结论
在本文中,我们探讨了列表和函数,这是Haskell中最常见的两个概念。 我们通常不将它们用作单子。 但是我们看到了它们仍然如何适应单子结构。 我们可以在do语法中使用它们,并遵循我们已经知道的模式使事情起作用。
也许您曾经尝试过学习Haskell,但是发现monad有点太复杂了。 希望本文有助于阐明monad的结构。 如果您想让自己的Haskell旅程重新开始,请下载我们的初学者清单 ! 或者从头开始学习monad,请阅读我们有关功能数据结构的系列!
From: https://hackernoon.com/common-but-not-so-common-monads-ae7ded7911d2