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

Encapsulated applications #492

Open
cdsmith opened this issue May 31, 2017 · 17 comments
Open

Encapsulated applications #492

cdsmith opened this issue May 31, 2017 · 17 comments
Labels
discussion Needs more information or major design decisions

Comments

@cdsmith
Copy link
Collaborator

cdsmith commented May 31, 2017

Definitely a speculative proposal, but it might lead somewhere.

The proposal is to create an abstract type to encapsulate and build up simulations, interactions, etc. Something like:

data Application state  -- abstract

defaultApplication :: state -> Application state

withTimeStep :: (Double -> state -> state) -> Application state -> Application state
withEventHandler :: (Event -> state -> state) -> Application state -> Application state
withPicture :: (state -> Picture) -> Application state -> Application state

applicationOf :: Application state -> IO ()

The various event handlers, time steps, and visualization functions would all be run, in some defined order. Pictures would be stacked with the outermost picture on top.

Advantages:

  1. No need to add a time step if it's not needed.
  2. Ability to abstract different effects that span events, time, and pictures (such as UI controls).
  3. Ability to incorporate more components (for example, sound?) without making the top-level entry point more complex.

The API is probably not right yet, but that's the idea.

@cdsmith cdsmith added the discussion Needs more information or major design decisions label May 31, 2017
@cdsmith cdsmith changed the title Encapsulated simulations Encapsulated applications May 31, 2017
@cdsmith cdsmith mentioned this issue Jun 1, 2017
@nomeata
Copy link
Contributor

nomeata commented Jul 20, 2017

If the Application is not abstract, then one can write Application-modifying functions, as in
restartable in https://github.com/nomeata/codeworld-talk/blob/master/Combinators.hs#L210 (or the more complicated examples there).

@cdsmith
Copy link
Collaborator Author

cdsmith commented Jul 21, 2017

I played with an initial implementation, but I am a bit disappointed. I'd hoped to build one application which could describe simulations, interactions, and collaborations... even eventually the reflex-based FRP interface from #486. But collaborations require StaticPtr arguments, and that scuttles the whole thing. We could leave out collaborations entirely, but that seems to defeat the point of unifying the API.

The best idea I have is to offer these alternatives for the entry point.

applicationOf :: Application state -> IO ()
multiApplicationOf :: Int -> StaticPtr (StdGen -> Application State) -> IO ()
unsafeMultiApplicationOf :: Int -> (StdGen -> Application State) -> IO ()

In this world (oddly) Application doesn't even know whether it's single-player or multi-player. If you add a multiplayer-style event handler to an Application and then run it as single player, it just always gets 0 for the player number.

If Application knew whether it was multi-player or not, then we're forced into a choice between runtime errors or too much type hackery.

@cdsmith
Copy link
Collaborator Author

cdsmith commented Jul 21, 2017

@nomeata Can we make that possible without exposing the guts of Application? Something like this could do it, perhaps:

applicationState :: Application state -> state
withExtraState :: state2 -> Application state1 -> Application (state1, state2)

resettable :: Application state -> Application (state, state)
resettable app = withEventHandler reset $ withExtraState init $ app
  where init = applicationState app
        reset (KeyPress "Esc") (i, c) = (i, i)
        reset _                (i, c) = (i, c)

@cdsmith
Copy link
Collaborator Author

cdsmith commented Jul 21, 2017

Actually, you don't even need withExtraState for this one. You could just use the init that's already in scope. But other examples, like pausing, would need extra state.

@cdsmith
Copy link
Collaborator Author

cdsmith commented May 4, 2018

I'm reviving this idea because I'm becoming more convinced that I want to teach event handlers before time steps. Here's my current proposed API.

data Rule state where
  TimeRule :: (Double -> state -> state) -> Rule state
  EventRule :: (Event -> state -> state) -> Rule state
  PictureRule :: (state -> Picture) -> Rule state
  SoundRule :: (state -> Sound) -> Rule state
  MultiEventRule :: (Int -> Event -> state -> state) -> Rule state
  MultiPictureRule :: (Int -> state -> Picture) -> Rule state
  MultiSoundRule :: (Int -> state -> Sound) -> Rule state

