在Real World Haskell中,他们这样描述组合子:
在 Haskell 中,我们将其他函数作为参数并返回新函数作为组合子的函数。
然后他们声明这个maybeIO
函数是一个组合子,它的类型签名看起来像这样:
maybeIO :: IO a -> IO (Maybe a)
但我能看到的maybeIO
是一个函数,它接受一个包装在 IO monad 中的值并返回一个 IO monad 中的值。那么这个函数如何成为一个组合子呢?
在Real World Haskell中,他们这样描述组合子:
在 Haskell 中,我们将其他函数作为参数并返回新函数作为组合子的函数。
然后他们声明这个maybeIO
函数是一个组合子,它的类型签名看起来像这样:
maybeIO :: IO a -> IO (Maybe a)
但我能看到的maybeIO
是一个函数,它接受一个包装在 IO monad 中的值并返回一个 IO monad 中的值。那么这个函数如何成为一个组合子呢?
当我们说组合器时,我们实际上可能意味着两件事。这个词有点过分了。
我们通常指的是“组合”事物的功能。例如,您的函数接受一个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:通常当我们说组合器时,我们指的是一个更一般的概念,称为“组合器模式”,其中我们有一些原始函数和许多用户定义的函数来构建更复杂的值。
组合子没有严格的定义,所以在这个意义上它并不意味着什么。然而,在 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
这里我们使用两个组合器!我们使用了(.)
from before,fmap
实际上也是一个非常有用的组合子。我们说它将纯计算“提升”到 IO monad,我们真正lines
的意思是它具有类型签名
lines :: String -> [String]
但fmap lines
有签名
fmap lines :: IO String -> IO [String]
当您想将纯计算与 IO 计算结合起来时, sofmap
很有用。
这只是两个非常简单的例子。随着您了解更多 Haskell,您会发现自己需要(并发明)越来越多的组合函数。Haskell 非常强大,您可以获取函数并转换它们、组合它们、将它们翻转过来然后将它们粘在一起。当我们这样做时,我们有时需要一些胶水,我们称之为组合器。
“组合器”在 Haskell 中的使用并没有精确定义。使用它来指代将其他函数作为参数的函数是最正确的,例如组合演算,但在 Haskell 术语中,它经常被重载为也意味着“修改”或“组合”函数,尤其是 a Functor
or Monad
。在这种情况下,您可能会说组合器是一个“在上下文中采取一些动作或值并在上下文中返回新的、修改过的动作或值”的函数。
你的例子,maybeIO
通常被称为optional
optional :: Alternative f => f a -> f (Maybe a)
optional fa = (Just <$> fa) <|> pure Nothing
它具有类似组合器的性质,因为它接受计算f a
并对其进行一般修改以反映其价值的失败。
这些被称为组合器的原因也与它们的使用方式有关。一个典型的地方optional
(实际上,Alternative
一般来说)是在解析器组合库中。在这里,我们倾向于使用简单Parser
的 s 来构建基本的解析器,例如
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 模式。