Яка мета читацької монади?


122

Читальна монада настільки складна і здається марною. У такій необхідній мові, як Java або C ++, немає рівнозначної концепції для монади читача, якщо я не помиляюся.

Чи можете ви надати простий приклад і трохи прояснити це?


21
Ви використовуєте монадію читача, якщо хочете, коли ви хочете - читати деякі значення з (немодифікуючого) середовища, але не хочете явно передавати це середовище навколо. У Java або C ++ ви б використовували глобальні змінні (хоча це не зовсім те саме).
Даніель Фішер

5
@Daniel: Це звучить дуже жахливо, як відповідь
SingleNegationElimination

@TokenMacGuy Занадто короткий для відповіді, і зараз мені вже пізно придумати щось довше. Якщо ніхто більше не зробить, я буду після того, як я спав.
Даніель Фішер

8
У Java або C ++ монада Reader буде аналогічною параметрам конфігурації, переданим об'єкту в його конструкторі, які ніколи не змінюються протягом життя об'єкта. У Clojure це було б трохи схоже на змінну динамічного діапазону, яка використовується для параметризації поведінки функції, не потребуючи явної передачі її в якості параметра.
danidiaz

Відповіді:


169

Не лякайтеся! Монада для читання насправді не така складна, і має справжню просту у використанні утиліту.

Є два способи наближення монади: ми можемо запитати

  1. Що монада робити ? Якими операціями він оснащений? Для чого це добре?
  2. Як здійснюється монада? Звідки воно виникає?

З першого підходу читацька монада є деяким абстрактним типом

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є лише функціональна композиція з порядком перемикання функцій!


6
Дуже цікава відповідь. Чесно кажучи, я читав її знову багато разів, коли хочу переглянути монаду. До речі, щодо алгоритму nagamax "можливі значення <- mapM (negate. Negamax (negate color))" здаються невірними. Я знаю, що код, який ви надаєте, - лише для того, щоб показати, як працює монада читача. Але якщо у вас є час, ви могли б виправити код алгоритму negamax? Тому що цікаво, коли ви використовуєте читацьку монаду для вирішення negamax.
chipbk10

4
Так Readerце функція з якоюсь конкретною реалізацією класу типу monad? Якщо сказати це раніше, це допомогло б мені спантелитити трохи менше. Спочатку я цього не отримував. На півдорозі я подумав: "О, це дозволяє повернути щось, що дасть бажаний результат, як тільки ви надасте відсутнє значення". Я думав, що це корисно, але раптом зрозумів, що функція робить саме це.
ziggystar

1
Прочитавши це, я розумію більшість із цього. Хоча для цієї localфункції потрібно ще пояснення ..
Крістоф Де Троєр

@Philip У мене питання про екземпляр Monad. Чи не можемо ми записати функцію прив’язки як (Reader f) >>= g = (g (f x))?
zeronone

@zeronone де є x?
Ашиш Негі

56

Я пам’ятаю, як ви були спантеличені, як і ви, доки я самостійно не виявив, що варіанти монади Читача є скрізь . Як я це відкрив? Тому що я продовжував писати код, який виявився на ньому невеликими варіантами.

Наприклад, я в один момент писав якийсь код, щоб розібратися з історичними цінностями; значення, які змінюються з часом. Дуже проста модель цього - функції від моменту часу до значення на той момент часу:

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екземпляри - це основні зразки роботи з Readers. Functor= make a, Readerщо модифікує вихід іншого Reader; Applicative= підключіть два Readers до одного входу та об'єднайте їхні виходи; Monad= перевірити результат а Readerта використовувати його для побудови іншого Reader. The localі withReaderфункції = make a, Readerщо змінює вхід на інший Reader.


5
Чудова відповідь. Ви можете також використовувати GeneralizedNewtypeDerivingрозширення для виведення Functor, Applicative, Monadі т.д. для ньютайпов на основі базових їх типів.
Рейн Генріхс

20

У Java або C ++ ви можете отримати доступ до будь-якої змінної з будь-якої точки без будь-яких проблем. Проблеми з’являються, коли ваш код стає багатопоточним.

У Haskell у вас є лише два способи передавати значення з однієї функції в іншу:

  • Ви передаєте значення через один із вхідних параметрів функції дзвінка. Недоліками є: 1) ви не можете передати ВСІ змінні таким чином - список вхідних параметрів просто підірве ваш розум. 2) в послідовності викликів функції: fn1 -> fn2 -> fn3функція fn2може не потребувати параметра, з якого ви переходите fn1до fn3.
  • Ви передаєте значення в межах деякої монади. Недолік: ви повинні чітко зрозуміти, що таке концепція Монада. Передача значень - це лише одне із безлічі застосунків, де ви можете використовувати Monads. Насправді концепція Монада неймовірно потужна. Не засмучуйтесь, якщо ви відразу не зрозуміли. Просто намагайтеся і читайте різні підручники. Знання, які ви отримаєте, окупляться.

Монітор Reader просто передає дані, якими ви хочете поділитися між функціями. Функції можуть читати ці дані, але не можуть їх змінити. Ось і все, що робить монарда Читача. Ну, майже всі. Існує також ряд функцій на кшталт local, але вперше ви можете дотримуватися asksлише цього.


3
Подальшим недоліком використання монад для неявного передавання даних є те, що дуже просто знайти себе в написанні безлічі коду «імперативного стилю» в doпримітці, що було б краще перетворитись на чисту функцію.
Бенджамін Ходжсон

4
@ BenjaminHodgson Написання коду з "імперативним виглядом" з монадами в до-примітці не означає, що слід писати побічний (нечистий) код. Насправді, побічний код у Haskell може бути можливий лише в монаді IO.
Дмитро Беспалов

Якщо інша функція додається до цієї за допомогою whereпункту, чи буде вона прийнята як третій спосіб передачі змінних?
Elmex80s
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.