От классов С++ до Haskell и государств

Мне нужно преобразовать этот код на С++

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) Monad.

Что я сделал до сих пор:

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)

test main().

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

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

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

Ответы

Ответ 1

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

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

Update:

Вот как можно реализовать 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.

Ответ 2

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

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

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

Императивный код

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

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

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

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

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

Стиль IO Monad

Вышеизложенное требует, чтобы все изменяемые переменные указывались заранее, вне функции, определяя их в данных состояния. Вместо того, чтобы манипулировать такими вещами, вы также можете более точно имитировать исходный код и использовать реальное, изменяемое состояние с честностью к богу в 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 для хранения текущей версии. Разница между этими двумя подходами примерно эквивалентна, на языке ООП, изменяемому объекту с методами setter, которые изменяют внутреннее состояние и неизменяемые объекты с изменяемыми ссылками на них.

Объекты

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

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

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

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 и получает тип данных, содержащий его функции-члены. Он не может напрямую обращаться к "private" членам 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