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