Haskell Language
State Monad
Szukaj…
Wprowadzenie
State sa
która reprezentuje obliczenia, które przenoszą i potencjalnie modyfikują stan typu s
i dają wynik typu a
, ale termin „monada stanu” może ogólnie odnosić się do dowolnej monady, która niesie stan. Pakiet mtl
i transformers
zapewniają ogólne implementacje monad państwowych.
Uwagi
Nowo przybyli do Haskell często unikają monady State
i traktują ją jak tabu - tak jak rzekomą korzyścią z programowania funkcjonalnego jest unikanie stanu, więc czy nie stracicie tego, kiedy używacie State
? Bardziej szczegółowy widok polega na tym, że:
- Stan może być użyteczny w małych, kontrolowanych dawkach ;
- Typ
State
zapewnia bardzo precyzyjną kontrolę dawki.
Powodem jest to, że jeśli masz action :: State sa
, to mówi ci, że:
-
action
jest wyjątkowa, ponieważ zależy od stanu; - Stan ma typ
s
, więc naaction
nie może mieć wpływu żadna stara wartość w twoim programie - tylkos
lub pewna wartość osiągalna z niektórychs
; -
runState :: State sa -> s -> (a, s)
nakłada „barierę” wokół działania stanowego, tak że nie można zaobserwować jego skuteczności spoza tej bariery.
Jest to dobry zestaw kryteriów określających, czy należy użyć opcji State
w danym scenariuszu. Chcesz, aby Twój kod minimalizował zakres stanu , zarówno wybierając wąski typ dla s
i ustawiając runState
jak najbliżej „dolnej części” (tak, aby na twoje działania mogła mieć wpływ jak najmniej możliwy.
Numeracja węzłów drzewa za pomocą licznika
Mamy taki typ danych drzewa:
data Tree a = Tree a [Tree a] deriving Show
I chcemy napisać funkcję, która przypisuje liczbę do każdego węzła drzewa z licznika rosnącego:
tag :: Tree a -> Tree (a, Int)
Długa droga
Najpierw zrobimy to na dłuższą metę, ponieważ całkiem ładnie ilustruje mechanikę niskiego poziomu monady State
.
import Control.Monad.State
-- Function that numbers the nodes of a `Tree`.
tag :: Tree a -> Tree (a, Int)
tag tree =
-- tagStep is where the action happens. This just gets the ball
-- rolling, with `0` as the initial counter value.
evalState (tagStep tree) 0
-- This is one monadic "step" of the calculation. It assumes that
-- it has access to the current counter value implicitly.
tagStep :: Tree a -> State Int (Tree (a, Int))
tagStep (Tree a subtrees) = do
-- The `get :: State s s` action accesses the implicit state
-- parameter of the State monad. Here we bind that value to
-- the variable `counter`.
counter <- get
-- The `put :: s -> State s ()` sets the implicit state parameter
-- of the `State` monad. The next `get` that we execute will see
-- the value of `counter + 1` (assuming no other puts in between).
put (counter + 1)
-- Recurse into the subtrees. `mapM` is a utility function
-- for executing a monadic actions (like `tagStep`) on a list of
-- elements, and producing the list of results. Each execution of
-- `tagStep` will be executed with the counter value that resulted
-- from the previous list element's execution.
subtrees' <- mapM tagStep subtrees
return $ Tree (a, counter) subtrees'
Refaktoryzacja
Podziel licznik na akcję postIncrement
Tę część, w której get
ting bieżący licznik, a następnie put
ting counter + 1, można podzielić na akcję postIncrement
, podobnie jak wiele języków w stylu C:
postIncrement :: Enum s => State s s
postIncrement = do
result <- get
modify succ
return result
Podziel ścieżkę drzewa na funkcję wyższego rzędu
Logikę chodzenia po drzewie można podzielić na własne funkcje, takie jak:
mapTreeM :: Monad m => (a -> m b) -> Tree a -> m (Tree b)
mapTreeM action (Tree a subtrees) = do
a' <- action a
subtrees' <- mapM (mapTreeM action) subtrees
return $ Tree a' subtrees'
Za pomocą tego i funkcji postIncrement
możemy przepisać tagStep
:
tagStep :: Tree a -> State Int (Tree (a, Int))
tagStep = mapTreeM step
where step :: a -> State Int (a, Int)
step a = do
counter <- postIncrement
return (a, counter)
Użyj klasy Traversable
mapTreeM
rozwiązanie mapTreeM
można łatwo przepisać na instancję klasy Traversable
:
instance Traversable Tree where
traverse action (Tree a subtrees) =
Tree <$> action a <*> traverse action subtrees
Zauważ, że wymagało to od nas zastosowania Applicative
(operator <*>
) zamiast Monad
.
Z tym, teraz możemy napisać tag
jak profesjonalista:
tag :: Traversable t => t a -> t (a, Int)
tag init t = evalState (traverse step t) 0
where step a = do tag <- postIncrement
return (a, tag)
Pamiętaj, że działa to dla każdego typu Traversable
, nie tylko naszego typu Tree
!
Pozbycie się Traversable
boilerplate
GHC ma rozszerzenie DeriveTraversable
które eliminuje potrzebę pisania powyższej instancji:
{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
data Tree a = Tree a [Tree a]
deriving (Show, Functor, Foldable, Traversable)