На мою скромну думку, відповіді на відоме питання "Що таке монада?" , особливо найбільш голосуючі, спробуйте пояснити, що таке монада, не чітко пояснюючи, чому монади справді необхідні . Чи можна їх пояснити як вирішення проблеми?
На мою скромну думку, відповіді на відоме питання "Що таке монада?" , особливо найбільш голосуючі, спробуйте пояснити, що таке монада, не чітко пояснюючи, чому монади справді необхідні . Чи можна їх пояснити як вирішення проблеми?
Відповіді:
Тоді у нас є перша велика проблема. Це програма:
f(x) = 2 * x
g(x,y) = x / y
Як ми можемо сказати, що потрібно виконати спочатку ? Як ми можемо сформувати впорядковану послідовність функцій (тобто програму ), використовуючи не більше функцій ?
Рішення: складати функції . Якщо ви хочете спочатку, g
а потім f
, просто напишіть f(g(x,y))
. Таким чином, «програма» є функцією , а також: main = f(g(x,y))
. Гаразд, але ...
Більше проблем: деякі функції можуть вийти з ладу (тобто g(2,0)
розділити на 0). У FP у нас немає "винятків" (виняток не є функцією). Як ми її вирішуємо?
Рішення: Дозвольмо функціям повертати два види речей : замість того, щоб мати g : Real,Real -> Real
(функція з двох реальних в реальну), давайте дозволимо g : Real,Real -> Real | Nothing
(функція з двох реальних в (справжніх або нічого)).
Але функції повинні (бути простішими) повертати лише одне .
Рішення: давайте створимо новий тип даних, що підлягає поверненню, " тип боксу ", який закриває, можливо, справжній або просто нічого. Отже, ми можемо мати g : Real,Real -> Maybe Real
. Гаразд, але ...
Що зараз відбувається з f(g(x,y))
? f
не готовий до споживання Maybe Real
. І ми не хочемо змінювати будь-яку функцію, з якою ми могли б з'єднатися, g
щоб споживати a Maybe Real
.
Рішення: давайте мати спеціальну функцію для "підключення" / "складання" / "посилання" функцій . Таким чином, ми можемо, за лаштунками, адаптувати вихід однієї функції для подачі наступної.
У нашому випадку: g >>= f
(підключити / скласти g
до f
). Ми хочемо >>=
отримати g
вихід, перевірити його і, якщо він Nothing
просто не дзвонить f
і не повертається Nothing
; або, навпаки, витягати коробку Real
і годувати f
нею. (Цей алгоритм є лише реалізацією >>=
для Maybe
типу). Також зауважте, що >>=
потрібно писати лише один раз на "тип боксу" (інше поле, різний алгоритм адаптації).
Існує багато інших проблем, які можна вирішити за допомогою цього самого шаблону: 1. Використовуйте "поле" для кодування / зберігання різних значень / значень, і виконайте такі функції, g
що повертають ці "коробкові значення". 2. Попросіть композитора / лінкера, g >>= f
який допоможе підключити g
вихід до f
входу, тому нам взагалі нічого не потрібно змінювати f
.
Чудовими проблемами, які можна вирішити за допомогою цієї методики, є:
маючи глобальний стан, що кожна функція в послідовності функцій ("програма") може розділяти: рішення StateMonad
.
Нам не подобаються "нечисті функції": функції, які дають різний вихід за один і той же вхід. Тому позначимо ці функції, зробивши їх поверненням позначеного / коробкового значення: IO
монада.
Загальне щастя!
IO
монада - це ще одна проблема в списку IO
(пункт 7). З іншого боку, IO
з’являється лише один раз і наприкінці, тож не розумійте вашої «більшості часу, коли говорите ... про IO».
Either
). Найбільше відповідей - на те, «навіщо нам потрібні функтори?».
g >>= f
щоб допомогти підключити g
вихідний сигнал до f
вхідного сигналу, тому нам взагалі нічого не потрібно міняти f
." це зовсім не правильно . Перед тим, як f(g(x,y))
, f
можна було виготовити що завгодно. Це могло бути f:: Real -> String
. З "монадичним складом" його потрібно змінити на отримання Maybe String
, інакше типи не підходять. Більше того, >>=
сама по собі не підходить !! Це те, >=>
що робить ця композиція, а не >>=
. Дивіться дискусію з dfeuer під відповіддю Карла.
Відповідь, звичайно, "у нас немає" . Як і у всіх абстракціях, це не потрібно.
Haskell не потрібна абстракція монади. Це не потрібно для виконання IO чистою мовою. IO
Тип піклується про те тільки штрафом сам по собі. Існуючий Монадический desugaring з do
блоків можуть бути замінені на desugaring bindIO
, returnIO
і failIO
як це визначено в GHC.Base
модулі. (Це не задокументований модуль щодо злому, тому мені доведеться вказати на його джерело для документації.) Отже, ні, немає необхідності в абстракції монади.
Тож якщо вона не потрібна, навіщо вона існує? Тому що було встановлено, що багато моделей обчислень утворюють монадичні структури. Абстракція структури дозволяє писати код, який працює у всіх екземплярах цієї структури. Якщо говорити коротше - повторне використання коду.
У функціональних мовах найпотужнішим інструментом для повторного використання коду був склад функцій. Старий добрий (.) :: (b -> c) -> (a -> b) -> (a -> c)
оператор надзвичайно потужний. Це дозволяє легко писати крихітні функції та склеювати їх разом із мінімальними синтаксичними чи семантичними накладними.
Але бувають випадки, коли типи виходять не зовсім правильно. Що ти робиш, коли маєш foo :: (b -> Maybe c)
і bar :: (a -> Maybe b)
? foo . bar
не вводить перевірку, тому що вони не є одним b
і Maybe b
тим же типом.
Але ... це майже правильно. Ви просто хочете трохи свободи. Ви хочете мати можливість ставитися Maybe b
так, ніби це в основному b
. Це погана ідея, коли б просто розцінювати їх як один і той же тип. Це більш-менш те саме, що і нульові покажчики, які Тоні Хоаре чудово назвав помилкою в мільярд доларів . Тож якщо ви не можете ставитися до них як до одного типу, можливо, ви можете знайти спосіб розширити передбачений механізм композиції (.)
.
У цьому випадку важливо реально вивчити теорію, що лежить в основі (.)
. На щастя, хтось це вже зробив для нас. Виявляється, що комбінація (.)
і id
утворює математичну конструкцію відому як категорію . Але є й інші способи формування категорій. Наприклад, категорія Клейслі дозволяє трохи доповнити об'єкти, що складаються. Категорія Kleisli для Maybe
складатиметься із (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
та id :: a -> Maybe a
. Тобто об’єкти в категорії збільшують (->)
a з a Maybe
, так і (a -> b)
стає (a -> Maybe b)
.
І раптом ми розширили силу композиції на речі, над якими традиційна (.)
операція не працює. Це джерело нової сили абстракції. Категорії Kleisli працюють з більшою кількістю типів, ніж просто Maybe
. Вони працюють з кожним типом, який може скласти належну категорію, дотримуючись закони категорій.
id . f
=f
f . id
=f
f . (g . h)
=(f . g) . h
Поки ви зможете довести, що ваш тип дотримується цих трьох законів, ви можете перетворити його на категорію Клейслі. І що в цьому великого? Ну, виявляється, монади - це саме те саме, що і категорії Клейслі. Monad
«И return
так же , як Клейслі id
. Monad
«S (>>=)
не збігається з Клейслі (.)
, але це виявляється дуже легко писати один з точки зору іншого. І закони категорій такі ж, як закони монад, коли ви перекладаєте їх через різницю між (>>=)
і (.)
.
То чому б пройти через усе це турбування? Чому виникає Monad
абстракція в мові? Як я вже нагадав вище, це дозволяє повторно використовувати код. Це навіть дозволяє повторно використовувати код у двох різних вимірах.
Перший вимір повторного використання коду відбувається безпосередньо від наявності абстракції. Ви можете написати код, який працює у всіх екземплярах абстракції. Є весь пакет монад-циклів, що складається з циклів, які працюють з будь-яким екземпляром Monad
.
Другий вимір - непрямий, але він випливає з існування композиції. Коли композиція проста, цілком природно писати код невеликими шматками для багаторазового використання. Це той самий спосіб, коли (.)
оператор функцій заохочує писати невеликі функції багаторазового використання.
То чому існує абстракція? Тому що це доведено як інструмент, який дозволяє більше складу в коді, в результаті чого створюється багаторазовий код і заохочується до створення більш багаторазового коду. Повторне використання коду є одним із святих граалів програмування. Абстракція монади існує тому, що вона трохи рухає нас до цього святого грааля.
newtype Kleisli m a b = Kleisli (a -> m b)
. Категорії Kleisli - це функції, де категоричний тип повернення ( b
в даному випадку) є аргументом конструктору типів m
. Iff Kleisli m
утворює категорію, m
є монадою.
Kleisli m
здається, утворює категорію, об'єктами якої є типи Haskell і такою, що стрілки від a
на b
- це функції від a
до m b
, з id = return
і (.) = (<=<)
. Це правильно, чи я змішую різні рівні речей чи щось таке?
a
і b
, але вони не прості функціями. Вони прикрашені додатковою m
в зворотному значенні функції.
Про це заявив Бенджамін Пірс у TAPL
Система типу може розглядатися як обчислення свого роду статичного наближення до поведінки термінів у програмі.
Ось чому мова, оснащена потужною системою типу, суворо виразніше, ніж погано набрана мова. Ви можете думати про монадів аналогічно.
Як @Carl та sigfpe , ви можете оснастити тип даних усіма потрібними вам операціями, не вдаючись до монад, класів типу чи будь-яких інших абстрактних матеріалів. Однак монади дозволяють вам не тільки писати код для багаторазового використання, але й абстрагувати всі зайві деталі.
Наприклад, скажімо, що ми хочемо відфільтрувати список. Найпростіший спосіб - використовувати filter
функцію:, filter (> 3) [1..10]
яка дорівнює [4,5,6,7,8,9,10]
.
Трохи складніша версія filter
, що також передає акумулятор зліва направо, є
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
Щоб отримати все i
, таке, що i <= 10, sum [1..i] > 4, sum [1..i] < 25
ми можемо написати
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
що дорівнює [3,4,5,6]
.
Або ми можемо перезначити nub
функцію, яка видаляє повторювані елементи зі списку, з точки зору filterAccum
:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4]
дорівнює [1,2,4,5,3,8,9]
. Список передається як акумулятор. Код працює, тому що можна залишити монаду списку, тому весь обчислення залишається чистим ( notElem
фактично не використовується >>=
, але він міг би). Однак не можна безпечно залишити монаду IO (тобто ви не можете виконати дію IO і повернути чисте значення - значення завжди буде обгорнуте монадою IO). Іншим прикладом є змінні масиви: після того, як ви покинули монаду ST, де живе змінений масив, ви більше не можете оновлювати масив у постійний час. Тому нам потрібна монадійна фільтрація з Control.Monad
модуля:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
filterM
виконує монадійну дію для всіх елементів зі списку, поступаючись елементам, для яких повертається монадійна дія True
.
Приклад фільтрації з масивом:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
відбитки, [1,2,4,5,3,8,9]
як очікувалося.
І версія з монадою IO, яка запитує, які елементи повернути:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
Напр
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
І як остаточну ілюстрацію filterAccum
можна визначити через filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
з StateT
монадою, яка використовується під кришкою, є просто звичайним типом даних.
Цей приклад ілюструє, що монади дозволяють не лише абстрагувати обчислювальний контекст і писати чистий код для багаторазового використання (за рахунок компонованості монад, як пояснює @Carl), але й одночасно обробляти визначені користувачем типи даних та вбудовані примітиви.
Я не думаю, що його IO
слід розглядати як особливо видатну монаду, але це, безумовно, одна з найбільш приголомшливих для початківців, тому я використаю це для свого пояснення.
Найпростіша можлива система вводу-виводу для чисто функціональної мови (і фактично тієї, з якої розпочався Haskell):
main₀ :: String -> String
main₀ _ = "Hello World"
З ледачою, цього простого підпису достатньо, щоб насправді створити інтерактивні термінальні програми - хоча це дуже обмежено. Найбільше засмучує те, що ми можемо виводити лише текст. Що робити, якщо ми додали ще кілька захоплюючих можливостей виведення?
data Output = TxtOutput String
| Beep Frequency
main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
-- , Beep 440 -- for debugging
]
мило, але, звичайно, набагато реалістичнішим "альтернативним результатом" було б написання файлу . Але тоді ви також хочете прочитати з файлів якийсь спосіб . Будь-який шанс?
Що ж, коли ми беремо нашу main₁
програму і просто передаємо файл до процесу (використовуючи засоби операційної системи), ми по суті реалізували читання файлів. Якби ми могли спровокувати це читання файлів з мови Haskell ...
readFile :: Filepath -> (String -> [Output]) -> [Output]
Це використовує "інтерактивну програму" String->[Output]
, подає їй рядок, отриманий з файлу, і дає неінтерактивну програму, яка просто виконує задану програму.
Тут є одна проблема: ми насправді не маємо поняття, коли файл читається. У цьому [Output]
списку впевнений наказ про результати , але ми не отримуємо замовлення про те, коли будуть зроблені входи .
Рішення: внесіть вхідні події також пункти в список речей, які потрібно зробити.
data IO₀ = TxtOut String
| TxtIn (String -> [Output])
| FileWrite FilePath String
| FileRead FilePath (String -> [Output])
| Beep Double
main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
[TxtOutput "Hello World"]
]
Гаразд, тепер ви можете помітити дисбаланс: ви можете прочитати файл і зробити висновок залежним від нього, але ви не можете використовувати вміст файлу, щоб вирішити, наприклад, також прочитати інший файл. Очевидне рішення: зробити результат вхідних подій також чимось типовим IO
, а не просто Output
. Це впевнено включає простий текст, але також дозволяє читати додаткові файли тощо.
data IO₁ = TxtOut String
| TxtIn (String -> [IO₁])
| FileWrite FilePath String
| FileRead FilePath (String -> [IO₁])
| Beep Double
main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
[TxtOut "Hello World"]
]
Це фактично дозволить вам виразити будь-яку операцію з файлом, яку ви хочете отримати в програмі (хоча, можливо, не з хорошою продуктивністю), але це дещо ускладнюється:
main₃
дає цілий список дій. Чому ми просто не використаємо підпис :: IO₁
, який має особливий випадок?
Список справді вже не дає надійного огляду потоку програми: більшість наступних обчислень буде "оголошено" лише в результаті деякої операції введення. Таким чином, ми можемо також скинути структуру списку і просто скласти "і тоді зробити" для кожної операції виводу.
data IO₂ = TxtOut String IO₂
| TxtIn (String -> IO₂)
| Terminate
main₄ :: IO₂
main₄ = TxtIn $ \_ ->
TxtOut "Hello World"
Terminate
Не дуже погано!
На практиці ви не хочете використовувати звичайні конструктори для визначення всіх своїх програм. Потрібно мати пару таких фундаментальних конструкторів, але для більшості матеріалів вищого рівня ми хотіли б написати функцію з гарним підписом високого рівня. Виявляється, більшість із них виглядатимуть досить схоже: прийміть якесь змістовно набране значення та отримаєте в результаті дії IO.
getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂
Очевидно, тут є візерунок, і ми краще запишемо його як
type IO₃ a = (a -> IO₂) -> IO₂ -- If this reminds you of continuation-passing
-- style, you're right.
getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)
Тепер це починає виглядати знайомим, але ми все ще маємо справу лише з тонко замаскованими простими функціями під кришкою, і це ризиковано: кожна «дія-значення» несе відповідальність за фактичне передавання отриманої дії будь-якої міститься функції (інше потік управління всією програмою легко порушується однією недоброзичливою дією посередині). Ми краще зробимо цю вимогу явною. Ну, виявляється, це закони монад , хоча я не впевнений, що ми можемо реально їх сформулювати без стандартних операторів зв'язування / приєднання.
У будь-якому випадку ми зараз дійшли до формулювання IO, що має належний монадний екземпляр:
data IO₄ a = TxtOut String (IO₄ a)
| TxtIn (String -> IO₄ a)
| TerminateWith a
txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()
txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith
instance Functor IO₄ where
fmap f (TerminateWith a) = TerminateWith $ f a
fmap f (TxtIn g) = TxtIn $ fmap f . g
fmap f (TxtOut s c) = TxtOut s $ fmap f c
instance Applicative IO₄ where
pure = TerminateWith
(<*>) = ap
instance Monad IO₄ where
TerminateWith x >>= f = f x
TxtOut s c >>= f = TxtOut s $ c >>= f
TxtIn g >>= f = TxtIn $ (>>=f) . g
Очевидно, що це не ефективна реалізація IO, але вона в принципі корисна.
IO3 a ≡ Cont IO2 a
. Але я мав на увазі цей коментар більше як кивок для тих, хто вже знає монаду про продовження, оскільки він точно не має репутації як сприятливого для початківців.
Монади - це просто зручна основа для вирішення класу повторюваних проблем. По-перше, монади повинні бути функторами (тобто повинні підтримувати відображення, не дивлячись на елементи (або їх тип)), вони також повинні принести операцію прив'язки (або ланцюжка) та спосіб створити монадичне значення з типу елемента ( return
). Нарешті, bind
і return
має задовольняти двом рівнянням (ліва і права тотожність), які також називаються законами монад. (Крім того, можна визначити монади, щоб мати aflattening operation
замість зв'язування.)
Список монада зазвичай використовуються для боротьби з Індетермінізм. Операція зв’язування вибирає один елемент списку (інтуїтивно їх усі в паралельних світах ), дозволяє програмісту робити деякі обчислення з ними, а потім об'єднує результати у всіх світах до одного списку (шляхом об'єднання чи вирівнювання вкладеного списку ). Ось як можна було б визначити функцію перестановки в монадійних рамках Haskell:
perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
let shortened = take index l ++ drop (index + 1) l
trailer <- perm shortened
return (leader : trailer)
Ось приклад сеансу відбиття :
*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]
Слід зазначити, що монада списку аж ніяк не є стороною, що впливає на обчислення. Математична структура, яка є монадою (тобто відповідає вищезгаданим інтерфейсам і законам), не передбачає побічних ефектів, хоча побічні явища часто добре вписуються в монадійні рамки.
Монади служать в основному для складання функцій разом у ланцюжку. Період.
Тепер спосіб їх складання відрізняється від існуючих монад, внаслідок чого виникає різна поведінка (наприклад, для імітації змінного стану в монаді стану).
Плутанина в монадах полягає в тому, що будучи настільки загальним, тобто механізмом складання функцій, їх можна використовувати для багатьох речей, тим самим змушуючи людей вірити в те, що монади - це про стан, про IO тощо, коли йдеться лише про "композиційні функції" ".
Тепер одна цікава річ про монади - це те, що результат композиції завжди має тип "M a", тобто значення всередині конверта з позначкою "M". Ця функція виявляється дуже приємною для здійснення, наприклад, чіткого поділу між чистим від нечистого коду: оголосити всі нечисті дії як функції типу "IO a" і не надавати жодної функції при визначенні монади IO, щоб вийняти " значення "всередині" IO a ". Результат полягає в тому, що жодна функція не може бути чистою і одночасно виводити значення з "IO a", оскільки немає можливості приймати таке значення, залишаючись чистим (функція повинна бути всередині монади "IO", щоб використовувати таке значення). (ПРИМІТКА: ну, нічого не ідеально, тому "IO straitjacket" можна зламати за допомогою "unsafePerformIO: IO a -> a"
Вам потрібні монади, якщо у вас є конструктор типів і функції, які повертають значення цього сімейства типів . Зрештою, ви хочете поєднати такі функції разом . Це три ключові елементи, щоб відповісти, чому .
Дозвольте мені детальніше. Ви маєте Int
, String
і Real
і функції типу Int -> String
, String -> Real
і так далі. Ви можете легко комбінувати ці функції, закінчуючи Int -> Real
. Життя чудове.
Потім, одного дня, вам потрібно створити нове сімейство типів . Це може бути тому, що вам потрібно врахувати можливість повернення значення ( Maybe
), повернення помилки ( Either
), декількох результатів (List
) тощо.
Зверніть увагу, що Maybe
це конструктор типу. Він приймає тип, як Int
і повертає новий тип Maybe Int
. Перше, що потрібно пам’ятати, ні конструктор типів, ні монада.
Звичайно, ви хочете використовувати у своєму коді конструктор типів , і незабаром ви закінчите такі функції, як Int -> Maybe String
іString -> Maybe Float
. Тепер ви не можете легко поєднувати свої функції. Життя вже не добре.
І ось коли на допомогу приходять монади. Вони дозволяють знову поєднувати такі функції. Потрібно просто змінити склад . для > == .