Что такое "лифт" в Хаскеле?

Я не понимаю, что такое "подъем". Должен ли я сначала понять монады, прежде чем понять, что такое "лифт"? (Я тоже совершенно не осведомлен о монадах :) Или кто-то может объяснить мне это простыми словами?

Ответ 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>.