常见(但不常见)单子

上周,我们研究了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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值