От C ++ до классов и состояний Haskell - PullRequest
13 голосов
/ 03 июня 2011

Я должен преобразовать этот код C ++

class A {

public: 
   int x_A;

    void setX_A (int newx) {
        x_A = newx;
    }

    void printX_A() {
       printf("x_A is %d", x_A);
    }
};

class B : public A {
public:
    int x_B;

    void setX_B (int newx) {
       x_B = newx;
    }

    void printX_B() {
       printf("x_B is %d", x_B);
    }

};

main() {
    A objA;
    B objB;
    objA.setX_A(2);
    objA.printX_A();
    objB.printX_A();
    objB.setX_B(5);
    objB.printX_B();
}

в код на Haskell и симуляцию main() с использованием монады State (или StateT).

То, что я сделал до сих пор, это:

import Control.Monad.State
import Control.Monad.Identity

-- Fields For A
data FieldsA = FieldsA {x_A::Int} deriving (Show)

    -- A Class Constructor
constA :: Int -> FieldsA
constA = FieldsA

class A a where
    getX_A :: StateT a IO Int
    setX_A :: Int -> StateT a IO ()
    printX_A :: StateT a IO ()

instance A FieldsA where
    getX_A = get >>= return . x_A
    setX_A newx = do
        fa <- get
        put (fa { x_A = newx })
    printX_A = do
        fa <- get
        liftIO $ print fa
        return ()


data FieldsB = FieldsB{ fa::FieldsA, x_B::Int } deriving (Show)

constB :: Int -> Int -> FieldsB
constB int1 int2 = FieldsB {fa = constA int1, x_B = int2}

class A b => B b where
    getX_B :: StateT b IO Int
    setX_B :: Int -> StateT b IO ()
    printX_B :: StateT b IO ()

-- A Functions for Class B
instance A FieldsB where
    getX_A = do
      (FieldsB (FieldsA x_A) x_B) <- get
      return (x_A)
    setX_A newx = do
        (FieldsB (FieldsA x_A) x_B) <- get
        put (constB newx x_B)
    printX_A = do
        fb <- get
        liftIO $ print fb
        return ()
-- B specific Functions
instance B FieldsB where
    getX_B = get >>= return . x_B
    setX_B newx = do
        fb <- get
        put (fb { x_B = newx })
    printX_B = do
        fb <- get
        liftIO $ print fb
        return ()

test :: StateT FieldsA (StateT FieldsB IO ) ()
test = do
      x <- get
      setX_A 4
      printX_A

      --lift $ setX_A 99
      --lift $ setX_B 99
      --lift $ printX_A
      --lift $ printX_B

      --printX_A
      return ()

go = evalStateT (evalStateT test (constA 1)) (constB 2 3)
--go = runIdentity $ evalStateT (evalStateT test (constA 1)) (constA 1)

тест, являющийся main().

Теперь о проблеме, которую я имею: Когда я использую лифт, она работает нормально, потому что функция становится типа StateT FieldsB, но когда я пытаюсь использовать setX_A без лифта, возникает проблема

*** Type           : StateT FieldsA IO ()
*** Does not match : StateT FieldsA (StateT FieldsB IO) ()

Если я поменяю тип setX_A на второй, то он не будет работать, когда я использую его с лифтом (потому что класс B является производным от A).

Ответы [ 2 ]

13 голосов
/ 03 июня 2011

Прежде всего, спасибо за предоставленную детализацию, она значительно облегчает понимание вашей проблемы!

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

Хотя я уверен, что вы понимаете, что это очень императивный код, и перевод его непосредственно на Haskell немного глуп, это задание, поэтому давайте поговорим о способах сделать это, которые немного ближе к стандартному Haskell.

Код императива

Стиль Государственной Монады:

Оставив пока IO, чтобы сделать что-то подобное в чистом коде, типичный подход будет выглядеть примерно так:

  • Создайте тип данных, содержащий все ваше состояние
  • «Изменить» состояние с помощью get и put

Для вывода вы можете использовать StateT вокруг IO, или вы можете добавить поле к данным состояния, представляющим вывод, содержащий список String с, и делать все это без IO.

Это наиболее близкий к "правильному" способу применения вашего текущего подхода и примерно то, что предлагает @Rotsor.

IO Monad Style

Выше все еще требуется, чтобы все изменяемые переменные были определены заранее, вне функции, путем определения их в данных состояния. Вместо того, чтобы манипулировать вещами таким образом, вы также можете имитировать исходный код более прямо и использовать реальное, честное и изменяемое состояние в IO. Используя только A в качестве примера, вы получите что-то вроде этого:

data FieldsA = FieldsA { x_A :: IORef Int}

constA :: Int -> IO FieldsA
constA x = do xRef <- newIORef x
              return $ FieldsA xRef

class A a where
    getX_A :: a -> IO Int
    setX_A :: a -> Int -> IO ()
    printX_A :: a -> IO ()

instance A FieldsA where
    getX_A = readIORef . x_A
    setX_A = writeIORef . x_A
    printX_A a = getX_A a >>= print

Это концептуально намного ближе к оригиналу и соответствует тому, что @augustss предложил в комментариях к вопросу.

