Я хотел бы написать функцию
step :: State S O
, где O
- это тип записи:
data O = MkO{ out1 :: Int, out2 :: Maybe Int, out3 :: Maybe Bool }
Суть в том, что я хотел бы собрать свой O
вывод кусочно. Под этим я подразумеваю, что в разных местах по определению step
я тут же узнаю, что, например, out2
должно быть Just 3
, но я не знаю, как не запутано, что out1
и out3
должно быть. Кроме того, существует естественное значение по умолчанию для out1
, которое можно вычислить из конечного состояния; но все еще должна быть возможность переопределить его в step
.
И, что самое важное, я хочу это "развернуть", чтобы пользователи могли предоставлять свои собственные типы S
и O
и я даю им остальное.
Мой текущий подход - обернуть все в WriterT (HKD O Last)
, используя Higgledy , автоматизированный способ создания типа HKD O Last
, который isomorphi c to
data OLast = MkOLast{ out1' :: Last Int, out2' :: Last (Maybe Int), out3' :: Last (Maybe String) }
Это идет с очевидным экземпляром Monoid
, поэтому я могу, по крайней мере морально, сделать следующее:
step = do
MkOLast{..} <- execWriterT step'
s <- get
return O
{ out1 = fromMaybe (defaultOut1 s) $ getLast out1'
, out2 = getLast out2'
, out3 = fromMaybe False $ getLast out3'
}
step' = do
...
tell mempty{ out2' = pure $ Just 42 }
...
tell mempty{ out1' = pure 3 }
Это код, с которым я мог бы жить.
Проблема в том, что я могу сделать это только морально . В практике , то, что я должен написать, является довольно запутанным кодом, потому что HKD O Last
Хиггли выставляет поля записи как линзы, поэтому реальный код в конечном итоге выглядит примерно так:
step = do
oLast <- execWriterT step'
s <- get
let def = defaultOut s
return $ runIdentity . construct $ bzipWith (\i -> maybe i Identity . getLast) (deconstruct def) oLast
step' = do
...
tell $ set (field @"out2") (pure $ Just 42) mempty
...
tell $ set (field @"out3") (pure 3) mempty
Первая бородавка в step
, которую мы можем спрятать за функцией:
update :: (Generic a, Construct Identity a, FunctorB (HKD a), ProductBC (HKD a)) => a -> HKD a Last -> a
update initial edits = runIdentity . construct $ bzipWith (\i -> maybe i Identity . getLast) (deconstruct initial) edits
, чтобы мы могли «развернуть» это как
runStep
:: (Generic o, Construct Identity o, FunctorB (HKD o), ProductBC (HKD o))
=> (s -> o) -> WriterT (HKD o Last) (State s) () -> State s o
runStep mkDef step = do
let updates = execWriterT step s
def <- gets mkDef
return $ update def updates
Но что меня беспокоит, так это места, где частичное выходы записываются. Пока что лучшее, что я смог придумать, - это использовать OverloadedLabels
для предоставления #out2
в качестве возможного синтаксиса:
instance (HasField' field (HKD a f) (f b), Applicative f) => IsLabel field (b -> Endo (HKD a f)) where
fromLabel x = Endo $ field @field .~ pure x
output :: (Monoid (HKD o Last)) => Endo (HKD o Last) -> WriterT (HKD o Last) (State s) ()
output f = tell $ appEndo f mempty
, что позволяет конечным пользователям писать step'
как
step' = do
...
output $ #out2 (Just 42)
...
output $ #out3 3
но это все еще немного громоздко; кроме того, он использует довольно много тяжелой техники за кулисами. Особенно с учетом того, что мой вариант использования таков, что все внутренние компоненты библиотеки необходимо объяснять поэтапно.
Итак, я ищу улучшения в следующих областях:
- Более простая внутренняя реализация
- Более хороший API для конечных пользователей
- Я был бы счастлив с совершенно другим подходом также из первых принципов, если пользователю не требуется определять свои собственные
OLast
рядом с O
...