Използване на монадата на състоянието за скриване на изрично състояние

Опитвам се да напиша малка игра на Haskell и има доста голямо количество състояние, необходимо за предаване. Искам да опитам да скрия състоянието с монадата State

Сега се натъкнах на проблем: функции, които приемат състояние и аргумент, бяха лесни за писане, за да работят в монада състояние. Но има и функции, които просто приемат състоянието като аргумент (и връщат модифицирано състояние или евентуално нещо друго).

В една част от моя код имам този ред:

let player = getCurrentPlayer state

Бих искал да не приема състояние, а вместо това да пише

player <- getCurrentPlayerM

в момента изпълнението му изглежда така

getCurrentPlayer gameState = 
  (players gameState) ! (on_turn gameState)

и изглеждаше достатъчно просто да го накарам да работи в държавната монада, като го напишем така:

getCurrentPlayerM = do state <- get
                       return (players state ! on_turn state)

Това обаче предизвиква оплаквания от ghc! Няма екземпляр за (MonadState GameState m0), произтичащ от използване на `get', се казва. Вече бях пренаписал много подобна функция, с изключение на това, че не беше nullary във формата си State monad, така че по предчувствие я пренаписах така:

getCurrentPlayerM _ = do state <- get
                         return (players state ! on_turn state)

И разбира се, работи! Но, разбира се, трябва да го наричам getCurrentPlayerM () и се чувствам малко глупаво, като го правя. Преминаването в спор беше това, което исках да избегна на първо място!

Допълнителна изненада: като гледам вида му в ghci, получавам

getCurrentPlayerM :: MonadState GameState m => t -> m P.Player

но ако се опитам да задам това изрично в моя код, получавам друга грешка: „Аргумент без променлива тип в ограничението MonadState GameState m“ и предложение за езиково разширение, което да го разреши. Предполагам, че това е така, защото моят GameState е тип, а не тип клас, но защо е приет на практика, но не и когато се опитвам да бъда изричен за него, съм по-объркан.

Така че да обобщим:

  1. Защо не мога да пиша нулеви функции в монадата State?
  2. Защо не мога да декларирам типа, който моята заобиколна функция всъщност има?

person Community    schedule 02.06.2012    source източник


Отговори (1)


Проблемът е, че не пишете сигнатури на типове за вашите функции и се прилага ограничението за мономорфизъм.

Когато пишете:

getCurrentPlayerM = ...

вие пишете дефиниция на унарна ограничена стойност от най-високо ниво без декларация на тип, така че компилаторът на Haskell ще се опита да изведе тип за дефиницията. Въпреки това, ограничението за мономорфизъм (буквално: ограничение за единична форма) гласи, че всички дефиниции от най-високо ниво с ограничения на изведен тип трябва да се разрешават до конкретни типове, т.е. те не трябва да бъдат полиморфни.


За да обясня какво имам предвид, вземете този по-прост пример:

pi = 3.14

Тук дефинираме pi без тип, така че GHC извежда типа Fractional a => a, т.е. "всеки тип a, стига да може да се третира като дроб." Този тип обаче е проблематичен, защото означава, че pi не е константа, въпреки че изглежда така. Защо? Тъй като стойността на pi ще бъде преизчислена в зависимост от това какъв тип искаме да бъде.

Ако имаме (2::Double) + pi, pi ще бъде Double. Ако имаме (3::Float) + pi, pi ще бъде Float. Всеки път, когато се използва pi, той трябва да бъде изчислен отново (защото не можем да съхраняваме алтернативни версии на pi за всички възможни дробни типове, нали?). Това е добре за простия литерал 3.14, но какво ще стане, ако искаме повече десетични знаци от pi и използваме фантастичен алгоритъм, който го изчислява? Тогава не бихме искали да се изчислява отново всеки път, когато се използва pi, нали?

Ето защо в доклада на Haskell се посочва, че дефинициите с унарен тип от най-високо ниво трябва да имат един тип (мономорфен), за да се избегне този проблем. В този случай pi ще получи типа default от Double. Можете да промените цифровите типове по подразбиране, ако желаете, като използвате ключовата дума default:

default (Int, Float)

pi = 3.14 -- pi will now be Float

Във вашия случай обаче получавате предполагаемия подпис:

getCurrentPlayerM :: MonadState GameState m => m P.Player

Това означава: „За всяка монада със състояние, която съхранява GameStates, извлечете играч.“ Въпреки това, тъй като се прилага ограничението за мономорфизъм, Haskell е принуден да се опита да направи този тип неполиморфен, като избере конкретен тип за m. Въпреки това, той не може да намери такъв, тъй като няма тип по подразбиране за монади на състояние, както има за числа, така че се отказва.

Или искате да дадете на функцията си изричен подпис на типа:

getCurrentPlayerM :: MonadState GameState m => m P.Player

... но ще трябва да добавите езиковото разширение FlexibleContexts Haskell, за да работи, като добавите това в горната част на вашия файл:

{-# LANGUAGE FlexibleContexts #-}

Или можете да посочите изрично коя монада на състоянието искате:

getCurrentPlayerM :: State GameState P.Player

Можете също да деактивирате ограничението за мономорфизъм, като добавите разширението за това; много по-добре е обаче да добавите подписи на типове.

{-# LANGUAGE NoMonomorphismRestriction #-}

PS. Ако имате функция, която приема вашето състояние като параметър, можете да използвате:

value <- gets getCurrentPlayer

Трябва също така да обмислите използването на обективи с State monads, което ви позволява да пишете много чист код за имплицитно предаване на състояние.

person dflemstr    schedule 02.06.2012
comment
Изричното посочване коя монада искам изглежда е правилното решение в моя случай. Да, вече се сблъсках с проблема със записите в записите и прочетох малко за обективите (наред с други неща, когато търсих отговора тук; изглежда, че се препоръчва много!). Благодаря, страхотни обяснения! - person ; 03.06.2012