I find lenses very useful for accessing deeply nested data, but frequently "containers" like MVar
or TVar
s throw off some of the nice properties of lenses.
For example:
data SomeStruct = SomeStruct { _b :: Int
}
makeLenses ''SomeStruct
data AppState = AppState { _a :: SomeStruct
}
makeLenses ''AppState
data App = App { _state :: AppState
}
makeLenses ''App
I can make new lenses using very nice left-to-right composition:
let v = App (AppState (SomeStruct 3))
in v^.state.a.b
However, if _state
were of type TVar, the left-to-right composition breaks down, and lenses feel a lot clunkier to use:
t <- newTVarIO $ AppState (SomeStruct 3)
let v = App t
atomically $ (^.a.b) <$> readTVar (v^.state)
^.a.b
gets pushed to the left hand side despite ^.state
being the innermost lens. Is there some way I could deal with these sorts of "container" types and lenses in a more ergonomic way?
There is a library (formerly part of lens proper) called lens-action that helps mixing getters and folds with monading actions without yanking you too much out of the lensy world.
For example, for the type
data App = App { _state :: TVar AppState }
We could write
ghci> :t \v -> v^!state.act readTVar.a.b
\v -> v^!state.act readTVar.a.b :: App -> STM Int
The idea is that instead of using the typical view function (^.)
we use its monadic counterpart (^!)
. And we insert monadic actions using functions like act
or acts
. Normal getters and folds don't need to be lifted and composition is still done with plain (.)
.