在Real World Haskell中,他们这样描述组合子:
在Haskell中,我们将接受其他函数作为参数并返回新函数的函数称为组合子。
然后稍后他们声明maybeIO
函数是一个组合子,其类型签名如下:
maybeIO :: IO a -> IO (Maybe a)
但是我所看到的只是maybeIO
是一个函数,它接受一个包装在IO单子中的值,并返回一个在IO单子中的值。那么这个函数如何成为一个组合子呢?
在Real World Haskell中,他们这样描述组合子:
在Haskell中,我们将接受其他函数作为参数并返回新函数的函数称为组合子。
然后稍后他们声明maybeIO
函数是一个组合子,其类型签名如下:
maybeIO :: IO a -> IO (Maybe a)
但是我所看到的只是maybeIO
是一个函数,它接受一个包装在IO单子中的值,并返回一个在IO单子中的值。那么这个函数如何成为一个组合子呢?
当我们说组合器(combinator)时,实际上有两种含义,这个词有点含义重载。
我们通常指的是一种“组合”事物的函数。例如,您的函数接收一个 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)
这是更广阔领域中的一部分,称为“组合逻辑”,其中您试图基本上消除自由变量并用组合子和几个原始函数替换它。
简而言之:通常我们提到组合子时,指的是更一般的概念,称为“组合子模式”,其中有一些原始函数和大量用户定义的函数来构建更复杂的值。
对于组合子没有严格的定义,所以从这个意义上讲,它并没有什么实际含义。但是,在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
也是一个非常有用的组合子。我们说它可以将一个纯计算提升(lift)到IO单子中,实际上我们的意思是lines
具有以下类型签名:
lines :: String -> [String]
但是fmap lines
的签名如下:
fmap lines :: IO String -> IO [String]
因此,fmap
在你想将纯计算与IO计算结合时非常有用。
这只是两个非常简单的例子。随着您学习更多的Haskell,您会发现自己需要(和发明)越来越多的组合函数。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)
-- 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模式。