Использование монады состояния для скрытия явного состояния

Я пытаюсь написать небольшую игру на Haskell, и нужно передать изрядное количество состояний. Я хочу попытаться скрыть состояние с помощью монады состояния

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

В одной части моего кода у меня есть такая строка:

let player = getCurrentPlayer state

Я бы хотел, чтобы он не принимал состояние, а вместо этого писал

player <- getCurrentPlayerM

в настоящее время его реализация выглядит так

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

и казалось достаточно простым, чтобы заставить его работать в монаде State, написав это так:

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

Однако это вызывает жалобы со стороны ghc! В нем говорится, что нет экземпляра для (MonadState GameState m0), возникающего из-за использования `get '. Я уже переписал очень похожую функцию, за исключением того, что она не была нулевой в форме монады состояния, поэтому, догадываясь, я переписал ее следующим образом:

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

Вам также следует изучить возможность использования линз с Монады состояний, что позволяет писать очень чистый код для неявной передачи состояния.

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