Как это сделать
В Haskell это на самом деле очень просто:
Prelude Control.Monad> liftM2 (+) (* 2) (* 3) 10
50
Или, альтернативно:
Prelude Control.Applicative> (+) <$> (* 2) <*> (* 3) $ 10
50
Или, более подробно:
import Control.Monad
main :: IO ()
main = do let (+$) = liftM2 (+) -- Define a new infix operator
let double = (* 2)
let triple = (* 3)
print $ (double +$ triple) 10
Что здесь происходит? Haskell - очень инопланетный язык, если ваш опыт заключается в PHP и JavaScript, поэтому я постараюсь разбить его. Но просто напоследок: соответствующие функции здесь (liftM2
и <$>
/ <*>
) гораздо более общие, чем просто это использование, которое поначалу может затруднить их понимание. Однако полученная мощность того стоит.
Краткий обзор высокого уровня
Вот моя попытка обобщения очень высокого уровня: liftM2
принимает двоичную функцию op
и создает двоичную функцию, которая работает с «более сложными» значениями. Если ваши «более сложные» значения являются функциями с одним аргументом f
и g
, есть только один хороший способ объединить их и создать новую функцию с одним аргументом: создать функцию, которая передает свой аргумент x
в f
и g
, а затем вычисляет f x `op` g x
- объединяет их результаты, используя op
. В JavaScript это будет что-то вроде
function liftM2_functions(op) {
return function (f,g) {
return function (x) { op(f(x), g(x)) }
}
}
Так как liftM2
работает только с двоичными функциями, есть также liftM3
для троичных функций и т. Д., Но это грязно; таким образом, были введены операторы <$>
и <*>
, так что liftM<i>n</i> f a1 a2 ... a<i>n</i>
в точности эквивалентен f a1 <*> a2 <*> ... <*> a<i>n</i>
.
Подробнее
Если вы хотите получить более подробный ответ, читайте дальше. Я предупрежу вас, что я не уверен, насколько это ясно; он опирается на некоторые концепции, которые я имею в виду в полуколке, и я, возможно, не сделал хорошую работу. Но если вы игра, то мы идем.
Во-первых, давайте просто объясним некоторый синтаксис Haskell:
(* 2)
и (* 3)
- это сокращенные способы написания \x -> x*2
и \x -> x*3
, которые являются просто Haskell для JavaScript function (x) { return x*2 }
и function (x) { return x*3 }
.
(+)
- это просто функция, которая выполняет сложение; это префиксная форма инфиксного оператора +
.
f x
является приложением функции f(x)
; f x y
анализируется как (f x) y
, но может рассматриваться как приложение с двумя аргументами f(x,y)
.
- Инфиксный оператор
$
также является функциональным приложением, но с более низким приоритетом: ... $ x
анализируется как (...) x
.
<$>
и <*>
являются причудливыми инфиксными операторами.
Типы
Так! После этого давайте сначала рассмотрим первый фрагмент. В liftM2 (+) (* 2) (* 3) 10
требуемая «функция добавления функции» - liftM2 (+)
; два аргумента: (* 2)
и (* 3)
; и аргумент к результату , что равен 10
. В чем дело? В Haskell правильный способ думать об этом с точки зрения типов. Вот соответствующие типы, но будьте осторожны, вы, вероятно, не поймете их сразу.
liftM2 :: Monad m => (a -> b -> r) -> m a -> m b -> m r
(+) :: Num a => a -> a -> a
(* 2) :: Num a => a -> a
(* 3) :: Num a => a -> a
10 :: Num a => a
Здесь f :: t
означает «f
имеет тип t
». Эти типы довольно абстрактны, поэтому давайте упростим некоторые вещи. Когда вы видите Num a
, это означает «для некоторого типа a
, который является числом»; мы можем думать об этом как, например, Int
или Double
. Так что это дает нам
(+) :: Int -> Int -> Int
(* 2) :: Int -> Int
(* 3) :: Int -> Int
10 :: Int
Хорошо, 10
- это целое число. А что насчет (* 2)
и (* 3)
? Это функции, обозначенные ->
, которые отображают целое число в целое число. Вы знаете, что (+)
- это двоичная функция для целых чисел, поэтому вы можете подумать, что она будет иметь тип (Int,Int) -> Int
. Однако в Haskell правильный способ думать об этом - использовать функцию, которая принимает целое число, а возвращает другую функцию ; это называется карри. В JavaScript это будет реализовано как
function add(x) {
return function (y) {
return x + y
}
}
// Usage: add(10)(11) = 21.
Возможно, вы не понимаете, почему это хорошо, но через некоторое время оно станет актуальным.
Теперь давайте займемся liftM2 :: Monad m => (a -> b -> r) -> m a -> m b -> m r
. Что на земле происходит здесь? Игнорирование бита Monad m
говорит о том, что liftM2
принимает функцию с двумя аргументами, "monadic a
" и "monadic b
", и выдает "monadic r
". Однако - благодаря карри - это то же самое, что сказать, что liftM2
принимает функцию с двумя аргументами, а возвращает функцию с двумя аргументами, аргументы и результаты которой являются монадическими: liftM2 :: Monad m => (a -> b -> r) -> (m a -> m b -> m r)
. Вот что здесь происходит: liftM2 (+)
- монадическая версия сложения.
Монады (не паникуйте!)
Теперь я продолжаю использовать слово monadic, и я не определил его. И я не собираюсь! В Интернете есть множество учебных пособий по монадам, а некоторые даже хороши; если вам интересно, посмотрите «Вы могли бы изобрести монады!» . Вот все, что вам нужно понять на данный момент: монадическое значение - это то, которое каким-то образом «дополняется», имея некоторую дополнительную структуру вокруг него. Поскольку списки являются монадой, [1,2,3]
является монадическим int в монаде списка; там структура такова, что это int, который может быть или один, или два, или три. Здесь мы имеем дело с монадой для чтения : в данном случае монадное int - это просто функция, которая принимает некоторый объект типа a
и возвращает int. Идея в том, что это целое число, которое может читать и зависеть от некоторой среды.
Итак, мы имеем именно это. (* 2)
- это функция, которая принимает и возвращает целое число. Специализируясь на типе liftM2
для использования монады читателя, мы получаем следующее:
liftM2 :: (a -> b -> r) -> ((e -> a) -> (e -> b) -> (e -> r))
Теперь это многообещающе! Функция liftM2
принимает двоичную функцию и создает функцию , которая сама действует на функции . Применяя его к (+) :: Int -> Int -> Int
, мы получаем
liftM2 (+) :: (e -> Int) -> (e -> Int) -> (e -> Int)
Если подумать, это тип добавления функций: liftM2 (+)
принимает две функции с одним аргументом и создает новую функцию с одним аргументом, и делает это путем добавления их результатов.
Теперь, как реализован liftM2
? Вот так (переформатированная версия реальной реализации ):
liftM2 f m1 m2 = do x1 <- m1
x2 <- m2
return $ f x1 x2
Что здесь происходит? Это говорит: «получить значение типа a
из m1
, получить другое из m2
и объединить эти значения с заданным f
». Внутри нашей монады читателя (функции с одним аргументом) m1 :: e -> Int
и x1 <- m1
применяют m1
к "среде", которая в нашем примере - 10
. Так что в нашем случае у нас есть
do doubled <- (* 2)
tripled <- (* 3)
return $ doubled + tripled
Все это выражение, конечно, должно зависеть от загадочной среды, поскольку я нигде не упоминал об этом. Таким образом, его тип равен Int -> Int
: функция, которая принимает целое число, удваивает и утраивает его, а затем объединяет их вместе. Который, конечно, где мы начали.
Аппликативные функторы (не паникуйте!)
А как насчет <$>
и <*>
? Я объяснил их на очень высоком уровне выше, и я могу рассказать вам основную идею: <$>
подобен функции liftM1
, а <*>
применяет «сложные» функции к «сложным» значениям. Их фактические типы
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Я не буду здесь подробно объяснять это, потому что не думаю, что смогу сделать очень хорошую работу. В одном предложении объект в функторе является в некотором смысле простым значением с дополнительными усложнениями, а <$>
(также называемый fmap
) оставляет сложности в одиночестве и применяет функцию к простому старому значению внутри. И <*>
является частью дополнительной структуры, которая позволяет применять функции к нескольким простым значениям внутри нескольких структур одновременно. Но если вам нужна дополнительная информация, она там есть (хотя я не знаю хорошего ресурса - люди часто рекомендуют , однако, Learn You A Haskell ).
Заключение
Используя некоторые мощные конструкции (аппликативные функторы и / или монады), мы можем получить это понятие добавления функций в основном бесплатно, а также многое другое.(Умножение функций? liftM2 (*)
. Уровень функций if-then-else? liftM3 if' where if' c t f = if c then t else f
. И т. Д.) И самое замечательное в Haskell заключается в том, что эти расширенные функции запечены в .Функторы и монады определены в Prelude, модуле, который неявно включается в каждую программу на Haskell (например, пространство имен java.lang
в Java);модуль Control.Monad
(который определяет liftM2
) и модуль Control.Applicative
(который определяет аппликативные функторы, <$>
и <*>
) являются частью стандартной библиотеки 1 .Характер высшего порядка и статически типизированного Хаскелла позволяет использовать эти концепции;делать это в PHP или Java было бы в принципе невозможно, и хотя JavaScript мог выражать некоторые концепции, тот факт, что он динамически типизирован, означает, что реализация должна выглядеть совсем иначе.Другие языки, такие как Scala, могут использовать и действительно используют эти концепции, хотя Scala - единственный, о котором я могу подумать (и они представлены в Scalaz , а не в стандартной библиотеке).Главная идея заключается в том, что, имея доступ к этим мощным абстракциям, вы можете писать более простой код и меньше его, чтобы делать менее абстрактные вещи, такие как добавление функций, которые вы хотите делать.
1: Если честно, Control.Applicative
не соответствует стандарту Haskell 2010.Тем не менее, он включен по умолчанию в GHC, безусловно, наиболее используемый компилятор Haskell (это стандарт де-факто), а также в Hugs (интерпретатор Haskell, который имел значительное использование, но уступает позиции GHC), и до сих поркак я могу сказать, JHC и YHC (два других компилятора), а также.И ничто не мешает компилятору, не имеющему отношения к Haskell, строить Control.Applicative, поскольку код полностью соответствует Haskell 2010.