Я не понимаю, что такое "подъем". Должен ли я сначала понять монады, прежде чем понять, что такое "лифт"? (Я тоже совершенно не осведомлен о монадах :) Или кто-то может объяснить мне это простыми словами?
Что такое "лифт" в Хаскеле?
Ответ 1
Подъем - это скорее шаблон проектирования, чем математическая концепция (хотя я ожидаю, что кто-то здесь сейчас опровергнет меня, показав, как подъемы - это категория или что-то в этом роде).
Обычно у вас есть некоторый тип данных с параметром. Что-то вроде
data Foo a = Foo { ...stuff here ...}
Предположим, вы обнаружили, что во многих случаях использование Foo
принимает числовые типы (Int
, Double
и т.д.), и вам по-прежнему приходится писать код, который разворачивает эти числа, добавляет или умножает их, а затем переносит их обратно. вверх. Вы можете замкнуть это, написав один раз код развертки и переноса. Эта функция традиционно называется "лифт", потому что она выглядит следующим образом:
liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c
Другими словами, у вас есть функция, которая принимает функцию с двумя аргументами (например, оператор (+)
) и превращает ее в эквивалентную функцию для Foos.
Теперь вы можете написать
addFoo = liftFoo2 (+)
Изменение: дополнительная информация
Конечно, вы можете иметь liftFoo3
, liftFoo4
и так далее. Однако в этом часто нет необходимости.
Начните с наблюдения
liftFoo1 :: (a -> b) -> Foo a -> Foo b
Но это точно так же, как fmap
. Поэтому вместо liftFoo1
вы написали бы
instance Functor Foo where
fmap f foo = ...
Если вы действительно хотите полной регулярности, вы можете сказать
liftFoo1 = fmap
Если вы можете превратить Foo
в функтор, возможно, вы сможете сделать его аппликативным функтором. На самом деле, если вы можете написать liftFoo2
, то аппликативный экземпляр выглядит следующим образом:
import Control.Applicative
instance Applicative Foo where
pure x = Foo $ ... -- Wrap 'x' inside a Foo.
(<*>) = liftFoo2 ($)
Оператор (<*>)
для Foo имеет тип
(<*>) :: Foo (a -> b) -> Foo a -> Foo b
Применяет упакованную функцию к упакованному значению. Так что, если вы можете реализовать liftFoo2
, вы можете написать это с точки зрения этого. Или вы можете реализовать это напрямую и не беспокоиться о liftFoo2
, потому что модуль Control.Applicative
включает в себя
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
и также есть liftA
и liftA3
. Но вы на самом деле не используете их очень часто, потому что есть другой оператор
(<$>) = fmap
Это позволяет вам написать:
result = myFunction <$> arg1 <*> arg2 <*> arg3 <*> arg4
Термин myFunction <$> arg1
возвращает новую функцию, заключенную в Foo. Это, в свою очередь, может быть применено к следующему аргументу, используя (<*>)
и так далее. Так что теперь вместо того, чтобы иметь функцию подъема для каждой арности, у вас есть цепочка аппликаций.
Ответ 2
Пол и yairchu являются хорошими объяснениями.
Я хотел бы добавить, что функция снятия может иметь произвольное количество аргументов и что они не должны быть одного типа. Например, вы также можете определить liftFoo1:
liftFoo1 :: (a -> b) -> Foo a -> Foo b
В общем случае снятие функций, принимающих один аргумент, фиксируется в классе типов Functor
, а операция подъема называется fmap
:
fmap :: Functor f => (a -> b) -> f a -> f b
Обратите внимание на сходство с типом liftFoo1
. Фактически, если у вас есть liftFoo1
, вы можете сделать Foo
экземпляр Functor
:
instance Functor Foo where
fmap = liftFoo1
Кроме того, обобщение поднятия на произвольное число аргументов называется аппликативным стилем. Не беспокойтесь о том, чтобы погрузиться в это до тех пор, пока вы не поймете снятие функций с фиксированным числом аргументов. Но когда вы это сделаете, Учите вас в Haskell, есть хорошая глава об этом. Typeclassopedia - еще один хороший документ, описывающий Functor и Applicative (а также другие классы типов, прокрутите вниз до правой главы в этом документе).
Надеюсь, это поможет!
Ответ 3
Давайте начнем с примера (для более ясного представления добавлен пробел):
> import Control.Applicative
> replicate 3 'a'
"aaa"
> :t replicate
replicate :: Int -> b -> [b]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) => f Int -> f b -> f [b]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> ['a','b','c']
"abc"
liftA2
преобразует функцию простых типов в функцию тех же типов, заключенных в Applicative
, как списки, IO
и т.д.
Другой распространенный лифт - это lift
из Control.Monad.Trans
. Он преобразует монадическое действие одной монады в действие трансформированной монады.
В общем случае "lift" поднимает функцию/действие в "обернутый" тип (поэтому оригинальная функция начинает работать "под обертками").
Лучший способ понять это, монады и т.д. И понять, почему они полезны, - это, вероятно, написать код и использовать его. Если что-то, что вы ранее запрограммировали, что вы подозреваете, может извлечь из этого пользу (то есть это сделает этот код короче и т.д.), Просто попробуйте это, и вы легко поймете концепцию.
Ответ 4
Подъем - это концепция, которая позволяет вам преобразовать функцию в соответствующую функцию внутри другой (обычно более общей) настройки
взгляните на http://haskell.org/haskellwiki/Lifting
Ответ 5
Согласно этот блестящий учебник, функтором является некоторый контейнер (например, Maybe<a>
, List<a>
или Tree<a>
, который может хранить элементы другого типа, a
). Я использовал нотацию Java generics, <a>
, для типа элемента a
и думаю о элементах как ягоды на дереве Tree<a>
. Существует функция fmap
, которая принимает функцию преобразования элементов, a->b
и контейнер functor<a>
. Он применяет a->b
к каждому элементу контейнера, эффективно преобразуя его в functor<b>
. Когда предоставляется только первый аргумент, a->b
, fmap
ждет functor<a>
. То есть, поставка a->b
сама по себе превращает эту функцию уровня элемента в функцию functor<a> -> functor<b>
, которая работает с контейнерами. Это называется подъемом функции. Так как контейнер также называется функтором, то Фунтеры, а не Монады являются предпосылкой для подъема. Монады являются своего рода "параллельными" для подъема. Оба полагаются на понятие Functor и делают f<a> -> f<b>
. Разница заключается в том, что при подъеме используется a->b
для преобразования, тогда как Monad требует, чтобы пользователь определял a -> f<b>
.