在Real World Haskell中,他们描述了这样的组合器:
在Haskell中,我们引用将其他函数作为参数并将新函数作为组合子返回的函数。
然后他们说明maybeIO
函数是一个组合子,它的类型签名如下:
maybeIO :: IO a -> IO (Maybe a)
但我只能看到maybeIO
是一个函数,它接受包含在IO monad中的值并返回IO monad中的值。那么这个函数如何成为一个组合器呢?
答案 0 :(得分:17)
当我们说组合子时,我们可能有两件事情。这个词有点超负荷。
我们通常指的是一种“结合”事物的功能。例如,您的函数接受IO
值并构建更复杂的值。使用这些“组合器”,我们可以从相对较少的原始函数中组合并创建新的复杂IO
值,以创建IO
值。
例如,我们使用mapM_ readFile
而不是创建一个读取10个文件的函数。这里组合器是我们用来组合和构建值的函数
更严格的计算机科学术语是“没有自由变量的函数”。所以
-- 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:通常当我们说组合子时,我们引用一个更普遍的概念,称为“组合模式”,其中我们有一些原始函数和许多用户定义的函数来构建更复杂的值。
答案 1 :(得分:11)
对组合子没有严格的定义,因此在这个意义上它并不意味着什么。但是,在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]
因此,当您想要将纯计算与IO计算相结合时,fmap
非常有用。
这些只是两个非常简单的例子。随着您了解更多Haskell,您将发现自己需要(并发明)越来越多的组合函数。 Haskell在处理函数和转换函数,组合它们,将它们内部转换然后将它们粘在一起的方式非常强大。当我们这样做时,我们有时需要胶水,我们称之为组合器。
答案 2 :(得分:3)
“Combinator”在Haskell中的使用并没有精确定义。使用它来引用其他函数作为参数la Combinator Calculus的函数是最正确的,但在Haskell术语中,它经常被重载,也意味着“修改”或“组合”函数,尤其是{{1} }或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
但最终,组合者更多地关注API的意图和用法,而不是其严格的外延。您经常会看到从“基本部分”构建的库,如函数或-- 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')
,然后将其修改并与一组“组合器”组合。上面的satisfy
示例是一个典型的例子,但总的来说这是一个非常常见的Haskell模式。