Что такое комбинатор в Haskell

на Реальный Мир Haskell, они описывают комбинаторы вроде этого:

в Haskell мы ссылаемся на функции, которые принимают другие функции в качестве аргументов и возвращают новые функции как комбинаторы.

а потом они заявляют, что maybeIO функция является комбинатором, и ее сигнатура типа выглядит так:

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

но все, что я вижу, это maybeIO - это функция, которая принимает значение, обернутое в монаду IO и возвращает значение в монаде ИО. Тогда как эта функция становится комбинатором ?

3 ответов


есть действительно 2 вещи, которые мы могли бы иметь в виду, когда говорим combinator. Слово немного перегружено.

  1. мы обычно имеем в виду функцию, которая "объединяет" вещи. Например, ваша функция принимает IO value и создает более сложное значение. Используя эти "комбинаторы", мы можем объединить и создать новый комплекс IO значения из относительно немногих примитивных функций для создания IO значения.

    например, вместо создания функции который читает 10 файлов, мы используем mapM_ readFile. Здесь комбинаторы-это функции, которые мы используем для объединения и построения значений

  2. более строгий термин computer sciencey - это "функция без свободных переменных". Так что

     -- 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: обычно, когда мы говорим combinator, мы ссылаемся на более общее понятие, называемое "комбинаторный шаблон", где у нас есть несколько примитивных функций и множество пользовательских функций для создания более сложных значений.


нет строгого определения комбинаторов, так это ничего не значит в этом смысле. Однако в 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! Но, конечно, это был Хаскелл., этот клей легко доступен.

readFileLines = fmap lines . readFile

здесь мы используем два комбинаторы! Мы используем (.) и fmap на самом деле очень полезный комбинатор. Мы говорим, что он" поднимает " чистое вычисление в монаду IO, и на самом деле мы имеем в виду, что lines имеет подпись типа

lines :: String -> [String]

но fmap lines имеется подпись

fmap lines :: IO String -> IO [String]

так fmap полезно, когда вы хотите объединить чистые вычисления с IO вычисления.


это только два очень простых примера. Когда вы узнаете больше Haskell, вы обнаружите, что вам нужно (и изобретать) все больше и больше комбинаторных функций для себя. Haskell очень мощный в том, как вы можете взять функции и преобразовать их, объединить их, вывернуть их наизнанку, а затем склеить их вместе. Кусочки клея, которые нам иногда нужны, когда мы это делаем, те биты, которые мы называем комбинаторами.


"Combinator" точно не определен в его использовании в Haskell. Наиболее правильно использовать его для ссылки на функции, которые принимают другие функции в качестве аргументов a la Комбинаторное Исчисление но в терминологии Haskell он часто перегружен, чтобы также означать" модификацию "или" комбинацию " функции, особенно Functor или Monad. В этом случае вы можете сказать, что комбинатор-это функция ,которая " принимает некоторое действие или значение в контексте и возвращает новое, измененное действие или значение в контексте".

ваш пример, maybeIO часто называют optional

optional :: Alternative f => f a -> f (Maybe a)
optional fa = (Just <$> fa) <|> pure Nothing

и он имеет комбинаторную природу, потому что он принимает вычисление f a и изменяет его в целом, чтобы отразить неудачу в его значении.

причина, по которой они называются комбинаторами, также связана с тем, как они используются. Типичное место для просмотра optional (и действительно, Alternative вообще) находится в библиотеках комбинаторов синтаксического анализатора. Здесь мы склонны создавать базовые Парсеры использование simple 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 "комбинаторы", а также, что, возможно, делает мнемонический смысл

-- 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 которые затем модифицируются и объединяются с набором "комбинаторов". The Parser пример выше является квинтэссенцией примера, но в целом это очень распространенный шаблон Haskell.