Что такое «лифтинг» в Хаскеле?

138

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

GabiMe
источник
9
Может быть полезно, а может и нет: haskell.org/haskellwiki/Lifting
kennytm

Ответы:

179

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

Обычно у вас есть некоторый тип данных с параметром. Что-то вроде

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. Это, в свою очередь, может быть применено к следующему аргументу, используя (<*>), и так далее. Так что теперь вместо того, чтобы иметь функцию лифта для каждой арности, у вас есть цепочка аппликаций.

Пол Джонсон
источник
26
Вероятно, стоит напомнить, что лифты должны соблюдать стандартные законы lift id == idи lift (f . g) == (lift f) . (lift g).
Карлос Шайдеггер
13
Лифты действительно «категория или что-то». Карлос только что перечислил законы Функтора, где idи .являются стрелкой-идентификатором и композицией стрелки какой-либо категории, соответственно. Обычно , когда речь идет о Haskell, категория в вопросе «Hask», чьи стрелы Haskell функции (другими словами, idи .относятся к функциям Haskell вы знаете , и любовь).
Дэн Бертон,
3
Это следует читать instance Functor Foo, не так instance Foo Functorли? Я бы отредактировал себя, но я не уверен на 100%.
Амаллой
2
Подъем без аппликативного = Функтор. Я имею в виду, у вас есть 2 варианта: Функтор или Аппликативный Функтор. Первый поднимает однопараметрические функции, второй многопараметрические функции. Почти все. Правильно? Это не ракетостроение :) это просто звучит так. Спасибо за отличный ответ, кстати!
Джегедус
2
@atc: это частичное приложение. См. Wiki.haskell.org/Partial_application
Пол Джонсон
41

Пол и Яирчу - хорошие объяснения.

Я хотел бы добавить, что поднимаемая функция может иметь произвольное количество аргументов и что они не обязательно должны быть одного типа. Например, вы также можете определить liftFoo1:

liftFoo1 :: (a -> b) -> Foo a -> Foo b

В общем случае, снятие функций, которые принимают 1 аргумент, фиксируется в классе типов Functor, и операция подъема называется fmap:

fmap :: Functor f => (a -> b) -> f a -> f b

Обратите внимание на сходство с liftFoo1типом s. На самом деле, если у вас есть liftFoo1, вы можете сделать Fooэкземпляр Functor:

instance Functor Foo where
  fmap = liftFoo1

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

Надеюсь это поможет!

Мартейн
источник
25

Давайте начнем с примера (для более ясного представления добавлен пробел):

> 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. Он преобразует монадическое действие одной монады в действие трансформированной монады.

В общем, «лифт» поднимает функцию / действие в «обернутый» тип (поэтому исходная функция начинает работать «под обертками»).

Лучший способ понять это, монады и т. Д. И понять, почему они полезны, - это, вероятно, написать код и использовать его. Если есть что-то, что вы ранее запрограммировали, что, как вы подозреваете, может извлечь из этого пользу (т.е. это сделает этот код короче и т. Д.), Просто попробуйте это, и вы легко поймете эту концепцию.

yairchu
источник
13

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

взгляните на http://haskell.org/haskellwiki/Lifting

Насер Хаджлоо
источник
40
Да, но эта страница начинается "Мы обычно начинаем с (ковариантного) функтора ...". Не совсем дружелюбный новичок.
Пол Джонсон
3
Но «функтор» связан, поэтому новичок может просто щелкнуть по нему, чтобы увидеть, что такое Функтор. По общему признанию, связанная страница не так хороша. Мне нужно получить учетную запись и исправить это.
jrockway
10
Это проблема, которую я видел на других сайтах функционального программирования; каждая концепция объясняется в терминах других (незнакомых) концепций до тех пор, пока новичок не пойдет по кругу (и не облетит круг). Должно быть что-то делать с симпатичной рекурсией.
ДНК
2
Проголосуйте за эту ссылку. Лифт связывает один мир с другим.
eccstartup
3
Подобные ответы хороши только тогда, когда вы уже поняли тему.
doubleOrt
-2

Согласно этому блестящему уроку , функтор - это некоторый контейнер (например Maybe<a>, List<a>или Tree<a>который может хранить элементы какого-то другого типа a). Я использовал нотацию обобщений Java <a>для типов элементов aи представляю элементы как ягоды на дереве Tree<a>. Есть функция fmap, которая принимает функцию преобразования элемента a->bи контейнер functor<a>. Это относится a->bк каждому элементу контейнера, эффективно превращающему его в functor<b>. Когда только первый аргумент подается, a->b, fmapожидает , пока functor<a>. То есть, a->bтолько поставка превращает эту функцию уровня элемента в функцию, functor<a> -> functor<b>которая работает над контейнерами. Это называется подъемомфункции. Поскольку контейнер также называется функтором , Функторы, а не Монады, являются предпосылкой для подъема. Монады своего рода «параллельны» подъему. Оба полагаются на понятие Функтора и делают f<a> -> f<b>. Разница в том, что лифт используется a->bдля преобразования, тогда как Monad требует от пользователя определения a -> f<b>.

Val
источник
5
Я поставил вам оценку, потому что «функтор - это какой-то контейнер» - это приманка со вкусом тролля. Пример: функции от некоторого rк типу (давайте использовать cдля разнообразия), являются Функторами. Они не «содержат» ничего c. В этом случае fmap - это композиция функций, которая принимает a -> bфункцию и r -> aединицу, чтобы дать вам новую r -> bфункцию. Все еще нет контейнеров. Также, если бы я мог, я бы отметил это снова для последнего предложения.
BMeph
1
Кроме того, fmapэто функция, которая ничего не «ждет»; «Контейнер», являющийся Функтором, - вот и вся точка подъема. Кроме того, монады - это, во всяком случае, двойная идея для подъема: монада позволяет вам использовать то, что было поднято несколько раз подряд, как если бы оно было поднято только один раз - это больше известно как выравнивание .
BMeph
1
@BMeph To wait, to expect, to anticipateявляются синонимами. Говоря «функция ожидает», я имел в виду «функция ожидает».
Val
@BMeph Я бы сказал, что вместо того, чтобы думать о функции как о контрпримере к идее о том, что функторы являются контейнерами, вы должны думать об экземпляре нормального функтора функции как о контрпримере к идее, что функции не являются контейнерами. Функция представляет собой отображение домена в кодомен, домен является перекрестным произведением всех параметров, а домен является выходным типом функции. Точно так же список является отображением Naturals во внутренний тип списка (домен -> кодомен). Они становятся еще более похожими, если вы запомнили функцию или не сохранили список.
точка с запятой
@BMeph - один из списков единственных причин, который больше похож на контейнер в том, что во многих языках они могут быть видоизменены, тогда как традиционно функции не могут. Но в Haskell даже это не является справедливым утверждением, поскольку ни один из них не может быть мутирован, и оба могут быть мутированы при копировании: b = 5 : aи f 0 = 55 f n = g nоба включают псевдо-мутирование «контейнера». Кроме того, тот факт, что списки, как правило, полностью хранятся в памяти, тогда как функции, как правило, хранятся в виде расчета. Но запоминание / монорфные списки, которые не сохраняются между вызовами, разрушают эту идею.
точка с запятой