Haskell - монада состояний - признак императивного мышления?

Пишу простую игру - Тетрис. Впервые в жизни я использую функциональное программирование для этой цели, в качестве языка я выбрал Haskell. Однако я заражен ООП и императивным мышлением и боюсь неосознанно применять этот образ мышления в моей программе на Haskell.

Где-то в моей игре мне нужна информация о прошедшем времени (таймер) и нажатых / нажатых клавишах (клавиатура). Подход, используемый в уроках SDL, переведенных на Haskell, выглядит так:

Main.hs

data AppData = AppData {
    fps :: Timer 
    --some other fields    
}

getFPS :: MonadState AppData m => m Timer
getFPS = liftM fps get

putFPS :: MonadState AppData m => Timer -> m ()
putFPS t = modify $ \s -> s { fps = t }

modifyFPSM :: MonadState AppData m => (Timer -> m Timer) -> m ()
modifyFPSM act = getFPS >>= act >>= putFPS

Timer.hs

data Timer = Timer { 
    startTicks :: Word32,
    pausedTicks :: Word32,
    paused :: Bool,
    started :: Bool
}

start :: Timer -> IO Timer
start timer = SdlTime.getTicks >>= \ticks -> return $ timer { startTicks=ticks, started=True,paused=False }

isStarted :: Timer -> Bool
isStarted Timer { started=s } = s

А потом использовал вот так: modifyFPSM $ liftIO . start. Это делает Timer в некоторой степени чистым (это не явно монада, и его функции возвращают ввод-вывод только потому, что он требуется для измерения времени). Однако это засоряет код вне модуля Timer геттерами и сеттерами.

Мой подход, используемый в Keyboard.hs:

data KeyboardState = KeyboardState {
    keysDown :: Set SDLKey, -- keys currently down
    keysPressed :: Set SDLKey -- keys pressed since last reset 
};

reset :: MonadState KeyboardState m => m ()
reset = get >>= \ks -> put ks{keysPressed = Data.Set.empty} 

keyPressed :: MonadState KeyboardState m => SDLKey -> m ()
keyPressed key = do
     ks <- get 
     let newKeysPressed = Data.Set.insert key $ keysPressed ks
     let newKeysDown = Data.Set.insert key $ keysDown ks
     put ks{keysPressed = newKeysPressed, keysDown = newKeysDown}

keyReleased :: MonadState KeyboardState m => SDLKey -> m ()
keyReleased key = do
     ks <- get 
     let newKeysDown = Data.Set.delete key $ keysDown ks
     put ks{keysDown = newKeysDown}

Это делает модуль самодостаточным, но я боюсь, что это мой способ выразить объект из ООП в Haskell и разрушить всю суть FP. Итак, мой вопрос:

Как правильно это сделать? Или как еще можно подойти к такой ситуации? И если вы заметите какие-либо другие недостатки (будь то проблемы с дизайном или стилем), не стесняйтесь указать на это.


person PL_kolek    schedule 27.12.2013    source источник
comment
Некоторые мысли, не связанные с вашим вопросом: 1. Не думайте, что монада - это антоним чистого. Фактически, весь (правильный) монадический код должен быть чистым. IO объясняет, как построить нечистую программу, но сами IO значения чистые. 2. В большинстве таймеров нет смысла говорить, что он приостановлен, если он не был запущен. Чтобы исключить это как возможное состояние, в котором может находиться значение, вы можете заменить эти два поля одним полем, содержащим что-то вроде data TimerStatus = Stopped | Running | Paused.   -  person Carl    schedule 27.12.2013
comment
Функциональное реактивное программирование - это наиболее функциональный подход к тому, что вы делаете. Если вы хотите научиться думать по-новому, перейдите на Haskell и выполните FRP. По возможности используйте аппликативный синтаксис вместо монадического синтаксиса. Используйте функции высшего порядка и бесточечный стиль, когда он не запутан.   -  person not my job    schedule 28.12.2013
comment
Я рассматривал FRP, но решил не погружаться так глубоко в FP в моем первом проекте. Понимания Haskell достаточно для написания игры, а Haskell + FRP - слишком много для начала.   -  person PL_kolek    schedule 28.12.2013


Ответы (2)


В большинстве программ есть какое-то понятие состояния. Так что вам не нужно беспокоиться каждый раз, когда вы используете монаду State в той или иной форме. Это все еще чисто функция, поскольку вы, по сути, пишете

Arg1 -> Arg2 -> State -> (State, Result)

Но вместо того, чтобы писать свои комбинаторы монады состояния, вместо этого подумайте о том, чтобы написать их как простые чистые функции, а затем использовать modify для внедрения их в монаду состояний.

reset :: KeyBoard -> KeyBoard
keyPressed :: Key -> KeyBoard -> KeyBoard
...

И затем, когда вам действительно нужно состояние, их легко использовать

 do
   nextKey <- liftIO $ magic
   modify $ keyPressed nextKey

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

TL; DR: небольшое состояние - это неплохо и даже может облегчить понимание кода, но перетаскивать его в каждую часть вашего кода - плохо.

person Daniel Gratzer    schedule 27.12.2013

Вопреки распространенному мнению, философия Haskell заключается не в устранении состояния, а в том, чтобы сделать состояние явным, инкапсулировать его и контролировать. Не стесняйтесь использовать монаду состояния сколько душе угодно, если это сделает ваш код более понятным.

Haskell очень хорош в абстракции и позволит вам выражать концепции, которые вы хотите использовать в своей игре, на более высоком уровне, чем то, что у вас здесь. Вы, вероятно, захотите изучить «Функциональное реактивное программирование».

person Tom Ellis    schedule 27.12.2013
comment
Тогда чем он отличается, например, от Java? Инкапсуляция является частью ООП, и ее цель - контролировать состояние. Когда я создал этот модуль Keyboard, он был слишком похож на обычный объект Java с установщиками и состоянием, инкапсулированным в монаду State. - person PL_kolek; 27.12.2013
comment
Хороший вопрос. ООП возникло как еще один подход с этой целью. На мой взгляд, философия Haskell более удовлетворительна, потому что она делает состояние явным. Я отредактировал свой ответ, чтобы упомянуть ясность состояния. Вы, вероятно, также хотели бы изучить FRP. - person Tom Ellis; 27.12.2013