Haskell中的组合子是什么?

14

Real World Haskell中,他们这样描述组合子:

在Haskell中,我们将接受其他函数作为参数并返回新函数的函数称为组合子。

然后稍后他们声明maybeIO函数是一个组合子,其类型签名如下:

maybeIO :: IO a -> IO (Maybe a)

但是我所看到的只是maybeIO是一个函数,它接受一个包装在IO单子中的值,并返回一个在IO单子中的值。那么这个函数如何成为一个组合子呢?


3
“组合子”一词有一些技术含义,但大多数Haskell程序员完全不关注它们。在口语用法中,它通常表示“值”或“东西”。 - shachaf
1
Haskell维基有一个名为“组合子”的页面,其中包含两个定义:http://www.haskell.org/haskellwiki/Combinator - Chris Kuklewicz
3个回答

23

当我们说组合器(combinator)时,实际上有两种含义,这个词有点含义重载。

  1. 我们通常指的是一种“组合”事物的函数。例如,您的函数接收一个 IO 值并构建出一个更复杂的值。使用这些“组合器”,我们可以从相对较少的原始函数中组合和创建新的复杂的 IO 值。

    例如,我们不是创建一个函数来读取 10 个文件,而是使用 mapM_ readFile。在这里,“组合器”是我们用来组合和构建值的函数。

  2. 更严格的计算机科学术语是“无自由变量的函数”。因此,

 -- 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)

这是更广阔领域中的一部分,称为“组合逻辑”,其中您试图基本上消除自由变量并用组合子和几个原始函数替换它。

简而言之:通常我们提到组合子时,指的是更一般的概念,称为“组合子模式”,其中有一些原始函数和大量用户定义的函数来构建更复杂的值。


13

对于组合子没有严格的定义,所以从这个意义上讲,它并没有什么实际含义。但是,在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在将函数转换、组合、反转并粘合在一起方面非常强大。当我们这样做时,有时需要一些胶水来连接它们,我们称之为组合器。


1
组合子的严格定义来自组合逻辑,应用于编程语言的组合子并非在 Haskell 中发明。事实上,Haskell 社区借用了计算机科学中一个早已确立的术语,并开始在各个地方使用它,污染了其含义。 - ᄂ ᄀ
人类语言就像病毒一样。领域特定的含义并不总是意味着对单词或短语的独占性使用。只要在 Haskell 对话中不会造成太多混淆,这并不是什么大问题。如果有人查询 hackage,第一站当然会看到 CL 的严格领域含义描述在前,而 Haskell 的日常领域用法描述在后。我没有发现任何抱怨或污染。 - wide_eyed_pupil

5
"组合子"在Haskell中的使用并没有严格的定义。最准确的用法是指以组合子演算为例,采取其他函数作为参数的函数;但在Haskell术语中,它经常被多重载以表示“修改”或“组合”函数,特别是对于FunctorMonad。在这种情况下,你可以说组合子是一个函数,它“在上下文中接受一些行动或值,并返回一个新的、修改后的行动或值”。
您提到的示例,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模式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接