cib*_*en1 565
然后,我们遇到了第一个大问题.这是一个程序:
f(x) = 2 * x
g(x,y) = x / y
我们怎么说 首先要执行的是什么?我们如何使用不多于函数形成有序的函数序列(即程序)?
解决方案:撰写功能.如果你想先g,然后f,只需写f(g(x,y)).这样,"程序"也是一个功能:main = f(g(x,y)).好的但是 ...
更多问题:某些功能可能会失败(即g(2,0)除以0).我们有没有"例外"在FP(一个例外是不是一个函数).我们如何解决它?
解决方案:让我们允许函数返回两种东西:而不是g : Real,Real -> Real(从两个实数到实数的函数),让我们允许g : Real,Real -> Real | Nothing(从两个实数到(真实或无)的函数).
但是函数应该(更简单)只返回一件事.
解决方案:让我们创建一个要返回的新类型的数据,一个" 拳击类型 ",它可能包含一个真实的或者根本就没有.因此,我们可以拥有g : Real,Real -> Maybe Real.好的但是 ...
现在发生了f(g(x,y))什么?f还没准备好消费Maybe Real.而且,我们不想改变我们可以连接的每个功能g来消费Maybe Real.
解决方案:让我们有一个特殊的功能来"连接"/"撰写"/"链接"功能.这样,我们可以在幕后调整一个函数的输出来提供下一个函数.
在我们的例子: g >>= f(连接/组合g到f).我们想>>=获得g输出,检查它,如果它Nothing只是不打电话f和返回Nothing; 或相反,提取盒装Real并f用它喂食.(这个算法只是>>=针对该Maybe类型的实现).另请注意,每个"拳击类型"(不同的框,不同的自适应算法)>>=必须只写一次.
使用相同的模式可以解决许多其他问题:1.使用"框"来编纂/存储不同的含义/值,并具有类似的函数g返回那些"盒装值".2.有一个作曲家/链接器g >>= f来帮助连接g输出到f输入,所以我们根本不需要改变它f.
使用这种技术可以解决的显着问题是:
具有全局状态,即函数序列中的每个函数("程序")可以共享:解决方案StateMonad.
我们不喜欢"不纯的函数":为同一输入产生不同输出的函数.因此,让我们标记这些函数,使它们返回一个标记/盒装值:monad.IO
总幸福!
Car*_*arl 213
答案当然是"我们没有".与所有抽象一样,没有必要.
Haskell不需要monad抽象.没有必要以纯语言执行IO.这种IO类型本身就可以解决这个问题.现有一元脱糖do块可以与脱糖被替换bindIO,returnIO和failIO作为限定GHC.Base模块.(这不是关于hackage的文档模块,所以我必须指出它的文档来源.)所以不,不需要monad抽象.
所以,如果不需要,它为什么存在?因为发现许多计算模式形成了一元结构.抽象结构允许编写适用于该结构的所有实例的代码.更简洁地说 - 代码重用.
在函数式语言中,代码重用最强大的工具是函数的组合.好老(.) :: (b -> c) -> (a -> b) -> (a -> c)操作员非常强大.它可以轻松编写微小的函数并将它们粘合在一起,只需最少的语法或语义开销.
但是有些情况下类型不能正常运行.你有什么foo :: (b -> Maybe c)和你做什么bar :: (a -> Maybe b)?foo . bar不进行类型检查,因为b和Maybe b是不是同一类型.
但是......这几乎是正确的.你只想要一点余地.你希望能够Maybe b像对待它一样对待b.但是,将它们视为相同的类型是一个糟糕的主意.这与零指针大致相同,Tony Hoare称其为十亿美元的错误.因此,如果您不能将它们视为相同类型,也许您可以找到一种方法来扩展组合机制所(.)提供的.
在这种情况下,重要的是要真正检查潜在的理论(.).幸运的是,有人已经为我们这样做了.事实证明的组合(.)和id形成被称为一个数学结构类别.但是还有其他方法可以形成类别.例如,Kleisli类别允许组合的对象稍微增强.Kleisli类别Maybe包括(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)和id :: a -> Maybe a.也就是说,类别中的对象(->)用a 增加Maybe,因此(a -> b)变为(a -> Maybe b).
突然之间,我们将构图的力量扩展到传统(.)操作无效的事物上.这是新抽象力量的来源.Kleisli类别可以使用更多类型而不仅仅是Maybe.它们适用于可以组装适当类别的每种类型,遵守类别法.
id . f=ff . id=ff . (g . h)=(f . g) . h只要您能证明您的类型符合这三个法则,您就可以将其转换为Kleisli类别.那有什么大不了的?嗯,事实证明monad与Kleisli类别完全相同.Monad的return是相同的Kleisli id.Monad的(>>=)是不相同的Kleisli (.),但它原来是很容易写在每个其它方面.和类别的法律是一样的单子法律,当你之间跨越差异翻译它们(>>=)和(.).
那么为什么要经历这一切呢?为什么要用Monad语言抽象?正如我在上面提到的,它使代码重用成为可能.它甚至可以沿两个不同的维度重用代码.
代码重用的第一个维度直接来自抽象的存在.您可以编写适用于抽象所有实例的代码.整个monad-loops包由循环组成,可以与任何实例一起使用Monad.
第二个维度是间接的,但它来自组合的存在.当组合很容易时,用小的可重用块编写代码是很自然的.这与(.)函数操作符鼓励编写小型可重用函数的方式相同.
那为什么抽象存在呢?因为它被证明是一种工具,可以在代码中实现更多的组合,从而创建可重用的代码并鼓励创建更多可重用的代码.代码重用是编程的圣杯之一.monad抽象之所以存在,是因为它让我们向这个圣杯移动了一点点.
use*_*465 24
本杰明皮尔斯在TAPL说
类型系统可以被视为计算程序中术语的运行时行为的一种静态近似.
这就是为什么配备强大类型系统的语言比表达不良的语言更具表现力.你可以用同样的方式思考monad.
作为@Carl和sigfpe指出,您可以为数据类型配备所需的所有操作,而无需使用monad,类型类或任何其他抽象的东西.但是monad不仅允许您编写可重用的代码,还可以抽象出所有冗余的详细信息.
举个例子,假设我们要过滤一个列表.最简单的方法是使用filter函数:filter (> 3) [1..10],等于[4,5,6,7,8,9,10].
一个稍微复杂的版本filter,也是从左到右传递累加器,是
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
Run Code Online (Sandbox Code Playgroud)
要获得所有i,这样i <= 10, sum [1..i] > 4, sum [1..i] < 25,我们可以写
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
Run Code Online (Sandbox Code Playgroud)
等于[3,4,5,6].
或者我们可以重新定义nub函数,从列表中删除重复的元素filterAccum:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
Run Code Online (Sandbox Code Playgroud)
nub' [1,2,4,5,4,3,1,8,9,4]等于[1,2,4,5,3,8,9].列表在此处作为累加器传递.代码有效,因为它可以保留列表monad,因此整个计算保持纯粹(notElem实际上不使用>>=,但它可以).但是,无法安全地离开IO monad(即,您无法执行IO操作并返回纯值 - 该值始终将包含在IO monad中).另一个例子是可变数组:在你离开ST monad之后,一个可变数组存在,你不能再在恒定时间内更新数组.所以我们需要从Control.Monad模块中进行monadic过滤:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
Run Code Online (Sandbox Code Playgroud)
filterM对列表中的所有元素执行monadic动作,产生monadic动作返回的元素True.
带数组的过滤示例:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
Run Code Online (Sandbox Code Playgroud)
[1,2,4,5,3,8,9]按预期打印.
还有一个带有IO monad的版本,它会询问要返回的元素:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
Run Code Online (Sandbox Code Playgroud)
例如
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
Run Code Online (Sandbox Code Playgroud)
作为最后的说明,filterAccum可以用以下术语来定义filterM:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
Run Code Online (Sandbox Code Playgroud)
使用StateTmonad,在引擎盖下使用,只是一个普通的数据类型.
这个例子说明,monad不仅允许你抽象计算上下文并编写干净的可重用代码(由于monad的可组合性,如@Carl所解释的),而且还可以统一处理用户定义的数据类型和内置基元.
lef*_*out 19
我认为不IO应该被认为是一个特别优秀的单子,但对于初学者来说这肯定是最令人震惊的单子之一,所以我会用它来解释.
对于纯功能语言(实际上是Haskell开始使用的),最简单的可想象的IO系统是这样的:
main? :: String -> String
main? _ = "Hello World"
Run Code Online (Sandbox Code Playgroud)
有了懒惰,这个简单的签名足以实际构建交互式终端程序 - 但非常有限.最令人沮丧的是我们只能输出文字.如果我们添加一些更令人兴奋的输出可能性怎
data Output = TxtOutput String
| Beep Frequency
main? :: String -> [Output]
main? _ = [ TxtOutput "Hello World"
-- , Beep 440 -- for debugging
]
Run Code Online (Sandbox Code Playgroud)
可爱,但当然更现实的"替代输出"将写入文件.但是你也想要一些从文件中读取的方法.任何机会?
好吧,当我们采用我们的main?程序并简单地将文件传递给进程(使用操作系统工具)时,我们基本上实现了文件读取.如果我们可以从Haskell语言中触发该文件读取...
readFile :: Filepath -> (String -> [Output]) -> [Output]
Run Code Online (Sandbox Code Playgroud)
这将使用"交互式程序" String->[Output],向其提供从文件获得的字符串,并产生仅执行给定的非交互式程序.
这里有一个问题:我们实际上没有关于何时读取文件的概念.该[Output]列表肯定给出了一个很好的才能的输出,但我们没有得到当订单输入将完成.
解决方案:使输入事件也成为要执行的事项列表中的项目.
data IO? = TxtOut String
| TxtIn (String -> [Output])
| FileWrite FilePath String
| FileRead FilePath (String -> [Output])
| Beep Double
main? :: String -> [IO?]
main? _ = [ FileRead "/dev/null" $ \_ ->
[TxtOutput "Hello World"]
]
Run Code Online (Sandbox Code Playgroud)
好吧,现在你可能发现了一个不平衡:你可以读取一个文件并使输出依赖它,但你不能使用文件内容来决定例如也读取另一个文件.明显的解决方案:使输入事件的结果也是类型的结果IO,而不仅仅是Output.这确实包括简单的文本输出,但也允许读取其他文件等.
data IO? = TxtOut String
| TxtIn (String -> [IO?])
| FileWrite FilePath String
| FileRead FilePath (String -> [IO?])
| Beep Double
main? :: String -> [IO?]
main? _ = [ TxtIn $ \_ ->
[TxtOut "Hello World"]
]
Run Code Online (Sandbox Code Playgroud)
现在,这实际上允许您在程序中表达您可能想要的任何文件操作(尽管可能没有良好的性能),但它有点过于复杂:
main?产生一个整体名单的行动.为什么我们不简单地使用签名:: IO?,这是一个特例?
这些列表实际上不再给出程序流程的可靠概述:大多数后续计算仅作为某些输入操作的结果被"公布".所以我们不妨放弃列表结构,只需对每个输出操作进行"然后再做".
data IO? = TxtOut String IO?
| TxtIn (String -> IO?)
| Terminate
main? :: IO?
main? = TxtIn $ \_ ->
TxtOut "Hello World"
Terminate
Run Code Online (Sandbox Code Playgroud)
还不错!
实际上,您不希望使用普通构造函数来定义所有程序.需要有一些这样的基本构造函数,但对于大多数更高级的东西,我们想要编写一个具有一些不错的高级签名的函数.事实证明,大多数这些看起来非常相似:接受某种有意义类型的值,并产生IO动作作为结果.
getTime :: (UTCTime -> IO?) -> IO?
randomRIO :: Random r => (r,r) -> (r -> IO?) -> IO?
findFile :: RegEx -> (Maybe FilePath -> IO?) -> IO?
Run Code Online (Sandbox Code Playgroud)
这里显然有一种模式,我们最好把它写成
type IO? a = (a -> IO?) -> IO? -- If this reminds you of continuation-passing
-- style, you're right.
getTime :: IO? UTCTime
randomRIO :: Random r => (r,r) -> IO? r
findFile :: RegEx -> IO? (Maybe FilePath)
Run Code Online (Sandbox Code Playgroud)
现在开始看起来很熟悉了,但我们仍然只是处理欺骗性的简单功能,而且风险很大:每个"价值行动"都有责任实际传递任何包含函数的结果动作(否则整个程序的控制流很容易被中间的一个不良行为打乱.我们最好明确要求这个要求.好吧,事实证明这些是monad定律,但我不确定如果没有标准的绑定/连接运算符,我们可以真正制定它们.
无论如何,我们现在已经达到了一个具有正确monad实例的IO公式:
data IO? a = TxtOut String (IO? a)
| TxtIn (String -> IO? a)
| TerminateWith a
txtOut :: String -> IO? ()
txtOut s = TxtOut s $ TerminateWith ()
txtIn :: IO? String
txtIn = TxtIn $ TerminateWith
instance Functor IO? where
fmap f (TerminateWith a) = TerminateWith $ f a
fmap f (TxtIn g) = TxtIn $ fmap f . g
fmap f (TxtOut s c) = TxtOut s $ fmap f c
instance Applicative IO? where
pure = TerminateWith
(<*>) = ap
instance Monad IO? where
TerminateWith x >>= f = f x
TxtOut s c >>= f = TxtOut s $ c >>= f
TxtIn g >>= f = TxtIn $ (>>=f) . g
Run Code Online (Sandbox Code Playgroud)
显然,这不是IO的有效实现,但它原则上是可用的.
Monads 基本上用于将功能组合在一个链中。时期。
现在它们的组成方式在现有的 monad 中有所不同,从而导致不同的行为(例如,在状态 monad 中模拟可变状态)。
关于 monad 的困惑在于,它是如此通用,即组合函数的机制,它们可以用于很多事情,从而导致人们认为 monad 是关于状态,关于 IO 等,当它们只是关于“组合函数” ”。
现在,关于 monad 的一件有趣的事情是,组合的结果始终是“M a”类型,即标有“M”的信封内的值。这个特性恰好可以很好地实现,例如,将纯代码与不纯代码明确分离:将所有不纯操作声明为“IO a”类型的函数,并且在定义 IO monad 时不提供任何函数来取出“ a" 来自“IO a”内部的值。结果是没有一个函数可以是纯函数同时从“IO a”中取出一个值,因为没有办法在保持纯净的同时取出这样的值(函数必须在“IO”monad内部才能使用这样的价值)。(注意:嗯,没有什么是完美的,因此可以使用“unsafePerformIO : IO a -> a”打破“IO 紧身衣”
如果您有类型构造函数和返回该类型系列值的函数,则需要 monad 。最终,您希望将这些类型的功能组合在一起。这是回答原因的三个关键要素。
让我详细说明一下。您有Int、String和Real类型的函数Int -> String,String -> Real等等。您可以轻松组合这些功能,最后以Int -> Real. 生活很好。
然后,有一天,您需要创建一个新的类型系列。可能是因为您需要考虑返回无值(Maybe)、返回错误(Either)、返回多个结果( )的可能性List ) 等的可能性。
请注意,这Maybe是一个类型构造函数。它需要一个类型,就像Int并返回一个新类型Maybe Int。首先要记住,没有类型构造函数,没有 monad。
当然,您想在代码中使用类型构造函数,很快您就会以Int -> Maybe String和 等函数结束String -> Maybe Float函数结束。现在,您无法轻松组合功能。生活不再美好了。
这时候单子就可以派上用场了。它们允许您再次组合此类功能。你只需要改变构图。对于>==。
Monads只是一个方便的框架,用于解决一类重复出现的问题。首先,monad 必须是函子(即必须支持映射而不查看元素(或其类型)),它们还必须带来绑定(或链接)操作以及从元素类型 ( return)创建 monadic 值的方法。最后,bind并且return必须满足两个方程(左恒等式和右恒等式),也称为单子定律。(或者,可以将 monad 定义为具有flattening operation而不是绑定。)
该列表单子是通常用来对付非决定。绑定操作选择列表中的一个元素(直觉上所有元素都在并行世界中),让程序员对它们进行一些计算,然后将所有世界中的结果合并到单个列表中(通过连接或展平嵌套列表)。下面是如何在 Haskell 的 monadic 框架中定义一个置换函数:
perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
let shortened = take index l ++ drop (index + 1) l
trailer <- perm shortened
return (leader : trailer)
Run Code Online (Sandbox Code Playgroud)
这是一个示例repl会话:
*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]
Run Code Online (Sandbox Code Playgroud)
应该注意的是,列表 monad 绝不是一种副作用计算。数学结构是 monad(即符合上述接口和定律)并不意味着副作用,尽管副作用现象通常很好地适合于 monadic 框架。
| 归档时间: |
|
| 查看次数: |
49216 次 |
| 最近记录: |