Haskell: Йесод и состояние

Я читал код для Сократитель ссылок для игрушек. Тем не менее, есть важные части, которые я просто не могу понять.

Он имеет следующий код:

data URLShort = URLShort { state :: AcidState URLStore }

В целях тестирования я написал что-то вроде этого в своем собственном приложении:

data MyApp = MyApp { state :: Int }

Затем я мог бы скомпилировать, изменив

main = warpDebug 3000 MyApp

to

main = warpDebug 3000 (MyApp 42)

И тогда я мог бы читать состояние в обработчиках, выполнив

mystate <- fmap state getYesod 

вдохновлен acid <- fmap state getYesod в статье. Однако я не знал, как делать записи.

Я также пытался сделать:

data MyApp = MyApp { state :: State Int Int }

Но я не продвинулся далеко с этим.

Я пытался понять, как работает AcidState, просто делая несколько простых подобных примеров, полагая, что, поскольку AcidState хранит все в памяти, я должен быть в состоянии сделать то же самое?

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


person Clinton    schedule 04.04.2012    source источник


Ответы (1)


Тип данных AcidState a не является неизменяемым значением до конца; он содержит внутренние ссылки на изменяемые данные. То, что хранится в земле Йесод, в данном случае является просто неизменяемой ссылкой на эти данные. Когда вы обновляете состояние, вы фактически обновляете не значение базового типа данных, а память, на которую оно указывает.

Каждое значение в мире Haskell является неизменным. Однако многие вещи за пределами Царства Хаскелла не являются неизменными; например, когда вы делаете putStrLn, терминал меняет свой дисплей, чтобы показать новый контент. Само действие putStrLn представляет собой неизменяемое чистое значение, но оно описывает, как выполнить действие, связанное с изменением.

Есть и другие функции, которые также производят действия, выполняющие мутации; если вы делаете ref <- newIORef 0, вы получаете значение, описывающее действие, которое создает изменяемую ячейку памяти. Если вы затем сделаете modifyIORef ref (+1), вы получите значение, описывающее действие, которое увеличивает значение в этой ячейке на 1. Значение ref является чистым значением, это просто ссылка на изменяемую ячейку. Код также чисто функциональный, поскольку каждая часть описывает только действие; ничто не может быть изменено в программе Haskell.

Вот как AcidState реализует свое состояние: используя систему, которая управляет состоянием за пределами мира Haskell. Это не так плохо, как полная изменчивость, как в таких языках, как C, потому что в Haskell вы можете управлять изменчивостью с помощью монад. Использование AcidState совершенно безопасно и, насколько мне известно, не предполагает использования unsafePerformIO.

В этом случае с AcidState вы используете openAcidState emptyStore в монаде IO для создания нового кислотного состояния (эта строка является значением, описывающим действие ввода-вывода, которое открывает новое кислотное состояние). Вы используете createCheckpointAndClose для безопасного сохранения кислотного состояния на диск. Наконец, вы используете функцию update' для изменения содержимого кислотного состояния.

Чтобы создать «маленькое состояние» самостоятельно, используя IORefs (самая простая форма изменяемого состояния, за исключением, может быть, монады ST), вы сначала добавляете такое поле к базовому типу данных:

data VisitorCounter = VisitorCounter { visitorCounter :: IORef Int }

Затем вы делаете:

main = do
  counter <- newIORef 0
  warpDebug 3000 (VisitorCounter counter)

В обработчике вы можете изменить счетчик следующим образом:

counter <- fmap visitorCounter getYesod
modifyIORef counter (+1)
count <- readIORef counter
-- ... display the count or something

Обратите внимание на симметрию к AcidState.

Для счетчика сайтов я бы рекомендовал использовать TVars вместо IORefs из-за возможности одновременного изменения переменной несколькими клиентами. Однако интерфейс TVars очень похож.


Дополнительный вопрос от автора вопроса?

Я поместил { visitorCounter :: TVar Int } в свой базовый тип и следующий код в обработчик:

counter <- fmap visitorCounter getYesod
count <- readTVar counter

Первая строка компилируется нормально, но вторая выдает эту ошибку:

Couldn't match expected type `GHandler sub0 Middleware t0'
            with actual type `STM a0'
In the return type of a call of `readTVar'
In a stmt of a 'do' expression: count <- readTVar counter

Как я могу это исправить?

person dflemstr    schedule 04.04.2012
comment
Я не очень понимаю это. Я думал, что все данные в Haskell неизменяемы, даже монады, поскольку нотация do — это просто синтаксический сахар для функционального кода. Если эти данные являются тайно изменяемыми, то нет шансов, что Haskell, предполагая, что все неизменно, не вернет измененное значение (потому что он будет считать, что оно не изменилось). Кажется, вы предполагаете, что это работает как unsafePerformIO. Это действительно так? - person Clinton; 04.04.2012
comment
Я обновил свой ответ, пожалуйста, прокомментируйте, если у вас все еще есть вопросы. - person dflemstr; 04.04.2012
comment
Спасибо за обновленный ответ. Дополнительный вопрос: можете ли вы предоставить реализацию тривиальной версии чего-то вроде IORef или TVar, просто чтобы я мог видеть, что они делают (на данный момент это похоже на черную магию)? Я также хотел бы получить некоторую информацию о безопасности потоков, но я думаю, что мне было бы лучше сначала прочитать ваш ответ относительно деталей реализации. - person Clinton; 05.04.2012
comment
IORef это черная магия. Я не могу предоставить реализацию, равно как и не могу показать, как реализованы putStrLn или Int. Источник находится здесь, но это вряд ли информативно; Ссылки ввода-вывода примитивны в Haskell, поэтому все просто перенаправляется на внутренности GHC. То же самое касается TVar; он использует внутренние компоненты GHC для создания программно-транзакционной памяти. - person dflemstr; 05.04.2012
comment
Спасибо, теперь это имеет больше смысла. Я попробую. - person Clinton; 05.04.2012
comment
Не могли бы вы привести пример использования TVar? - person Clinton; 05.04.2012
comment
Пожалуйста, смотрите мой дополнительный вопрос в редактировании вашего поста, у меня проблемы с использованием TVar. - person Clinton; 05.04.2012
comment
Я получил это, используя liftIO. то есть count <- liftIO $ readTVarIO counter - person Clinton; 05.04.2012