applicationOf :: state -> [Rule state] -> IO ()
multiApplicationOf :: Int -> StaticPtr (StdGen -> state) -> StaticPtr [Rule state] -> IO ()
unsafeMultiApplicationOf :: Int -> (StdGen -> state) -> [Rule state] -> IO ()

The non-abstract type for Rule allows for combinators like @nomeata 's pausable and resettable. However, it prevents easily extending the realm of possible behaviors. For instance, suppose you wanted to add the ability to vibrate a mobile device; this would be easily handled by adding a VibrationRule :: (state -> Bool) -> Rule state constructor; but that would break all existing clients. One way around this could be to leave the type abstract, but invent some combinators like onLeft :: Rule a -> Rule (a, b) or even Lens' a b -> Rule b -> Rule a.

Another design choice could be to separate event handlers, such as:

data Rule state where
  ...
  KeyPressRule :: (Text -> state -> state) -> Rule state
  KeyReleaseRule :: (Text -> state -> state) -> Rule state
  PointerPressRule :: (Point -> state -> state) -> Rule state
  PointerReleaseRule :: (Point -> state -> state) -> Rule state
  PointerMovementRule :: (Point -> state -> state) -> Rule state
  ...

This removes the need for one built-in algebraic data type (possibly balancing the addition of the new Rule type?), but at the cost of losing a lot of abstraction. Then I almost want a monoid instance on Rule so that you can at least handle key presses and releases in a single rule. Though I suppose there's no advantage over just using [Rule state] as the type with a monoid instance.

@cdsmith
Copy link
Collaborator Author

cdsmith commented May 5, 2018

It's become clear that exposing the Rule constructors is a bad call, so this is my current API proposal.

data Rule state  -- abstract

-- Simple rules.
timeRule :: (Double -> state -> state) -> Rule state
eventRule :: (Event -> state -> state) -> Rule state
pictureRule :: (state -> Picture) -> Rule state
soundRule :: (state -> Sound) -> Rule state

-- Multi-player rules.  If used in single-player programs, player is always 0.
multiEventRule :: (Int -> Event -> state -> state) -> Rule state
multiPictureRule :: (Int -> state -> Picture) -> Rule state
multiSoundRule :: (Int -> state -> Sound) -> Rule state

-- Speculative rules.  Can be used to add a time or event rule in a multi-player
-- program, which knows whether it's speculative or final.  For example, you
-- may not want to reveal a secret card until you're sure which player drew
-- card, or update the score until you're certain a player scored a goal.
speculativeTimeRule :: (Bool -> Double -> state -> state) -> Rule state
speculativeEventRule :: (Bool -> Int -> Event -> state -> state) -> Rule state

-- For multi-player programs, decides for how long it's okay to do client-side
-- interpolation of state after a delayed event is received.
eventInterpolationRule :: (Event -> state -> Double) -> Rule state

-- Applies a rule to part of the state.
subrule :: Lens' whole part -> Rule part -> Rule whole

-- Grouping parts, for better abstraction and encapsulation.
rules :: [Rule state] -> Rule state
instance Monoid Rule  -- via rules

applicationOf :: state -> [Rule state] -> IO ()
multiApplicationOf :: Int -> StaticPtr (StdGen -> state) -> StaticPtr [Rule state] -> IO ()
unsafeMultiApplicationOf :: Int -> (StdGen -> state) -> [Rule state] -> IO ()

The use of Lens' is probably overkill; it could be replaced with an explicit getter/setter pair instead. Too bad.

@nomeata
Copy link
Contributor

nomeata commented May 5, 2018

I have not been following closely, but that does not feel like CodeWorld any more...
If it's just about adding sound, you could add sound "pictures" that have no width or height, but, well, sound.

@cdsmith
Copy link
Collaborator Author

cdsmith commented May 5, 2018

