Answers:
然后,我们有第一个大问题。这是一个程序:
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
。
使用此技术可以解决的显着问题是:
具有全局状态,函数序列中的每个函数(“程序”)可以共享:solution StateMonad
。
我们不喜欢“不纯函数”:对于相同输入产生不同输出的函数。因此,让我们标记这些函数,使它们返回标记/装箱的值:monad。IO
完全幸福!
IO
monad只是列表中的另一个问题IO
(第7点)。另一方面,IO
它只出现一次并在末尾出现,因此,不理解您的“大部分时间都在谈论...关于IO”。
Either
)。答案最多的是“为什么我们需要函子?”。
g >>= f
可以帮助将g
'的输出连接到f
'的输入,因此我们根本不需要进行任何更改f
。” 这根本不对。之前,在f(g(x,y))
,f
可以生产任何东西。可能是f:: Real -> String
。对于“单声道成分” ,必须将其更改为production Maybe String
,否则类型将不合适。而且,>>=
它本身不合适!这是>=>
做这个成分,不>>=
。请参阅卡尔的回答与dfeuer的讨论。
答案当然是“我们不”。与所有抽象一样,这不是必需的。
Haskell不需要monad抽象。无需使用纯语言执行IO。该IO
类型本身可以很好地解决这一问题。现有一元脱糖do
块可以与脱糖被替换bindIO
,returnIO
和failIO
作为限定GHC.Base
模块。(它不是关于黑客的文档化模块,因此,我必须指出其文档来源。)因此,不需要,不需要monad抽象。
因此,如果不需要它,为什么它存在?因为发现许多计算模式形成单子结构。结构的抽象允许编写可在该结构的所有实例中工作的代码。简而言之-代码重用。
在功能语言中,发现用于代码重用的最强大的工具就是功能的组合。好的老(.) :: (b -> c) -> (a -> b) -> (a -> c)
操作员功能强大。它使编写微小的函数和将它们粘合在一起变得容易,并且语法或语义开销最小。
但是在某些情况下,这些类型无法正确解决。当你有你做什么foo :: (b -> Maybe c)
和bar :: (a -> Maybe b)
?foo . bar
不进行类型检查,因为b
和Maybe b
类型不相同。
但是...几乎是正确的。您只需要一点余地。您希望能够将其Maybe b
视为基本b
。但是,仅仅将它们视为相同类型是一个糟糕的主意。这几乎与null指针相同,Tony Hoare曾将其称为十亿美元的错误。因此,如果您不能将它们视为同一类型,也许您可以找到一种方法来扩展(.)
提供的合成机制。
在这种情况下,真正检查基础理论很重要(.)
。幸运的是,已经有人为我们做到了。事实证明的组合(.)
和id
形成被称为一个数学结构类别。但是还有其他形成类别的方法。例如,Kleisli类别允许对组成的对象进行一些扩充。的Kleisli类别Maybe
将由(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
和组成id :: a -> Maybe a
。也就是说,类别中的对象用扩展(->)
了Maybe
,因此(a -> b)
变为(a -> Maybe b)
。
突然之间,我们将合成的功能扩展到了传统(.)
操作无法完成的事情。这是新的抽象能力的来源。Kleisli类别不仅可以使用更多类型,还可以使用更多类型Maybe
。它们适用于可以组合适当类别的每种类型,并遵守类别法则。
id . f
=f
f . id
=f
f . (g . h)
=(f . g) . h
只要您可以证明自己的类型符合这三个定律,就可以将其转换为Kleisli类别。那有什么大不了的?好吧,事实证明,单子与Kleisli类别完全相同。Monad
的return
是相同的Kleisli id
。Monad
的(>>=)
是不相同的Kleisli (.)
,但它原来是很容易写在每个其它方面。当您跨(>>=)
和之间的区别翻译类别法则时,类别法则与单子法则相同(.)
。
那么,为什么要经历所有这些麻烦呢?为什么要Monad
对语言进行抽象?正如我在上面提到的,它可以实现代码重用。它甚至可以实现两个不同维度上的代码重用。
代码重用的第一个维度直接来自抽象的存在。您可以编写适用于所有抽象实例的代码。整个monad-loops程序包均包含可与的任何实例一起使用的循环Monad
。
第二个维度是间接的,但是它来自于组合的存在。当编写起来很容易时,很自然地以小的,可重用的块编写代码。这与(.)
使函数的运算符鼓励编写小的可重用函数的方式相同。
那么为什么存在抽象呢?因为它被证明是一种可以在代码中实现更多组合的工具,所以可以创建可重用的代码并鼓励创建更多可重用的代码。代码重用是编程的圣杯之一。之所以存在monad抽象,是因为它使我们稍微朝着那个圣杯前进。
newtype Kleisli m a b = Kleisli (a -> m b)
。Kleisli类别是函数,其中归类返回类型(b
在这种情况下)是类型构造函数的参数m
。Iff Kleisli m
构成一个类别,m
是Monad。
Kleisli m
似乎形成了一个类别,其对象是Haskell类型,并且从a
到的箭头b
是从a
到m b
,带有id = return
和的函数(.) = (<=<)
。那是对的吗,还是我混淆了不同层次的东西?
a
和之间b
,但是它们不是简单的函数。它们在m
函数的返回值中附加了一个修饰符。
本杰明·皮尔斯在TAPL中说
类型系统可以看作是一种对程序中各项的运行时行为的静态近似计算。
这就是为什么配备功能强大的类型系统的语言要比类型不好的语言严格地表现力强的原因。您可以以相同的方式考虑monad。
正如@Carl和sigfpe所指出的那样,您可以为数据类型配备所需的所有操作,而无需求助于monad,typeclass或任何其他抽象的东西。但是,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]
为了得到所有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]
等于[3,4,5,6]
。
或者我们可以重新定义nub
从列表中删除重复元素的函数filterAccum
:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
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
模块中进行单项过滤:
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)
filterM
对列表中的所有元素执行单子动作,产生单子动作为其返回的元素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]
[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
例如
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
作为最后的例证,filterAccum
可以定义为filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
在StateT
幕后使用的monad只是普通的数据类型。
此示例说明,monad不仅允许您抽象计算上下文并编写干净的可重用代码(由于monad的可组合性,如@Carl所述),而且还可以统一地处理用户定义的数据类型和内置基元。
我认为IO
不应将其视为特别出色的单子,但对于初学者来说,它肯定是更令人惊讶的单子,因此我将用它来解释。
对于纯功能语言(实际上是Haskell最初使用的一种语言),最简单的可能的IO系统是这样的:
main₀ :: String -> String
main₀ _ = "Hello World"
由于懒惰,这个简单的签名足以实际构建交互式终端程序,但是非常有限。最令人沮丧的是,我们只能输出文本。如果我们增加了更多令人兴奋的输出可能性怎么办?
data Output = TxtOutput String
| Beep Frequency
main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
-- , Beep 440 -- for debugging
]
很可爱,但当然要写一个文件,更现实的“替代输出” 。但是然后,您还需要某种方式来读取文件。任何机会?
好吧,当我们采用main₁
程序并简单地将文件通过管道传送到进程(使用操作系统工具)时,我们实际上已经实现了文件读取。如果我们可以从Haskell语言中触发该文件读取...
readFile :: Filepath -> (String -> [Output]) -> [Output]
这将使用“交互式程序” 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"]
]
好的,现在您可能会发现不平衡:您可以读取文件并使输出依赖于该文件,但是您不能使用文件内容来决定例如也读取另一个文件。明显的解决方案:使输入事件的结果也成为某种类型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"]
]
现在,这实际上使您可以表达程序中可能需要的任何文件操作(尽管可能性能不佳),但是它有些复杂:
main₃
产生完整的动作列表。我们为什么不简单地使用签名:: IO₁
(作为特殊情况)呢?
这些列表实际上不再提供可靠的程序流程概览:大多数后续计算仅会由于某些输入操作而被“宣布”。因此,我们不妨放弃列表结构,仅对每个输出操作进行“先做然后做”。
data IO₂ = TxtOut String IO₂
| TxtIn (String -> IO₂)
| Terminate
main₄ :: IO₂
main₄ = TxtIn $ \_ ->
TxtOut "Hello World"
Terminate
还不错!
实际上,您不想使用简单的构造函数来定义所有程序。最好要有几个这样的基本构造函数,但是对于大多数更高层次的东西,我们想编写一个带有一些不错的高层签名的函数。事实证明,其中的大多数看上去都非常相似:接受某种有意义类型的值,并产生IO操作作为结果。
getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂
显然这里有一个模式,我们最好写成
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)
现在开始看起来很熟悉,但是我们仍然只处理底层隐藏的简单函数,这很冒险:每个“值动作”都有责任实际传递任何包含函数的结果动作(否则中间的一个不良行为很容易破坏整个程序的控制流)。我们最好明确要求。好吧,事实证明这些就是单子法则,尽管我不确定我们是否可以在没有标准绑定/联接运算符的情况下真正制定它们。
无论如何,我们现在已经得出了具有适当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
显然,这不是IO的有效实现,但原则上是可用的。
IO3 a ≡ Cont IO2 a
。但我的意思是,此评论更多地向那些已经知道延续单子的人致敬,因为它并不以初学者友好而闻名。
Monad只是解决一类重复出现问题的便捷框架。首先,单子必须是函子(即,必须在不查看元素(或其类型)的情况下支持映射),它们还必须进行绑定(或链接)操作以及从元素类型(return
)创建单子值的方法。最后,bind
并且return
必须满足两个方程(左右恒等式),也称为单子定律。(或者,可以将monad定义为具有flattening operation
而不是绑定。)
该列表单子是通常用来对付非决定。绑定操作选择列表中的一个元素(直观上所有元素都在并行世界中),让程序员对其进行一些计算,然后将所有世界中的结果组合到单个列表中(通过串联或展平嵌套列表) )。这是在Haskell的单子框架中定义置换函数的方式:
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)
这是一个示例repl会话:
*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]
应该注意的是,列表单子绝不影响计算。尽管单反现象通常很好地适合于单子框架,但数学结构为单子(即符合上述接口和定律)并不意味着有副作用。
Monad基本上起着将功能组合在一起的作用。期。
现在,它们的构成方式在现有的monad中有所不同,从而导致不同的行为(例如,模拟状态monad中的可变状态)。
关于monad的困惑在于它是如此的笼统,即一种功能组合的机制,它们可以用于很多事情,因此使人们相信monad仅仅涉及“组成函数”时是关于状态,关于IO等的。 ”。
现在,有关monads的一件有趣的事情是,合成的结果始终是“ M a”类型,即信封中标有“ M”的值。恰好可以很好地实现此功能,例如,将纯代码与不纯代码之间清楚地分开:将所有不纯行为声明为“ IO a”类型的函数,并且在定义IO monad时不提供任何函数来取出“ “ IO a”内部的“ a”值。结果是没有函数可以是纯函数,而不能同时从“ IO a”中取出一个值,因为在保持纯函数的同时无法获取该值(函数必须在“ IO” monad中才能使用)这样的值)。(注意:嗯,没有什么是完美的,因此可以使用“ unsafePerformIO:IO a-> a”来破坏“ IO straitjacket”
如果您有类型构造函数和返回该类型族值的函数,则需要monad 。最终,您希望将这些功能组合在一起。这是回答为什么的三个关键要素。
让我详细说明。你有Int
,String
并Real
和类型的功能Int -> String
,String -> Real
等等。您可以轻松地组合这些功能,以结尾Int -> Real
。生活很好。
然后,有一天,您需要创建一个新的类型家族。可能是因为您需要考虑不返回任何值(Maybe
),返回错误(Either
),多个结果(List
)等的可能性。
注意,这Maybe
是一个类型构造函数。它需要一个类型,例如,Int
然后返回一个新类型Maybe Int
。首先要记住的是,没有类型构造函数,也没有monad。
当然,您希望在代码中使用类型构造函数,很快您将以Int -> Maybe String
和结束函数String -> Maybe Float
。现在,您无法轻松地组合功能。生活不再美好。
这是单子救援的时候。它们使您可以再次组合这种功能。您只需要更改组成即可。对于> ==。