Небольшой вариант - сохранить объект как простое значение, но использовать IORef для хранения текущей версии. Разница между этими двумя подходами примерно эквивалентна изменяемому объекту в языке ООП с методами установки, которые изменяют внутреннее состояние по сравнению с неизменяемыми объектами с изменяемыми ссылками на них.

Предметы

Другая половина трудностей заключается в моделировании наследования в Haskell. Подход, который вы используете, является наиболее очевидным, к которому стремятся многие, но он несколько ограничен. Например, вы не можете по-настоящему использовать объекты в любом контексте, где ожидается супертип; например, если функция имеет тип, такой как (A a) => a -> a -> Bool, нет простого способа применить ее к двум различным подтипам A. Вы должны будете реализовать свое собственное приведение к супертипу.

Вот набросок альтернативного перевода, который, я бы сказал, более естественен для использования в Haskell и более точен в стиле ООП.

Сначала посмотрите, как все методы класса принимают объект в качестве первого аргумента. Это представляет неявное «это» или «я» в языках ООП. Мы можем сохранить шаг, предварительно применяя методы к данным объекта, чтобы получить коллекцию методов, уже «привязанных» к этому объекту. Затем мы можем сохранить эти методы как тип данных:

data A = A { _getX_A :: IO Int
           , _setX_A :: Int -> IO ()
           , _printX_A :: IO ()
           }

data B = B { _parent_B :: A 
           , _getX_B :: IO Int
           , _setX_B :: Int -> IO ()
           , _printX_B :: IO ()
           }

Вместо использования классов типов для предоставления методов, мы будем использовать их для предоставления приведения к супертипу :

class CastA a where castA :: a -> A
class CastB b where castB :: b -> B

instance CastA A where castA = id
instance CastA B where castA = _parent_B
instance CastB B where castB = id

Существуют более продвинутые приемы, которые мы могли бы использовать, чтобы избежать создания класса типов для каждого псевдо-ООП-класса, но здесь все будет просто.

Обратите внимание, что я поставил над полями объекта префикс подчеркивания. Это потому, что они специфичны для данного типа; Теперь мы можем определить «реальные» методы для любого типа, который может быть приведен к нужному нам:

getX_A x = _getX_A $ castA x
setX_A x = _setX_A $ castA x
printX_A x = _printX_A $ castA x

getX_B x = _getX_B $ castB x 
setX_B x = _setX_B $ castB x
printX_B x = _printX_B $ castB x

Для создания новых объектов мы будем использовать функции, которые инициализируют внутренние данные - эквивалентные закрытым членам на языке ООП - и создадим тип, представляющий объект:

newA x = do xRef <- newIORef x
            return $ A { _getX_A = readIORef xRef
                       , _setX_A = writeIORef xRef
                       , _printX_A = readIORef xRef >>= print
                       }

newB xA xB = do xRef <- newIORef xB
                parent <- newA xA
                return $ B { _parent_B = parent
                           , _getX_B = readIORef xRef
                           , _setX_B = writeIORef xRef
                           , _printX_B = readIORef xRef >>= print
                           }

Обратите внимание, что newB вызывает newA и получает тип данных, содержащий его функции-члены.Он не может получить доступ к «закрытым» членам A напрямую, но может заменить любую из функций A, если захочет.

Теперь мы можем использовать их почти идентичнокак по стилю, так и по значению, к вашему оригиналу, например:

test :: IO ()
test = do a <- newA 1
          b <- newB 2 3
          printX_A a
          printX_A b
          setX_A a 4
          printX_A a
          printX_B b
8 голосов
/ 03 июня 2011

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

data MainState = MainState { objA :: FieldsA, objB :: FieldsB }

Теперь ваша монада основной функции может выглядеть следующим образом:

type Main t = StateT MainState IO t

И, чтобы выбратьобъект, с которым вы работаете, вы можете использовать что-то вроде этого:

withObjA :: StateT FieldsA IO t -> Main t
withObjB :: StateT FieldsB IO t -> Main t

Использование будет примерно таким:

test :: Main ()
test = do
    withObjA $ do
        setX_A 2
        printX_A
    withObjB $ do
        printX_A
        setX_B 5
        printX_B

Обновление:

Вот как можно реализовать withObjA и withObjB:

withPart :: Monad m => (whole -> part) -> (part -> whole -> whole) -> StateT part m t -> StateT whole m t
withPart getPart setPart action = do
    whole <- get
    (t, newPart) <- lift $ runStateT action (getPart whole)
    put (setPart newPart whole)
    return t

withObjA :: StateT FieldsA IO t -> Main t
withObjA = withPart objA (\objA mainState -> mainState { objA = objA })

withObjB :: StateT FieldsB IO t -> Main t
withObjB = withPart objB (\objB mainState -> mainState { objB = objB })

Здесь функция withPart продвигает action, работающий на part, к действию, действующему на whole, используя getPart для извлечения части целого и setPart для обновления части целого.Я был бы признателен, если бы кто-нибудь рассказал мне о функции библиотеки, выполняющей нечто подобное.withObjA и withObjB реализуются путем передачи соответствующих функций доступа к withPart.

...