什么是Haskell中的Combinator

Sib*_*ibi 12 haskell combinators

Real World Haskell中,他们描述了这样的组合器:

在Haskell中,我们引用将其他函数作为参数并将新函数作为组合器返回的函数.

然后他们声明该maybeIO函数是一个组合器,它的类型签名如下所示:

maybeIO :: IO a -> IO (Maybe a)
Run Code Online (Sandbox Code Playgroud)

但我只能看到这maybeIO是一个函数,它接受IO monad中包含的值并返回IO monad中的值.那么这个函数如何成为组合子呢?

Dan*_*zer 19

当我们说组合子时,我们可能有两件事情.这个词有点超负荷.

  1. 我们通常指的是一种"结合"事物的功能.例如,您的函数接受一个IO值并构建一个更复杂的值.使用这些"组合器",我们可以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)
    
    Run Code Online (Sandbox Code Playgroud)

    这是一个名为"组合逻辑"的宏大字段的一部分,在该字段中,您寻求基本上消除自由变量并用组合器和一些原始函数替换它.

TLDR:通常当我们说组合器时,我们引用一个更普遍的概念,称为"组合模式",其中我们有一些原始函数和许多用户定义的函数来构建更复杂的值.


kqr*_*kqr 12

组合子没有严格的定义,因此它并不意味着任何意义上的任何东西.但是,在Haskell中很常见的是用更简单的函数或值来构建更复杂的函数或值,有时函数不能完全组合在一起,所以我们使用一些粘合剂使它们粘在一起.我们用来做组合的胶水,我们称之为组合器.

例如,如果要计算舍入到最接近整数的数字的平方根,可以将该函数编写为

approxSqrt x = round (sqrt x)
Run Code Online (Sandbox Code Playgroud)

您可能也意识到我们在这里所做的是采用两个函数并使用它们构建一个更复杂的函数作为构建块.然而,我们需要某种胶水将它们组合在一起,并且胶水是(.):

approxSqrt = round . sqrt
Run Code Online (Sandbox Code Playgroud)

因此函数组合运算符是函数的组合 - 它结合了函数来创建新函数.另一个例子是,您可能希望将文件的每一行读入列表.你可以用明显的方式做到这一点:

do
  contents <- readFile "my_file.txt"
  let messages = lines contents
  ...
Run Code Online (Sandbox Code Playgroud)

但!如果我们有一个函数读取文件并将内容作为字符串返回,我们会怎么做?然后我们可以做到

do
  messages <- readFileLines "my_file.txt"
  ...
Run Code Online (Sandbox Code Playgroud)

事实证明,我们有一个读取文件的函数,我们有一个函数,它接受一个大字符串并返回其中的行列表.如果我们只有一些粘合剂以有意义的方式将这两个功能粘在一起,我们就可以构建readFileLines!但当然,这就是Haskell,这种粘合剂很容易获得.

readFileLines = fmap lines . readFile
Run Code Online (Sandbox Code Playgroud)

在这里我们使用两个组合器!我们使用(.)之前的from,fmap实际上也是一个非常有用的组合器.我们说它将一个纯粹的计算"提升"到IO monad中,我们真正的意思是lines具有类型签名

lines :: String -> [String]
Run Code Online (Sandbox Code Playgroud)

fmap lines有签名

fmap lines :: IO String -> IO [String]
Run Code Online (Sandbox Code Playgroud)

所以fmap,当你想纯计算与IO计算的结合是非常有用的.


这些只是两个非常简单的例子.随着您了解更多Haskell,您将发现自己需要(并发明)越来越多的组合函数.Haskell在处理函数和转换函数,组合它们,将它们内部转换然后将它们粘在一起的方式非常强大.当我们这样做时,我们有时需要胶水,我们称之为组合器.


J. *_*son 5

“组合器”在 Haskell 中的使用并没有精确定义。这是最正确的使用它来指代其采取其它功能参数的函数一拉Combinator的微积分,但在Haskell术语它经常超载也意味着一个“修改”或“联合”的功能,尤其是一个FunctorMonad。在这种情况下,您可能会说组合器是一个函数,它“在上下文中执行一些操作或值,并在上下文中返回一个新的、修改过的操作或值”。

你的例子,maybeIO通常被称为optional

optional :: Alternative f => f a -> f (Maybe a)
optional fa = (Just <$> fa) <|> pure Nothing
Run Code Online (Sandbox Code Playgroud)

并且它具有类似组合器的性质,因为它接受计算f a并对其进行一般修改以反映其值的失败。

这些被称为组合器的原因也与它们的使用方式有关。一个典型的地方optional(实际上,Alternative通常)是在解析器组合器库中。在这里,我们倾向于使用简单的Parsers来构建基本的解析器,例如

satisfy :: (Char -> Bool) -> Parser Char
anyChar    = satisfy (const True)
whitespace = satisfy isSpace
number     = satisfy isNumeric
Run Code Online (Sandbox Code Playgroud)

然后我们使用“组合器”“修改”他们的行为

-- 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)
Run Code Online (Sandbox Code Playgroud)

通常,我们也将组合多个函数的函数称为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')
Run Code Online (Sandbox Code Playgroud)

但归根结底,组合器更多的是关于 API 的意图和用法,而不是其严格的外延。您会经常看到从“基本部分”(如函数)构建的库,或者satisfy然后对其进行修改并与一组“组合器”组合的库。Parser上面的例子是一个典型的例子,但总的来说这是一个非常常见的 Haskell 模式。