-
Notifications
You must be signed in to change notification settings - Fork 165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Request for comment: EventM state monad refactor #379
Comments
I'm all for easier APIs, hledger would adapt. Thank you ! |
Okay, thanks, @simonmichael - that's great to hear. |
@simonmichael I guess I'm wondering whether you think the specific changes described above would make things easier for |
I'll know more after porting to the new API but I expect it would be more of a help when first coding something - not now, when we are already coded for the old API.
|
I will attempt to find time to update purebred this week, and provide feedback about the change. |
@frasertweedale Okay, great! |
It sounds reasonable to me. I'll try to refactor my application to the feature branch sometime this week and will be able to give more specific feedback then. |
Thanks, @kquick! |
FYI: I also keep thinking of ways to update and clarify the summary comment at the top of this ticket, too. |
Yeah, although refactoring to use the new API would be a bit of work, I'm pretty convinced (as far as I can be without actually trying it) that this would improve the code in |
@byorgey okay, great. Another thing I wanted to mention that I will add to the summary comment, but that I wanted to make very explicit here: |
Thanks for reaching out! I guess my main concern is how to migrate in such a way that I can support the 2 major versions of Brick as easily as possible, since my project will need to keep supporting older code for a while. I guess I can more or less use |
@thomasjm I'm afraid that will probably not be practical. Migrating to this version of |
Okay understood. I'll try to find some time to try out the new branch. In general any shims to ease the possibility of making a |
I'll be one trying to support both APIs, since I need to support a range of ghc versions and all stackage snapshots.
|
For folks wanting to support older versions of I also want to mention that I do not explicitly participate in Stackage as a thing in the Haskell ecosystem, so any compatibility of |
I've tried new API with few of my own apps (Cj-bc/brick-3d, Cj-bc/brick-shgif), and it was easy to migrate! As I don't know much about API design, I'm not sure about pros and cons. But I felt confortable to write I've struggled a bit to unedrstand how event handler works when I first learnt brick because of its (a little bit) complecated type signature, so I think it's good idea to use well-known 'MonadState' to construct it. So I agree with this update! |
@Cj-bc that's very cool! And I'm glad to hear that the update was straightforward in your case. |
I find that my state ends up looking something like this: data MainScreenState
data SecondScreenState
data ThirdScreenState
data ScreenState = MSS MainScreenState | SSS SecondScreenState | TSS ThirdScreenState
data AppState = AppState { _field1 :: CommonState1, _field2 :: CommonState2, _screenState :: ScreenState }
handleEvent :: AppState -> BrickEvent n e -> EventM n (Next AppState)
handleEvent (AppState field1 field2 screenState) ev = case screenState of
MSS mss -> handleEventMainScreen mss field1 field2 ev
SSS sss -> handleEventSecondScreen sss ev
TSS tss -> handleEventThirdScreen tss field2 ev
handleEventMainScreen
:: MainScreenState -> CommonState1 -> CommonState2 -> BrickEvent n e -> EventM n (Next AppState)
handleEventSecondScreen
:: SecondScreenState -> BrickEvent n e -> EventM n (Next AppState)
handleEventThirdScreen
:: ThirdScreenState -> CommonState2 -> BrickEvent n e -> EventM n (Next AppState) Note that each of the screen specific event handlers take in the state specific to their screen, so that they don't have to do redundant partial pattern matches on |
If it were me, I'd just rewrite everything to be in terms of One challenge with your current data types that is exacerbated by the move to a state monad is that you can't easily use lenses over the different constructors of data MainScreenState
data SecondScreenState
data ThirdScreenState
data ScreenState = MSS MainScreenState | SSS SecondScreenState | TSS ThirdScreenState
data AppState = AppState { _field1 :: CommonState1, _field2 :: CommonState2, _screenState :: ScreenState }
handleEvent :: BrickEvent n e -> EventM n AppState ()
handleEvent ev = do
s <- use screenState
case s of
MSS _ -> handleEventMainScreen ev
SSS _ -> handleEventSecondScreen ev
TSS _ -> handleEventThirdScreen ev
handleEventMainScreen :: BrickEvent n e -> EventM n AppState ()
handleEventSecondScreen :: BrickEvent n e -> EventM n AppState ()
handleEventThirdScreen :: BrickEvent n e -> EventM n AppState () |
In case you haven't used something like handleEventMainScreen :: BrickEvent n e -> EventM n AppState ()
handleEventMainScreen ev = do
field1 .= someNewCommonState -- Direct assignment
screenState.unsafeMSS %= func -- Modification by a pure function, func
screenState.unsafeMSS .= newMSSValue -- Direct assignment (This assumes the existence of a lens like |
I've used and enjoy |
@jtdaugherty from the purebred project, we have a few problems to solve before we can properly analyse this proposed brick change. There are already several other users providing feedback. So if you are ready to proceed before we are able to give feedback, our default position is "no objection". |
@sullyj3 I don't know of a better way than to use mss :: Lens' ScreenState MainScreenState
mss =
lens (\ss -> case ss of
MSS mss -> mss
_ -> error "BUG: mss: tried to get MainScreenState from incorrect constructor")
(\ss mss -> case ss of
MSS _ -> MSS mss
_ -> error "BUG: mss: tried to write MainScreenState to incorrect constructor field") |
@frasertweedale no hurry, I'm not going to merge this for a while and not until the feedback suggests the change could be weathered. |
Hi, does anyone here know how to run an action on the inner state? There is bound to be a lens for that, but I do not see it. Original code: s' <- s & uiState . uiModal . _Just . modalDialog %%~ handleDialogEvent ev Work in progress: -- Something like this would be nice:
-- uiState . uiModal . _Just . modalDialog %%= handleDialogEvent ev
modal <- use $ uiState . uiModal . _Just . modalDialog
d <- fst <$> nestEventM modal (handleDialogEvent ev)
uiState . uiModal . _Just . modalDialog .= d But in most cases, the change is easy and more readable, I should have swarm updated over the weekend. 👍 |
s' <- s & uiState . uiModal . _Just . modalDialog %%~ handleDialogEvent ev becomes withLens (uiState.uiModal.singular _Just.modalDialog) (handleDialogEvent ev) |
In talking with another developer about the impact of these changes, the question arose: what is the performance overhead of https://github.com/jtdaugherty/brick/blob/refactor/event-state-monad/src/Brick/Types.hs#L115 I haven't done any performance measurement on the new branch and frankly I'm not sure how to do so. If anyone has thoughts on this, I'd love to know! |
In particular, what I'll need to investigate is whether the fact that |
@jtdaugherty just to bikeshed a bit on the name Maybe this could be named EDIT: or maybe it should be a Also, so far I have not used the second component of nestEventM' :: a -> EventM n a b -> EventM n s a
nestEventM' a em = fst <$> nestEventM a em |
But thanks a lot @jtdaugherty for that The exception is when we were inside mutateFirst :: Traversal' s a -> EventM n a () -> EventM n s ()
mutateFirst t em = do
ma <- preuse t
case ma of
Nothing -> return ()
Just a -> do
newA <- nestEventM' a em
t .= newA For use case like this: - s' <- s & uiState . uiModal . _Just . modalDialog %%~ handleDialogEvent ev
+ mutateFirst (uiState . uiModal . _Just . modalDialog) (handleDialogEvent ev) Maybe there is a more generic implementation (and better name) for that function, but hopefully it will be useful to other people porting magic lenses to the new API. 🙂 |
@xsebek Yes, this would be great if it's possible. I looked into it when I did the refactor and I got the impression that it wasn't, but I'd be happy to be wrong about that! But if a Zoom instance can't be written then I'd like to try to stick with
Okay, it's helpful to know that you needed a different version of
This is really interesting to me because while I can see the value in working with a |
@xsebek The branch now provides |
Thanks a lot @jtdaugherty! 👍 I investigated the On a moral level, I do not think the instance is impossible. But given that the current functions should take care of 99% of use cases, it is not a big deal. 🙂 |
Thanks, @xsebek. I do want to look into it more, and thanks for your work on it. I think |
In other news, in an attempt to clean up |
I also just updated the ticket description to incorporate the |
BTW, we have now finished updating the |
Thanks, @byorgey - I'm glad to hear that the changes led to improvements! |
Reading over the comments again, I am now planning on merging this to Thank you so much to everyone here for sharing your thoughts and trying out the branch! |
Hi everyone, Brick 1.0 is out and includes the changes discussed here! https://github.com/jtdaugherty/brick/blob/master/CHANGELOG.md#10 |
hledger 1.27 will use (and require) brick 1.0. The new code seems more straightforward and understandable - thanks @jtdaugherty ! The migration notes were very helpful. The one thing I didn't entirely figure out was how to compile with MonadFail across all GHC versions (so I just avoided it. For the record: it came to Prelude in ghc 8.8/base 4.13.. it's leaving Prelude in ghc 9.4/base 4.17.. it came to brick's EventM in 0.52.. [and was removed in 1.0, but only when building with base >= 4.13].. it shows in the hackage haddock for brick 0.73, but not for brick 1.0.) brick 1.0 seems to build well with old stackage snapshots/ghc versions, I tested back to lts-14.27/ghc-8.6 which required these extra deps:
|
Ah, sorry about the MonadFail thing. I forgot to mention that removal in the changelog. I ran into some irritation with MonadFail and decided it wasn't worth trying to deal with it since the MonadFail instance was not really being used for anything important. |
Ah, so it was removed. I think I can see that now.. except it seems to still be there when building with base < 4.13 / ghc < 8.8 ? Is that intended ? 1.0 doc: https://hackage.haskell.org/package/brick-1.0/docs/Brick-Types.html#t:EventM |
Thanks, I'm getting mixed up. I remember now that I made the derivation conditional. I forget why but on newer GHCs there was some issue with the derivation, so I just bailed and made it conditional. I'd be okay with reinstating it once I can look into it more (or someone knows what the proper fix would be). |
FWIW in hledger I also used only nestEventM', not nestEventM. |
Hi I am running into a problem with this migration and thought I'd ask here. Sorry if it's a dumb question. I have the following pattern for my event handlers - let newList = insertNewItem (t,m) _asMessages -- _asMessages is part of my AppState
in continue $ state { _asMessages = newList } For simple things like this I don't like to use lens. IMO, lens makes is more complicated to read, adds a layer of indirection and abstraction, and also takes up compile time. I would rather stick to simple record updates, atleast for simple things. Now in the migration guide it is written I should replace |
Ah okay, I figured something out. Not sure if this preferable or "officially recommended", but I'll note it down anyway if anyone else is wondering like me. I did not know MonadState s m => ASetter s s a b -> b -> m () So that seems like it modifies the state. So I tried with the following and it seems to work let newList = insertNewItem (t,m) _asMessages
in modify $ const $ state { _asMessages = newList } Admittedly, this is not nicer than just writing |
Hi @ecthiender - yes, some version of what you wrote is going to be required. If you want to avoid using lenses, then you'll need to do something like what you wrote, or abstract out the modification process a bit (which you'd have needed to do anyway, I think). If it were me, I'd probably write modifyMessages :: (Messages -> Messages) -> State -> State
modifyMessages f s = s { _asMessages = f (_asMessages s) } and then in the event handler, modify $ modifyMessages (insertNewItem (t, m)) That avoids the need for Incidentally, |
@ecthiender - I should also mention that the pattern you're encountering has an even nicer lens representation with asMessages %= insertNewItem (t, m) |
@ecthiender You can also use let newList = insertNewItem (t,m) _asMessages
in put $ state { _asMessages = newList } But I also prefer |
This ticket is a place for discussing the impact of work that is happening on the
refactor/event-state-monad
branch. The objective of this branch is essentially to refactorEventM
to make event handler code more modular, easier to write, and less verbose. This branch introduces several breaking API changes.If you write/maintain brick applications, I'd love your thoughts on how this will impact your applications (positively and negatively) if this were to be merged and released. I'd also love feedback if you actually try using the branch as well. Thanks!
Examples of the impact of these changes in action can be found in the demo programs. Here are a few examples:
Here's a summary of the changes and their impacts:
EventM
type is changing fromEventM n a
toEventM n s a
.EventM n s a
is now aMonadState
overs
. (The state monad introduced in this change is strict ins
.) This means that you can either use lenses or themtl
state monad API to manage your state. Therefore, event handler functions that previously took an explicit state argument and returned aNext
are now state-monadic over that state type instead. For example, a function that was previously written ashandleMyEvent :: s -> BrickEvent n e -> EventM n (Next s)
will now be writtenhandleMyEvent :: BrickEvent n e -> EventM n s ()
.Next
type is going away from the user-facing API. This meansEventM
blocks in event handlers no longer need to end withcontinue
,halt
, etc. Instead, the behavior previously provided by thecontinue
function is now the default behavior for anyEventM
block, andhalt
etc. need to be called explicitly to indicate what the library's main event loop should do once the event handler terminates. Functions likehalt
do not change control flow ofEventM
(i.e. do not cause early aborting).continue
itself was removed from the API. A practical consequence of this is that if anEventM
block now callshalt
, it cannot undo that by subsequently callingcontinue
sincecontinue
no longer exists.handleEventLensed
went away and has been replaced with support for thezoom
function frommicrolens-mtl
. This works with both lenses and traversals.Brick.Types
now re-exportszoom
for convenience.List
,Edit
, etc.) are now scoped to just the states they manage; e.g.handleEditorEvent :: BrickEvent n e -> EventM n (Editor t n) ()
. This means they are now used withzoom
, i.e.zoom someLens $ handleEditorEvent e
. This also impacted theForm
type whose field event handlers correspondingly changed to be scoped to their fields' state types.mtl
rather thantransformers
.To me, the biggest practical consequences of these changes are that:
EventM
code will be much cleaner, particularly if you like to use lenses to manage your state values. Event handlers can now use lens updates with themicrolens-mtl
API, e.g.,someLens .= someValue
etc.continue
noise since that is the default behavior of anyEventM
block unless otherwise specified byhalt
etc.EventM
a state monad over the application state type.EventM
will have to contend with these changes as well. (I suspect this will be rare.)The text was updated successfully, but these errors were encountered: