Що таке "підйом" в Haskell?


138

Я не розумію, що таке "підйом". Чи варто спочатку зрозуміти монади, перш ніж зрозуміти, що таке "підйомник"? (Я теж зовсім не знаю про монади) Або хтось може мені це пояснити простими словами?


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%.
amalloy

2
Підйом без додатка є = Functor. Я маю на увазі у вас є два варіанти: Functor або Applicative Functor. Перший піднімає однопараметричні функції, а другі багатопараметричні функції. Насправді це все. Правильно? Це не ракетна наука :) це просто так звучить. Дякую за чудову відповідь btw!
jhegedus

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 you a 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. Він перетворює монадійну дію однієї монади на дію трансформованої монади.

Загалом, «ліфт» піднімає функцію / дію в «загорнуті» типу (так початкова функція отримує роботу «під спудом»).

Найкращий спосіб зрозуміти це, монади тощо, і зрозуміти, чому вони корисні, - це, мабуть, кодування та використання. Якщо є щось, що ви кодували раніше, ви підозрюєте, що виграєте від цього (тобто це зробить цей код коротшим тощо), просто спробуйте його, і ви легко зрозумієте концепцію.


13

Підйом - це концепція, яка дозволяє перетворити функцію на відповідну функцію в межах іншого (зазвичай більш загального) параметра

подивіться на http://haskell.org/haskellwiki/Lifting


40
Так, але ця сторінка починається "Ми зазвичай починаємо з (коваріантного) функтора ...". Не зовсім новачок.
Пол Джонсон

3
Але "функтор" пов'язаний, тому новачок може просто натиснути його, щоб побачити, що таке Функтор. Справді, пов’язана сторінка не настільки хороша. Мені потрібно отримати рахунок і виправити це.
jrockway

10
Це проблема, яку я бачив на інших сайтах функціонального програмування; кожне поняття пояснюється з точки зору інших (незнайомих) понять, поки новачок не піде повним колом (і кругом вигину). Це повинно бути пов'язане з подобою рекурсії.
ДНК

2
Проголосуйте за це посилання. Ліфт здійснює зв'язки між одним та іншим світом.
eccstartup

3
Такі відповіді хороші лише тоді, коли ви вже розумієте тему.
подвійний

-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>яка працює над контейнерами. Це називається підйомомфункції. Оскільки контейнер також називають функтором , передумовою для підйому є саме Функтори, а не Монади. Монади є свого роду "паралельними" підняттям. Обидва покладаються на поняття Functor і роблять f<a> -> f<b>. Різниця полягає в тому, що ліфтинг використовує a->bдля конверсії, тоді як Monad вимагає від користувача визначення a -> f<b>.


5
Я дав вам оцінку, бо "функтор - це якась ємність" - це принада з ароматом тролів. Приклад: функції від деяких rдо типу (давайте використовувати cдля різноманітності) - це Functors. Вони не "містять" жодних 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, і інше, передбачає псевдо-мутацію "контейнера". Також факт, що списки, як правило, повністю зберігаються в пам'яті, тоді як функції зазвичай зберігаються як обчислення. Але запам'ятовування / монорфні списки, які не зберігаються між дзвінками, обидва вибивають з цієї ідеї.
крапка з комою
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.