什么是Haskell中的Combinator
-
21-12-2019 - |
题
在 现实世界哈斯克尔, ,他们这样描述组合器:
在Haskell中,我们引用将其他函数作为参数并返回新函数作为组合器的函数。
后来他们说 maybeIO
函数是一个组合器,它的类型签名如下所示:
maybeIO :: IO a -> IO (Maybe a)
但我只能看到 maybeIO
是一个函数,它接受一个包装在IO monad中的值,并在IO monad中返回一个值。那么这个函数如何成为一个combinator呢?
解决方案
当我们说combinator时,我们真的可以说两件事。这个词有点超载。
我们通常指的是"结合"事物的函数。例如,你的函数需要一个
IO
值并建立更复杂的值。使用这些"组合器",我们可以组合并创建新的复合体IO
要创建的基本函数相对较少的值IO
价值观。例如,我们不是创建一个读取10个文件的函数,而是使用
mapM_ readFile
.这里的组合器是我们用来组合和构建值的函数更严格的计算机科学术语是"没有自由变量的函数"。所以
-- The primitive combinators from a famous calculus, SKI calculus. id a = a -- Not technically primitive, genApp const const const a b = a genApp x y z = x z (y z)
这是一个名为"组合逻辑"的更大领域的一部分,在这个领域中,你试图从本质上消除自由变量,并用组合器和一些原始函数替换它。
TLDR:通常当我们说combinator时,我们指的是一个更一般的概念,称为"combinator模式",我们有少量的原始函数和许多用户定义的函数来构建更复杂的值。
其他提示
Combinator没有严格的定义,所以在这个意义上它并不意味着什么。然而,在Haskell中,用更简单的函数或值构建更复杂的函数或值是很常见的,有时函数不能完全组合在一起,所以我们使用一些胶水使它们粘在一起。我们用来做的胶水位,我们称之为组合器。
例如,如果要计算四舍五入为最接近整数的数字的平方根,则可以将该函数写为
approxSqrt x = round (sqrt x)
您可能还会意识到,我们在这里真正做的是使用两个函数并使用它们作为构建块构建更复杂的函数。然而,我们需要某种胶水将它们放在一起,而这种胶水是 (.)
:
approxSqrt = round . sqrt
所以函数组合运算符是函数的组合器-它组合函数以创建新函数。另一个例子是,也许你想读一个文件的每一行到一个列表中。你可以用显而易见的方式做到这一点:
do
contents <- readFile "my_file.txt"
let messages = lines contents
...
但是!如果我们有一个读取文件并将内容作为字符串返回的函数,我们会怎么做?那我们就可以
do
messages <- readFileLines "my_file.txt"
...
事实证明,我们有一个函数读取一个文件,我们有一个函数接受一个大字符串并返回其中的行列表。如果我们只有一些胶水以有意义的方式将这两个功能粘在一起,我们就可以构建 readFileLines
!但当然,这是Haskell,胶水是现成的。
readFileLines = fmap lines . readFile
在这里,我们使用两个组合器!我们使用 (.)
从以前开始, fmap
实际上也是一个非常有用的组合器。我们说它将一个纯粹的计算"提升"到IO monad中,我们真正的意思是 lines
具有类型签名
lines :: String -> [String]
但是 fmap lines
有签名吗?
fmap lines :: IO String -> IO [String]
所以 fmap
当您想要将纯计算与IO计算相结合时,非常有用。
这只是两个非常简单的例子。随着你对Haskell的了解越来越多,你会发现自己需要(并发明)越来越多的combinator函数。Haskell的功能非常强大,你可以把函数转换成函数,把它们组合起来,把它们翻过来,然后把它们粘在一起。当我们这样做时,我们有时需要的胶水位,我们称之为组合器的那些位。
"Combinator"在Haskell中的使用中并不完全精确定义。用它来引用将其他函数作为参数的函数是最正确的 Combinator微积分 但在Haskell术语中,它经常被重载,也意味着"修改"或"组合"函数,特别是 Functor
或 Monad
.在这种情况下,你可能会说一个组合器是一个函数,它"在上下文中采取一些动作或值,并在上下文中返回一个新的,修改过的动作或值"。
你的例子, maybeIO
通常被称为 optional
optional :: Alternative f => f a -> f (Maybe a)
optional fa = (Just <$> fa) <|> pure Nothing
它具有类似组合器的性质,因为它需要计算 f a
并对其进行一般性修改,以反映其价值中的失败。
这些被称为组合器的原因也与它们的使用方式有关。一个典型的地方看 optional
(而且确实如此, Alternative
一般)在解析器组合器库中。在这里,我们倾向于使用简单的构建基本的解析器 Parser
就像
satisfy :: (Char -> Bool) -> Parser Char
anyChar = satisfy (const True)
whitespace = satisfy isSpace
number = satisfy isNumeric
然后我们使用"组合器""修改"他们的行为"
-- the many and some combinators
many :: Alternative f => f a -> f [a] -- zero or more successes
some :: Alternative f => f a -> f [a] -- one or more successes
many f = some f <|> pure []
some f = (:) <$> f <*> many f
-- the void combinator forgets what's inside the functor
void :: Functor f => f a -> f ()
void f = const () <$> f
-- from the external point of view, this is another "basic" Parser
-- ... but we know it's actually built from an even more basic one
-- and the judicious application of a few "combinators"
blankSpace = Parser ()
blankSpace = void (many whitespace)
word :: Parser String
word = many (satisfy $ not . isSpace)
通常我们也调用组合多个函数的函数/Functors
/Monads
"组合者"也是如此,这也许是助记符
-- the combine combinator
combine :: Applicative f => f a -> f b -> f (a, b)
combine fa fb = (,) <$> fa <*> fb
-- the ignore-what's-next combinator
(<*) :: Applicative f => f a -> f b -> f a
fa <* fb = const <$> fa <*> fb
-- the do-me-then-forget-me combinator
(*>) :: Applicative f => f a -> f b -> f b
fa *> fb = flip const <$> fa <*> fb
line = Parser String
line = many (satisfy $ \c -> c /= '\n') <* satisfy (=='\n')
但最终,组合者更多的是关于API的意图和用法,而不是其严格的外延。您会经常看到从"基本部分"(如函数或 satisfy
然后修改并与一组"组合器"组合。该 Parser
上面的例子是一个典型的例子,但总的来说这是一个非常常见的Haskell模式。