Нбрк - Прогулянки з тайпкласами Haskell аплікаційні функтори

тайпкласами

Як ми з'ясували, функторы дозволяють втягувати “звичайну” функцію на певний обчислювальний контекст, перетворюючи їх у функцію над контекстами:

У цьому прикладі (* 3) - це функція "потроєння" (функція від одного числа, звідси і a - a). Функція fmap (* 3) тоді є “функцією потроєння вмісту деякого контейнера (функтора)”. Конкретизуємо приклад на списковий функцій [] :

Для деякого функтора f і якихось типів a і b ми маємо f a і f (a - b) .

Як же нам застосувати функцію в контексті до значення в контексті і отримати результат в контексті? Засобів тайпкласу Functor вже не вистачає, щоб застосувати f (a - b) до f a і отримати f b . Хотілося б вміти застосовувати функції до аргументів у f-світі, тим самим отримуючи можливість вибудовувати ланцюжок обчислень (комп'ютерну програму) всередині оточення (потрібного обчислювального контексту).

Очевидно, ми не можемо так просто взяти і написати якийсь загальний код для застосування функції до аргументу всередині якогось абстрактного функтора. Адже такий код повинен буде знати, як деконструювати функтор, щоб витягнути "чисту" функцію та її "чистий" аргумент, а також навпаки, як конструювати функтор з результатом даного застосування (аплікації).

Applicative

Найкрутіший тайпклас Applicative наділяє функтори такою можливістю. Аплікаційні функтори є уточненням функтора, тому будь-який екземпляр Applicative – це також і Functor (але не навпаки). Визначення аплікативного функтора:

Сигнатура точнісінько збігається з тим, чого ми намагалися отримати у нашому f-світі у прикладі вище!

Завдяки повсюдному карированию, ми можемо вільно застосовувати функції від кількох аргументіввсередині аплікативного контексту, головне, щоб збігалися всі типи входів/виходів. Аплікативне оточення, таким чином, ніби еволюціонує, змінюється при “протаскуванні” каруванням обчислення через контекст з кожним новим викликом. Наприклад, для деякого аплікативного функтора a:

Повернемося до визначення аплікативного функтора. Функція pure поміщає "чисте" значення в контекст і цим певною мірою нагадує fmap. Ідея тут у тому, що pure втягує значення в якийсь аплікативний контекст за замовчуванням, в (поки що) вільний від майбутніх ефектів контейнер.

Неважко помітити, що аплікативні функтори пов'язані з функторами наступним рівнянням:

Застосування g до вмісту контейнера x для Functor ідіоматично те ж саме, що приміщення g до аплікативного контейнера за замовчуванням і потім застосування функції всередині даного аплікативного контейнера. Використовуючи інфікований синонім для fmap можна переписати рівняння так:

Вправи

1. Implement an instance of Applicative for Maybe.

Нагадаю інтерфейс Applicative:

Це просто. Я визначаю тип MyMaybe, роблю його спочатку функтором, а потім і аплікативним функтором. Аплікаційний контекст обламується, і втягнута функція не висохлиться, коли в контексті знаходиться значення MyNothing :

Протестуємо на функції, що підсумовує чотири числа:

Внаслідок повсюдного карирування в Haskell, sum4 діє на аплікативний контекст MyMaybe, оновлюючи цей контекст після кожної аплікації (після кожного). Ми бачимо, як наш контекст еволюціонує, доходячи до останнього, четвертого аргументу. Ми також бачимо, як аплікації припиняються коли натикається на MyNothing , і контекст серед обчислення раптом стає постійним (приймає значенняMyNothing), протягуючи "нічого" до самого кінця.

Ми справді можемо думати про аплікативний контекст, як про деяку впорядковану послідовність аплікацій. Наприклад, ми можемо обірвати обчислення суми чотирьох чисел посередині і спробувати два різні “продовження” перерваного аплікативного контексту:

2.Визначте правильну definition of pure for ZipList instance of Applicative—there is only one implementation that satisfies the law relating pure and ( ).

Визначення ZipList дано таке:

Перш ніж написати pure для Applicative ZipList, подивимося як працює аплікація з функцією zipWith усередині такого контексту:

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

Елегантне рішення з функцією repeat, яка повторює свій параметр до нескінченності:

Ми втягуємо до аплікативного контексту нескінченну кількість функцій, тому наш функтор буде працювати зі списками аргументів будь-якої довжини.

Аплікаційні функтори

Деякі аплікативні функтори з Prelude Haskell:

Applicative [] реалізує аплікацію не “зшивання”, як ZipList вище, а послідовним застосуванням функції до кожного з входів та збором результатів:

Тобто, підтримує інтуїцію списку як аплікативного контексту недетермінованого вибору – ми отримуємо всі можливі результати одночасно.

Але мені найбільше подобається останній аплікативний функтор. Це – пара, перший елемент якої є деякою моноїд (алгебраїчну структуруз асоціативною бінарною операцією та нейтральним елементом щодо неї). Моноїдальні типи в Haskell представлені тайпкласом Monoid:

Одна з можливих інтуїцій, пов'язаних з цим досить загальним об'єктом, - це якийсь магазин (обойма), якийсь акумулятор або ланцюжок (рядок), що розростається, ланки якого ліпляться один до одного бінарною операцією mappend 1 . Наприклад, Monoid [] реалізує mappend звичайною конкатенацією списків, нейтральним елементом є порожній список (склеювання з порожнім списком не змінює вихідного списку).

Повернемося до нашого прекрасного аплікативного функтора. Щоб зрозуміти його зміст, треба зрозуміти його. Ось моя реалізація:

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

Таким чином, можна акумулювати, наприклад, логи того, що відбувається всередині контексту: проміжні результати обчислень, кроки, помилки, стек виклику і т.п. Реалізація вміє деконструювати поточний ( f (a -> b) ) та наступний ( f a ) елементи в аплікативному ланцюжку, тому можна контролювати процес обчислення, виходячи з його історії (еволюції аплікативного оточення).

Зрозуміло, структура замкнута щодо введеної в ній операції: обидва елементи належать множині-носія, як і їх "сума". Цей факт з алгебри в Haskell відповідає однаковим типам у сигнатурі mappend , а також тому факту, що конструктори значень a не виводять за межі типу.↩