Монада haskell для имитации кода стиля OO
Конкретные примеры на http://www.haskell.org/haskellwiki/State_Monad очень полезны для понимания того, как писать реальный код с помощью монад (см. Также stackru/9014218). Но большинство из нас, новых студентов, имеют опыт работы с OO, поэтому сопоставление OO-программы с haskell поможет продемонстрировать, как написать эквивалентный код на haskell. (Да, две парадигмы совершенно разные, и не стоит переводить код в стиле ОО непосредственно в haskell, но только один раз в качестве учебного пособия.)
Вот код в стиле OO, который создает 2 экземпляра объекта, а затем вызывает функции-члены, которые изменяют их соответствующие переменные-члены и, наконец, выводят их на печать. Как мы пишем это, используя монады состояний haskell?
class A:
int p;
bool q;
A() { p=0; q=False;} // constructor
int y() { // member function
if(q) p++; else p--;
return p;
}
bool z() { // member function
q = not q;
return q;
}
main:
// main body - creates instances and calls member funcs
a1 = A; a2 = A; // 2 separate instances of A
int m = a1.y();
m = m + a1.y();
bool n = a2.z();
print m, n, a1.p, a1.q, a2.p, a2.q;
2 ответа
Прямой перевод будет что-то вроде:
module Example where
import Control.Monad.State
data A = A { p :: Int, q :: Bool }
-- constructor
newA :: A
newA = A 0 False
-- member function
y :: State A Int
y = do
b <- getQ
modifyP $ if b then (+1) else (subtract 1)
getP
-- member function
z :: State A Bool
z = do
b <- gets q
modifyQ not
getQ
main :: IO ()
main = do
let (m,a1) = flip runState newA $ do
m <- y
m <- (m +) `fmap` y
return m
let (n,a2) = flip runState newA $ do
n <- z
return n
print (m, n, p a1, q a1, p a2, q a2)
-- general purpose getters and setters
getP :: State A Int
getP = gets p
getQ :: State A Bool
getQ = gets q
putP :: Int -> State A ()
putP = modifyP . const
putQ :: Bool -> State A ()
putQ = modifyQ . const
modifyP :: (Int -> Int) -> State A ()
modifyP f = modify $ \a -> a { p = f (p a) }
modifyQ :: (Bool -> Bool) -> State A ()
modifyQ f = modify $ \a -> a { q = f (q a) }
И я, вероятно, не стал бы беспокоиться о ручном геттере / сеттере и просто использовал линзы.
{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
module Main where
import Control.Applicative
import Control.Monad.State
import Data.Lenses
import Data.Lenses.Template
data A = A { p_ :: Int, q_ :: Bool } deriving Show
$( deriveLenses ''A )
-- constructor
newA :: A
newA = A 0 False
-- member function
y :: MonadState A m => m Int
y = do
b <- q get
if b then p $ modify (+1) else p $ modify (subtract 1)
p get
-- member function
z :: MonadState A m => m Bool
z = do
q $ modify not
q get
data Main = Main { a1_ :: A, a2_ :: A, m_ :: Int, n_ :: Bool } deriving Show
$( deriveLenses ''Main )
main :: IO ()
main = do
-- main body - creates instances and calls member funcs
print $ flip execState (Main undefined undefined undefined undefined) $ do
a1 $ put newA ; a2 $ put newA -- 2 separate instances of A
m . put =<< a1 y
m . put =<< (+) <$> m get <*> a1 y
n . put =<< a2 z
Но это не то, что я бы действительно написал, потому что я склоняю Хаскелл назад, чтобы попытаться имитировать ОО-стиль. Так что это выглядит просто неловко.
Для меня реальная цель объектно-ориентированного кода - программирование на интерфейс. Когда я использую такие объекты, я могу положиться на них для поддержки таких методов. Итак, в haskell я бы сделал это, используя класс типов:
{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
module Main where
import Prelude hiding (lookup)
import Control.Applicative
import Control.Monad.State
import Data.Lenses
import Data.Lenses.Template
import Data.Map
class Show a => Example a where
-- constructor
new :: a
-- member function
y :: MonadState a m => m Int
-- member function
z :: MonadState a m => m Bool
data A = A { p_ :: Int, q_ :: Bool } deriving Show
$( deriveLenses ''A )
instance Example A where
new = A 0 False
y = do
b <- q get
if b then p $ modify (+1) else p $ modify (subtract 1)
p get
z = do
q $ modify not
q get
data B = B { v_ :: Int, step :: Map Int Int } deriving Show
$( deriveLenses ''B )
instance Example B where
new = B 10 . fromList $ zip [10,9..1] [9,8..0]
y = v get
z = do
i <- v get
mi <- lookup i `liftM` gets step
case mi of
Nothing -> return False
Just i' -> do
v $ put i'
return True
data Main a = Main { a1_ :: a, a2_ :: a, m_ :: Int, n_ :: Bool } deriving Show
start :: Example a => Main a
start = Main undefined undefined undefined undefined
$( deriveLenses ''Main )
run :: Example a => State (Main a) ()
run = do
-- main body - creates instances and calls member funcs
a1 $ put new ; a2 $ put new -- 2 separate instances of a
m . put =<< a1 y
m . put =<< (+) <$> m get <*> a1 y
n . put =<< a2 z
main :: IO ()
main = do
print $ flip execState (start :: Main A) run
print $ flip execState (start :: Main B) run
Так что теперь я могу использовать то же самое run
для разных типов A
а также B
,
State
Монаду нельзя использовать для эмуляции классов. Он используется для моделирования состояния, которое "прилипает" к коду, который вы запускаете, а не состояния, которое "является независимым" и находится в объектно-ориентированных классах.
Если вы не хотите переопределения и наследования методов, ближе всего к классам ООП в Haskell вы можете использовать записи со связанными функциями. Единственное отличие, о котором вы должны знать в этом случае, состоит в том, что все "методы класса" возвращают новые "объекты", они не изменяют старый "объект".
Например:
data A =
A
{ p :: Int
, q :: Bool
}
deriving (Show)
-- Your "A" constructor
newA :: A
newA = A { p = 0, q = False }
-- Your "y" method
y :: A -> (Int, A)
y a =
let newP = if q a then p a + 1 else p a - 1
newA = a { p = newP }
in (newP, newA)
-- Your "z" method
z :: A -> Bool
z = not . q
-- Your "main" procedure
main :: IO ()
main =
print (m', n, p a1'', q a1'', p a2, q a2)
where
a1 = newA
a2 = newA
(m, a1') = y a1
(temp, a1'') = y a1'
m' = m + temp
n = z a2
Эта программа печатает:
(-3,True,-2,False,0,False)
Обратите внимание, что мы должны были создать новые переменные для хранения новых версий m
а также a1
(Я только добавил '
в конце каждый раз). В Haskell нет изменяемых переменных на уровне языка, поэтому не стоит пытаться использовать язык для этого.
Можно создавать изменяемые переменные с помощью ссылок ввода-вывода.
Обратите внимание, однако, что следующий код считается крайне плохим стилем кодирования среди Haskellers. Если бы я был учителем и имел ученика, который писал код, подобный этому, я бы не дал проходной балл по заданию; если бы я использовал программиста на Haskell, который писал такой код, я бы подумал уволить его, если бы у него не было ОЧЕНЬ веской причины для написания такого кода.
import Data.IORef -- IO References
data A =
A
{ p :: IORef Int
, q :: IORef Bool
}
newA :: IO A
newA = do
p' <- newIORef 0
q' <- newIORef False
return $ A p' q'
y :: A -> IO Int
y a = do
q' <- readIORef $ q a
if q'
then modifyIORef (p a) (+ 1)
else modifyIORef (p a) (subtract 1)
readIORef $ p a
z :: A -> IO Bool
z = fmap not . readIORef . q
main :: IO ()
main = do
a1 <- newA
a2 <- newA
m <- newIORef =<< y a1
modifyIORef m . (+) =<< y a1
n <- z a2
m' <- readIORef m
pa1 <- readIORef $ p a1
qa1 <- readIORef $ q a1
pa2 <- readIORef $ p a2
qa2 <- readIORef $ q a2
print (m', n, pa1, qa1, pa2, qa2)
Эта программа делает то же самое, что и вышеприведенная программа, но с изменяемыми переменными. Опять же, не пишите такой код, за исключением очень редких обстоятельств.