Haskell и State
Haskell - это чистый функциональный язык программирования.
Мой вопрос:
Каковы преимущества и недостатки использования Haskell для решения проблем, связанных с большим количеством состояний, например, программирования графического интерфейса или программирования игр?
Также вторичный вопрос: какие методы должны обрабатывать состояние функциональным способом?
Спасибо заранее.
Ответы
Ответ 1
Сначала я отвечу на ваш второй вопрос. На самом деле существует множество способов обработки изменчивого состояния в Haskell (и других языках FP). Прежде всего, Haskell поддерживает изменчивое состояние в IO, через конструкции IORef
и mvar
. Используя их, они будут хорошо знакомы программистам с императивных языков. Существуют также специализированные версии, такие как STRef
и TMVar
, а также изменяемые массивы, указатели и различные другие изменяемые данные. Самый большой недостаток состоит в том, что они обычно доступны только в IO или более специализированной монаде.
Самый распространенный способ моделирования состояния на функциональном языке - это явно передать состояние как аргумент функции и возвращаемое значение. Например:
randomGen :: Seed -> (Int, Seed)
Здесь randomGen
принимает параметр seed и возвращает новое семя. Каждый раз, когда вы его вызываете, вам нужно следить за семенем для следующей итерации. Этот метод всегда доступен для передачи состояния, но он быстро становится утомительным.
Вероятно, наиболее распространенным подходом Haskell является использование монады для инкапсуляции этого состояния. Мы можем заменить randomGen
следующим образом:
-- a Random monad is simply a Seed value as state
type Random a = State Seed a
randomGen2 :: Random Int
randomGen2 = do
seed <- get
let (x,seed') = randomGen seed
put seed'
return x
Теперь любые функции, которые нуждаются в PRNG, могут работать в монаде Random, чтобы запросить их по мере необходимости. Вам просто нужно предоставить начальное состояние и вычисление.
runRandomComputation :: Random a -> Seed -> a
runRandomComputation = evalState
(обратите внимание, что есть функции, которые значительно сокращают определение randomGen2, я выбрал наиболее явную версию).
Если вашему случайному вычислению также нужен доступ к IO
, вы используете версию трансформатора монады состояния, StateT
.
Особо следует отметить монаду ST
, которая по существу обеспечивает механизм инкапсуляции IO-специфических мутаций в сторону от остальной части IO. Монада ST предоставляет STRefs, которые являются изменяемой ссылкой на данные, а также изменяемыми массивами. Используя ST, можно определить такие вещи:
randomList :: Seed -> [Int]
где [Int] - это бесконечный список случайных чисел (он будет в конечном счете зависеть от вашего PSRG) от исходного семени, которое вы ему даете.
Наконец, Функциональное реактивное программирование. Вероятно, самые известные библиотеки для этого - Yampa и Reactive, но другие заслуживают внимания. Существует несколько подходов к изменяемому состоянию в рамках различных реализаций FRP; из-за моего небольшого использования они часто кажутся похожими на концептуальную структуру сигналов, как в QT или Gtk + (например, добавление слушателей для событий).
Теперь, для первого вопроса. Для меня самым большим преимуществом является то, что изменяемое состояние отделено от другого кода на уровне типа. Это означает, что код не может случайно изменить состояние, если он явно не упоминается в сигнатуре типа. Он также дает очень хороший контроль состояния только для чтения в сравнении с изменчивым состоянием (монада-монада и монада монахов). Мне очень полезно структурировать свой код таким образом, и полезно иметь возможность сказать только от сигнатуры типа, если функция может быть мутировавшей, состояние неожиданно.
У меня лично нет никаких оговорок относительно использования изменчивого состояния в Haskell. Самая большая трудность заключается в том, что было бы утомительно добавлять состояние к тому, что раньше не требовалось, но то же самое было бы утомительным в других языках, которые я использовал для подобных задач (С#, Python).
Ответ 2
Хотя я не сомневаюсь, что люди будут отвечать "использовать государственную монаду", я хотел бы указать еще один полезный метод: функциональное реактивное программирование (с Ямпой или иначе).
Ответ 3
В чем преимущества и недостатки использования Haskell для решения проблем, связанных с большим количеством состояний, например, программирования графического интерфейса или программирования игр?
Преимущество состоит в том, что, даже если вы не особенно пользуетесь чистотой, Haskell - просто хороший язык.
Первоклассные функции - не должно быть большой проблемой в 2010 году, но это так. Алгебраические типы с совпадением рисунков. Мощная проверка статического типа с помощью вывода типа. Чистый синтаксис. Первоклассный concurrency, STM и чистый поток parallelism. Хороший компилятор. Тонны библиотек и больше с каждым днем. Активное, полезное сообщество.
Это не большие идеологические решения, такие как чистота или лень. Это просто хорошие идеи. Это то, что может быть у большинства языков, но слишком многие этого не делают.
Ответ 4
Государственная монада - это худший способ моделирования GUI или игры в Haskell. Я думаю, что второй вариант - использовать concurrency в обоих случаях. Однако лучший вариант был упомянут Полом: Функциональное реактивное программирование (FRP).
Лично я выступаю за FRP (AFRP) со стрелками, который, я думаю, был впервые реализован как Ямпа, а затем раздвоен как немного более полезный Animas. Тем не менее, Yampa быстро достигает своего предела, поэтому я написал более мощную, более выразительную библиотеку под названием netwire, которая также имеет некоторые концептуальные улучшения первые два.
По своей сути AFRP является функциональной государственной системой. Он функционирует в этом состоянии, не моделируется как изменяющиеся значения переменных, а мутирующие функции. Это чище и не требует какого-либо императивного программирования, такого как государственные монады.
Ответ 5
Обычно вы должны использовать Monad Transformer с StateT и IO, потому что для представления (GUI) требуется IO для ответа, однако, как только вы определили свой Monad Transformer в newtype
, вы хотели бы чтобы сделать сигнатуры логики игры только с интерфейсом MonadState
, таким образом, у вас все еще есть преимущество изменений, отличных от IO-ness. Код ниже объясняет, что я имею в виду:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.State
data GameState = GameState { ... } deriving (Show)
newtype GameMonad a = GameMonad (StateT GameState IO a)
deriving (Monad, MonadState GameState, MonadIO)
-- This way, now you have a monad with the state of the info
-- what would you like now is being able to modify the state, without actually
-- having IO capabilites.
movePlayerOnState :: (MonadState GameState) m => Position -> m ()
-- In this function we get the state out of the state monad, and then we modify
-- with a pure function, then put the result back again
-- Other times you would like to have the GUI API available, requiring the IO monad
-- for that
renderGameFromState :: MonadIO m => GameState -> m ()
-- in this method you would use liftIO method to call the GUI API
Этот код довольно сложный, если вы не понимаете монады, но мое эмпирическое правило: узнать, что такое Государственная Монада, понять, что такое Трансформаторы Монада (без необходимости понимать, как они работают) и как используйте монадию StateT.
Я могу указать на проект Sokoban, который я сделал с другим партнером по команде, который может быть полезен, он использует ncurses в качестве графического интерфейса, но вы можете получить представление о логике и о том, как мы управляем состояниями в игре
http://github.com/roman/HaskBan
Удачи.