Вибачте, я дійсно не знаю своєї математики, тому мені цікаво, як вимовляти функції в типовому класі Applicative
Знаю свою математику чи ні, я вважаю, що це значною мірою не має значення. Як ви, напевно, знаєте, Хаскелл запозичує декілька біт термінології з різних областей абстрактної математики, особливо це стосується теорії категорій , звідки ми отримуємо функціонерів та монадів. Використання цих термінів у Haskell дещо відрізняється від формальних математичних визначень, але вони, як правило, достатньо близькі, щоб у будь-якому разі були хорошими описовими термінами.
Клас Applicative
типу сидить десь між Functor
і Monad
, тому можна було б очікувати, що він має подібну математичну основу. Документація для Control.Applicative
модуля починається з:
Цей модуль описує проміжну структуру між функтором та монадою: він забезпечує чисті вирази та послідовності, але не зв'язує. (Технічно - сильний в'ялий моноїдний функтор.)
Хм.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Я не зовсім такий привабливий, як Monad
я думаю.
В основному все це зводиться до того, що Applicative
не відповідає жодній цікавій математичній концепції , тому немає готових термінів, які б захоплювали те, як це використовується в Haskell. Отже, відкладіть математику поки що.
Якщо ми хочемо знати, як зателефонувати, (<*>)
це може допомогти дізнатися, що це в основному означає.
Так що з Applicative
, у всякому разі, і чому ж ми називаємо це що?
Що Applicative
на практиці означає спосіб підняття довільних функцій на a Functor
. Розглянемо поєднання Maybe
(мабуть, найпростішого нетривіального Functor
) та Bool
(аналогічно найпростішого нетривіального типу даних).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
Функція fmap
дозволяє нам переходити not
від роботи Bool
до роботи над Maybe Bool
. Але що робити, якщо ми хочемо зняти (&&)
?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Ну, це не те , що ми хочемо , щоб у всіх ! Насправді це майже марно. Ми можемо спробувати бути розумним і крадькома інший Bool
в Maybe
через спину ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... але це не добре. З одного боку, це неправильно. З іншого боку, це некрасиво . Ми могли б продовжувати намагатися, але виявляється, що немає можливості зняти функцію з декількох аргументів, щоб працювати на довільномуFunctor
. Дратівливий!
З іншого боку, ми могли б зробити це легко , якби ми використовували Maybe
«s Monad
екземпляр:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Тепер, це багато клопоту просто перевести просту функцію - саме тому Control.Monad
забезпечує функцію , щоб зробити це автоматично, liftM2
. 2 у своїй назві посилається на те, що він працює над функціями рівно двох аргументів; подібні функції існують для функцій 3, 4 та 5 аргументів. Ці функції є кращими , але не ідеальними, і вказувати кількість аргументів некрасиво і незграбно.
Що підводить нас до статті, яка представила клас типу Applicative . У ній автори роблять по суті два спостереження:
- Підняття багатоаргументних функцій на a
Functor
- це дуже природна річ
- Для цього не потрібні всі можливості а
Monad
Додаток для звичайних функцій пишеться простим складанням термінів, тому для того, щоб зробити "піднятий додаток" максимально простим і природним, документ вводить операторів інфіксації, щоб стояти за програмою, піднятим уFunctor
клас та тип типу, щоб забезпечити необхідне для цього .
Все це приводить нас до наступного моменту: (<*>)
просто представляє додаток функції - то чому б це вимовляти інакше, ніж у вас пробіл «оператор поєднання»?
Але якщо це не дуже задовольняє, ми можемо помітити, що Control.Monad
модуль також забезпечує функцію, яка робить те саме, що і для монад:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Де ap
, звичайно, коротше слово "застосувати". Оскільки будь-який Monad
може бути Applicative
і ap
потребує лише підмножини функцій, присутніх в останньому, ми можемо сказати, що якщо б (<*>)
не оператор, його слід викликати ap
.
Ми також можемо підійти до речей з іншого напрямку. Операція Functor
підйому називається fmap
тому, що це узагальнення map
операції за списками. Яка функція у списках буде працювати (<*>)
? Звичайно, є що ap
в списках, але це не особливо корисно.
Насправді, існує, можливо, більш природна інтерпретація списків. Що спадає на думку, коли ви дивитесь підпис такого типу?
listApply :: [a -> b] -> [a] -> [b]
Існує щось настільки спокусливе в ідеї складати списки паралельно, застосовуючи кожну функцію першої до відповідного елемента другої. На жаль для нашого старого друга Monad
, ця проста операція порушує закони монади, якщо списки мають різну довжину. Але це штрафує Applicative
, і в цьому випадку (<*>)
стає способом з'єднання узагальненої версії zipWith
, так що, можливо, ми можемо уявити, як називати це fzipWith
?
Ця ідея про блискавку насправді приносить нам повне коло. Пригадайте, що математичні речі раніше, про моноїдальних функторів? Як випливає з назви, це спосіб поєднання структури моноїдів і функторів, обидва вони знайомі класи типу Haskell:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Як вони виглядатимуть, якби ви склали їх у коробку і трохи потрусили? Від цього Functor
ми збережемо уявлення про структуру, незалежну від її параметра , а також Monoid
збережемо загальну форму функцій:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Ми не хочемо , щоб припустити , що є спосіб , щоб створити дійсно «порожній» Functor
, і ми не можемо викликати в уяві значення довільного типу, тому ми будемо фіксувати тип , mfEmpty
як f ()
.
Ми також не хочемо змушувати mfAppend
потребувати послідовного параметра типу, тому тепер ми маємо це:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
Для чого тип результату mfAppend
? У нас є два довільних типи, про які ми нічого не знаємо, тому у нас не багато варіантів. Найрозумніше - просто тримати обидва:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
На mfAppend
даний момент явно узагальнена версія zip
списків, і ми можемо Applicative
легко реконструювати :
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Це також показує нам, що pure
пов'язаний з елементом ідентичності а Monoid
, тому інші хороші назви для нього можуть бути будь-якими, що підказують одиничне значення, нульову операцію тощо.
Це було тривалим, так підсумовуючи:
(<*>)
- це лише модифікована функціональна програма, тож ви можете її прочитати як "ap" або "застосувати", або повністю відключити її так, як це було б у звичайній програмі.
(<*>)
також орієнтовно узагальнюється zipWith
за списками, так що ви можете читати його як "поштові функтори з", подібно до читання fmap
як "карта функтора з".
Перший ближче до наміру Applicative
класу типу - як підказує назва - тому я рекомендую.
Насправді, я закликаю до ліберального використання та невимовлення всіх операторів, що підняли додаток :
(<$>)
, яка перетворює функцію з одним аргументом на a Functor
(<*>)
, яка ланцюжком функцій багато аргументів є через Applicative
(=<<)
, яка пов'язує функцію, яка входить у Monad
існуючий обчислення
Усі троє, по суті, просто звичайне застосування функцій, трохи приправлене.