Читальна монада настільки складна і здається марною. У такій необхідній мові, як Java або C ++, немає рівнозначної концепції для монади читача, якщо я не помиляюся.
Чи можете ви надати простий приклад і трохи прояснити це?
Читальна монада настільки складна і здається марною. У такій необхідній мові, як Java або C ++, немає рівнозначної концепції для монади читача, якщо я не помиляюся.
Чи можете ви надати простий приклад і трохи прояснити це?
Відповіді:
Не лякайтеся! Монада для читання насправді не така складна, і має справжню просту у використанні утиліту.
Є два способи наближення монади: ми можемо запитати
З першого підходу читацька монада є деяким абстрактним типом
data Reader env a
такий, що
-- Reader is a monad
instance Monad (Reader env)
-- and we have a function to get its environment
ask :: Reader env env
-- finally, we can run a Reader
runReader :: Reader env a -> env -> a
То як ми цим користуємося? Ну, читацька монада хороша для передачі (неявної) інформації конфігурації через обчислення.
Кожен раз, коли у вас є "константа" в обчисленні, яка вам потрібна в різних точках, але дійсно ви хотіли б мати можливість виконувати однакові обчислення з різними значеннями, тоді вам слід використовувати монаду читача.
Читальні монади також використовуються, щоб робити те, що люди називають введенням залежності . Наприклад, алгоритм negamax використовується часто (у високооптимізованих формах) для обчислення значення позиції в грі з двома гравцями. Сам алгоритм хоч і не байдуже, в яку гру ви граєте, за винятком того, що вам потрібно вміти визначати, які "наступні" позиції в грі, і вам потрібно вміти визначати, чи є поточна позиція перемогою.
import Control.Monad.Reader
data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie
data Game position
= Game {
getNext :: position -> [position],
getState :: position -> GameState
}
getNext' :: position -> Reader (Game position) [position]
getNext' position
= do game <- ask
return $ getNext game position
getState' :: position -> Reader (Game position) GameState
getState' position
= do game <- ask
return $ getState game position
negamax :: Double -> position -> Reader (Game position) Double
negamax color position
= do state <- getState' position
case state of
FirstPlayerWin -> return color
SecondPlayerWin -> return $ negate color
Tie -> return 0
NotOver -> do possible <- getNext' position
values <- mapM ((liftM negate) . negamax (negate color)) possible
return $ maximum values
Потім це буде працювати з будь-якою кінцевою, детермінованою двома гравцями.
Ця модель корисна навіть для речей, які насправді не є ін'єкцією залежності. Припустимо, що ви працюєте у фінансах, ви можете розробити складну логіку для ціноутворення активу (похідне слово), що все добре і добре, і ви можете обійтися без будь-яких смердючих монад. Але потім ви модифікуєте свою програму для роботи з кількома валютами. Потрібно мати можливість конвертувати між валютами на ходу. Ваша перша спроба - визначити функцію верхнього рівня
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
щоб отримати спотові ціни. Ви можете зателефонувати цьому словнику у свій код .... але зачекайте! Це не вийде! Словник валюти є незмінним, тому він повинен бути однаковим не лише протягом вашої програми, але і з моменту його складання ! Так, що ти робиш? Що ж, одним із варіантів було б використання монарда Reader:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Мабуть, самий класичний варіант використання - це втілення перекладачів. Але, перш ніж ми розглянемо це, нам потрібно ввести ще одну функцію
local :: (env -> env) -> Reader env a -> Reader env a
Гаразд, тому Haskell та інші функціональні мови базуються на обчисленні лямбда . Обчислення лямбда має синтаксис, який виглядає так
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
і ми хочемо написати оцінювача для цієї мови. Для цього нам потрібно буде відслідковувати середовище, що є переліком прив’язок, пов’язаних із термінами (насправді це буде закриття, тому що ми хочемо зробити статичне оцінювання).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Коли ми закінчимо, нам слід отримати значення (або помилку):
data Value = Lam String Closure | Failure String
Отже, давайте запишемо перекладача:
interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
= do env <- ask
return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
= do (Env env) <- ask
case lookup (show v) env of
-- if it is not in the environment we have a problem
Nothing -> return . Failure $ "unbound variable: " ++ (show v)
-- if it is in the environment, then we should interpret it
Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
= do v1 <- interp' t1
case v1 of
Failure s -> return (Failure s)
Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!
Нарешті, ми можемо використовувати його, передаючи тривіальне середовище:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
І це все. Повністю функціональний перекладач обчислення лямбда.
Інший спосіб подумати над цим - запитати: як це реалізується? Відповідь полягає в тому, що читацька монада - це насправді одна з найпростіших та найелегантніших монард.
newtype Reader env a = Reader {runReader :: env -> a}
Зчитувач - просто фантазія назви функцій! Ми вже визначилися, runReader
що робити з іншими частинами API? Що ж, Monad
це також Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Тепер, щоб отримати монаду:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
що не так страшно. ask
дійсно просто:
ask = Reader $ \x -> x
поки local
це не так погано:
local f (Reader g) = Reader $ \x -> runReader g (f x)
Гаразд, тому читацька монада - це лише функція. Чому взагалі читач? Гарне питання. Насправді вам це не потрібно!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Вони ще простіші. Більше того, ask
це просто id
і local
є лише функціональна композиція з порядком перемикання функцій!
Reader
це функція з якоюсь конкретною реалізацією класу типу monad? Якщо сказати це раніше, це допомогло б мені спантелитити трохи менше. Спочатку я цього не отримував. На півдорозі я подумав: "О, це дозволяє повернути щось, що дасть бажаний результат, як тільки ви надасте відсутнє значення". Я думав, що це корисно, але раптом зрозумів, що функція робить саме це.
local
функції потрібно ще пояснення ..
(Reader f) >>= g = (g (f x))
?
x
?
Я пам’ятаю, як ви були спантеличені, як і ви, доки я самостійно не виявив, що варіанти монади Читача є скрізь . Як я це відкрив? Тому що я продовжував писати код, який виявився на ньому невеликими варіантами.
Наприклад, я в один момент писав якийсь код, щоб розібратися з історичними цінностями; значення, які змінюються з часом. Дуже проста модель цього - функції від моменту часу до значення на той момент часу:
import Control.Applicative
-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }
instance Functor (History t) where
-- Apply a function to the contents of a historical value
fmap f hist = History (f . observe hist)
instance Applicative (History t) where
-- A "pure" History is one that has the same value at all points in time
pure = History . const
-- This applies a function that changes over time to a value that also
-- changes, by observing both at the same point in time.
ff <*> fx = History $ \t -> (observe ff t) (observe fx t)
instance Monad (History t) where
return = pure
ma >>= f = History $ \t -> observe (f (observe ma t)) t
У Applicative
разі означає , що якщо у вас є employees :: History Day [Person]
і customers :: History Day [Person]
ви можете зробити це:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
Тобто, Functor
і Applicative
дозволяємо нам адаптувати регулярні неісторичні функції для роботи з історіями.
Екземпляр монади найбільш інтуїтивно зрозумілий з огляду на функцію (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. Тип типу a -> History t b
- це функція, яка відображає a
історію b
значень; наприклад, ви могли б мати getSupervisor :: Person -> History Day Supervisor
і getVP :: Supervisor -> History Day VP
. Отже, екземпляр Monad for History
полягає у складанні таких функцій; наприклад, getSupervisor >=> getVP :: Person -> History Day VP
це функція, яка отримує для будь-якої Person
історії історію VP
, яку вони мали.
Ну, ця History
монада насправді точно така ж, як Reader
. History t a
насправді те саме, що Reader t a
(що таке саме t -> a
).
Ще один приклад: нещодавно я прототипував проекти OLAP в Haskell. Однією з них є ідея "гіперкуба", який є відображенням від перетинів набору розмірів до значень. Ось ми знову:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Однією з поширених операцій на гіперкубах є застосування декількох скалярних функцій до відповідних точок гіперкуба. Це ми можемо отримати, визначивши Applicative
примірник для Hypercube
:
instance Functor (Hypercube intersection) where
fmap f cube = Hypercube (f . get cube)
instance Applicative (Hypercube intersection) where
-- A "pure" Hypercube is one that has the same value at all intersections
pure = Hypercube . const
-- Apply each function in the @ff@ hypercube to its corresponding point
-- in @fx@.
ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)
Я просто скопіював History
код вище та змінив імена. Як ви можете сказати, Hypercube
теж просто Reader
.
Це продовжується і продовжується. Наприклад, мовні перекладачі також Reader
застосовуються до цієї моделі:
Reader
ask
Reader
середовище виконання.local
Хороша аналогія полягає в тому, що а Reader r a
являє собою a
"дірки" в ньому, які заважають вам знати, про що a
ми говоримо. Дійсний a
ви можете отримати лише після того, як ви поставите послугу r
для заповнення дірок. Є багато подібних речей. У наведених вище прикладах "історія" - це значення, яке неможливо обчислити, поки ви не вкажете час, гіперкуб - це значення, яке неможливо обчислити, поки ви не вкажете перетин, а вираз мови - це значення, яке може не слід обчислювати, поки ви не надасте значення змінних. Це також дає вам інтуїцію щодо того, що Reader r a
таке саме r -> a
, оскільки така функція також інтуїтивно a
відсутня r
.
Таким чином Functor
, Applicative
і Monad
випадки Reader
є дуже корисним узагальненням для випадків, коли ви моделюєте що-небудь подібне "того, a
чого не вистачає r
", і дозволяють вам ставитися до цих "неповних" об'єктів так, ніби вони були завершеними.
Ще один спосіб сказати те саме: a Reader r a
- це те, що споживає r
та виробляє a
, а Functor
, Applicative
і Monad
екземпляри - це основні зразки роботи з Reader
s. Functor
= make a, Reader
що модифікує вихід іншого Reader
; Applicative
= підключіть два Reader
s до одного входу та об'єднайте їхні виходи; Monad
= перевірити результат а Reader
та використовувати його для побудови іншого Reader
. The local
і withReader
функції = make a, Reader
що змінює вхід на інший Reader
.
GeneralizedNewtypeDeriving
розширення для виведення Functor
, Applicative
, Monad
і т.д. для ньютайпов на основі базових їх типів.
У Java або C ++ ви можете отримати доступ до будь-якої змінної з будь-якої точки без будь-яких проблем. Проблеми з’являються, коли ваш код стає багатопоточним.
У Haskell у вас є лише два способи передавати значення з однієї функції в іншу:
fn1 -> fn2 -> fn3
функція fn2
може не потребувати параметра, з якого ви переходите fn1
до fn3
.Монітор Reader просто передає дані, якими ви хочете поділитися між функціями. Функції можуть читати ці дані, але не можуть їх змінити. Ось і все, що робить монарда Читача. Ну, майже всі. Існує також ряд функцій на кшталт local
, але вперше ви можете дотримуватися asks
лише цього.
do
примітці, що було б краще перетворитись на чисту функцію.
where
пункту, чи буде вона прийнята як третій спосіб передачі змінних?