Skip to content
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

halt doesn't act like mzero #23

Closed
mitchellwrosen opened this issue Nov 22, 2015 · 10 comments
Closed

halt doesn't act like mzero #23

mitchellwrosen opened this issue Nov 22, 2015 · 10 comments

Comments

@mitchellwrosen
Copy link

Hi, I've been playing around with brick a bit and I've noticed that halt doesn't behave like mzero. I'd like to write code that looks something like:

myHandler :: MyState -> Event -> EventM (Next MyState)
myHandler st evt = do
    when (evt `isKey` KEsc) $
        halt st

    ... further event handling ...

This is actually a somewhat larger problem for me than just when vs. if-then-else: I'd like to write composable, locally stateful event handlers using auto or similar, and step them forward in the top-level event handler them. However, currently I have no way of "inspecting" the return type of a handler (Next MyState) to see if indeed I should halt or continue.

I can easily work around this by defining my own ADT that encodes how to proceed, but it would essentially be almost identical to what already exists: either Continue or Halt.

Does this make sense? Thanks!

@mitchellwrosen
Copy link
Author

Also, for what it's worth, I've had to define my own

newtype EventM' a = EventM' (EventM a)
  deriving (Functor, Applicative, Monad)

because EventM is a type synonym and thus cannot be partially applied in a type such as Auto EventM Event AppState.

@jtdaugherty
Copy link
Owner

I've checked in some changes that address your second comment (making EventM a newtype).

As for the first bit: I don't know anything about the auto package so it isn't immediately obvious to me what changes in brick would make it easier to integrate with auto. I've taken a look at some different approaches to making halt do what you want, and although I agree making it "return immediately" would be nice, I don't know of any approaches that would work at present. The issue is that the fact that EventM can "halt" or "continue" is not related to the monad at all, but to the Next type that is required to be returned by event handlers. So to make the monad capable of returning immediately, the "halt/continue/suspend-and-resume" features would need to be merged into the monad. My experiments suggested that the EventM type would need to be tagged with the state type of the application to permit a "halt" operation to match the type of the state expected to be returned by the event handler (e.g. EventM s (Next s)). But I think that is pretty messy, and adding such a type variable infects more innocent code than I'd like (and requires the type variable to be added to other types that don't have anything to do with it).

Without more information I don't think I can embark on a refactoring. I'm happy to consider patches if you have ideas on how to implement this.

@mitchellwrosen
Copy link
Author

I haven't looked at the internals at all, but an API that seems more natural might be something like

data EventF a where
    Halt :: EventF a
    Suspend :: IO s -> (s -> a) -> EventF a

instance Functor EventF where ...

newtype EventM a = FreeT EventF (ReaderT (Map Name Viewport) (StateT EventState IO)) a
  deriving (Functor, Applicative, Monad, MonadFree EventF, ...)

continue :: s -> EventM s
continue s = pure s

halt :: EventM a
halt = liftF Halt

suspend :: IO s -> EventM s
suspend action = liftF (Suspend action id)

-----

appHandleEvent :: s -> e -> EventM s
defaultMain :: App s e -> s -> IO ()

@jtdaugherty
Copy link
Owner

Thanks for writing that up! halt needs to be modified to take a state parameter of type s since that's a requirement of the API. I'd also like to know whether there are any viable approaches that don't involve using free.

@mitchellwrosen
Copy link
Author

Ah, it was just a suggestion to allow you to move the Next semantics from the return type into the monad, as you mention. What's wrong with free if I may ask?

Alternatively - you could perhaps expose the Next ADT in some public Internal module. That'd suffice for my use case, although again, I can easily work around the problem by e.g. returning Just MyState to indicate continue, and Nothing to indicate halt.

@mitchellwrosen
Copy link
Author

Here's a brick/auto adapter module from some work on a reddit client if you want to take a look. It's a bit kludgy and likely going to change eventually.

https://github.com/mitchellwrosen/reddit-cli/blob/master/src/Brick/Auto.hs

@jtdaugherty
Copy link
Owner

I'd like to avoid exposing the internals of halt, continue, and suspendAndResume since I'd like to hide the implementation details. My objection to free is due to not understanding it, not being able to find any tutorials that clearly motivate it in this case, and wanting to avoid seemingly heavier abstractions where lighter ones will suffice (thus my desire for evidence that it is essential to a solution). If you can fill me in on this, that would be helpful!

Here's some extra context: my intention with the existing flow control API was to provide a mechanism for (only) the top-level event handler with the expectation that details about how to manage and compose event handling and state mutation would be a matter of doing pure operations rather than building up a library of EventM actions. Put another way, I imagined that composition would be free because most event-handling code wouldn't be written in EventM at all. I imagined most of it would be pure (except for handleEvent) and that if an application has a more sophisticated notion of flow control, it could be implemented in any way necessary and embedded in (and translated to) the expectations of EventM. I never intended for EventM computations to be especially composable because of this mentality. This is also why I would shy away from using EventM in a "return-immediately" imperative programming style since in my experience it has the potential to obscure event-handling logic and resist refactoring.

Thanks for linking to that demo module! That helps illuminate what you're dealing with in the auto/brick interaction. I guess if it were me doing it (again, speaking from a position of ignorance on auto), going on what I said above, I wouldn't write the Auto values in terms of EventM. It seems like that's the root cause of the need to instrument the rest of the API with the ability to deal with the results of the auto computations.

@mitchellwrosen
Copy link
Author

Makes sense, and I'm definitely exploring this "imperative-ish" game-loop type world myself for the first time in Haskell. In fact, looking back over my code, I'm not yet even using auto to its full potential, which is isolating stateful computations from each other. For that reason, I could have just used ordinary functions that mutate the global state... which is exactly what the original API is!.

For understanding free, I recommend some of Gabriel Gonzalez's posts such as this one.

@jtdaugherty
Copy link
Owner

Since it's been a while on this issue: is this a show-stopping problem for anything you need to do?

If not, I'd like to close this since I'm not inclined to make the change. I looked into free and wrote some toy programs with it but I don't grok it well enough to feel comfortable integrating it into a core part of my library.

@mitchellwrosen
Copy link
Author

Nope, it's not a show stopper by any means.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants