Теперь о проблеме у меня: Когда я использую лифт, он работает нормально, потому что функция становится типа StateT
FieldsB
, но когда я пытаюсь использовать setX_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