@nomeata Thanks for the impression. I'm a bit worried about this, too. That said, do you have a sense of what specifically you value about CodeWorld that's being lost here? I'm trying to distinguish between (in myself, as well) real concerns, versus general change aversion.

The motivation, by the way, isn't actually about sound. It's about my grappling with how to teach interactions earlier in my middle school curriculum. I could add a new entry point with event but not step, but again, combinatorial explosion...

@nomeata
Copy link
Contributor

nomeata commented May 5, 2018

Well, it is hard to pin-point exactly. “The simplest thing possible”. A single function without any data types around kinda qualifies there. It seems that there is much more to explain (or to sweep under the rag with “you don’t have to understand this now, just cargo-cult this pattern) before users can use interactions.

cdsmith added a commit that referenced this issue May 14, 2018
unsupported, and just here to play around with.

See #492
@cdsmith
Copy link
Collaborator Author

cdsmith commented May 14, 2018

I'm still not sure what to do here. In the interest of having something to play around with, though:

https://code.world/haskell#PPZc1srHmFYppEUGyYF_8AA

cdsmith added a commit that referenced this issue May 15, 2018
This allows a Rule to be transformed by a getter/setter pair, which
allows for wrapping state.  See #492
cdsmith added a commit that referenced this issue May 15, 2018
Remove the unnecessary special constructors for single-player events
and pictures.  See #492
@cdsmith
Copy link
Collaborator Author

cdsmith commented May 15, 2018

Here's a version using subrule. This isn't a compelling use case, but it does show how to use it. https://code.world/haskell#P9HkPvoG5nR1eFndtzvPfEg

@nomeata
Copy link
Contributor

nomeata commented May 15, 2018

Looks like this will evolve into lens soon…

@Gabriella439
Copy link

Perhaps the requirements should be formally stated so that we can judge proposed solutions against those requirements

@fernando-alegre
Copy link

The examples above look complicated for such a simple program. The list of rules looks like an imperative control loop, not like rules, because they seem to be executed in the order listed instead of triggered as needed. Also, how would you add randomness and external events, such as calendar time and date or URL parameters. These are not provided in the current interface, but planning for them in the new interface would help future-proofing it.

@cdsmith
Copy link
Collaborator Author

cdsmith commented May 15, 2018

@Gabriel439 I don't have a list of requirements, yet. This is all pretty speculative so far. I wonder where it goes. I will also implement the Application type proposed above in a separate module. Collecting requirements to try would be great, and @nomeata mentioned some earlier (pausable and resettable).

@fernando-alegre Good point that this can be understood as an imperative program. It's not, precisely: it doesn't matter what order different rule types appear in. But when you repeat a rule of the same type, it comes across that way. It's possible the Application type will feel less imperative. I'll do that next.

cdsmith added a commit that referenced this issue May 15, 2018
@cdsmith
Copy link
Collaborator Author

cdsmith commented May 15, 2018

@cdsmith
Copy link
Collaborator Author

cdsmith commented Jun 28, 2018

My immediate goal in reviving this was to find a convenient way to teach event handling before time steps. For that purpose, we've settled on the new activityOf entry points. So that reason is now obsolete.

I'll try to catalog the remaining opportunity here.

  • It's more natural to add sound, vibration, or other features with encapsulation. (The counterpoint is @nomeata's suggestion that these could just be special pictures; but I don't want to do it that way. Picture should be representable by Point -> Color, and adding special sound-effect pictures is weird.)
  • Make it easier to build combinators on entire interactions, like pausable, resettable, etc. While this is compelling, I don't see a simple enough API that is both abstract enough for the first bullet, and concrete enough for this one! For this purpose, you can't really do much better than (state, Event -> state -> state, state -> Picture) (or your own product type, if you value type-safety over concrete values).
  • The possibility of more code reuse between single-user and multi-user programs. I think we should agree this is not a goal.

Anyway, I don't consider this in active development any longer. I'll leave the experimental modules around for now, since they aren't hurting anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Needs more information or major design decisions
Projects
None yet
Development

No branches or pull requests

4 participants