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

Proposal: Mailbox Revamp #7

Closed
mgold opened this issue Jul 14, 2015 · 36 comments
Closed

Proposal: Mailbox Revamp #7

mgold opened this issue Jul 14, 2015 · 36 comments
Labels

Comments

@mgold
Copy link

mgold commented Jul 14, 2015

This proposal attempts to codify the ideas from this thread. This would be a breaking change to core, but no change to the language itself.

Motivation

As is clear from reading the thread, "mailbox" is has failed as a metaphor. This part of the language is, empirically, one of the hardest for newcomers to understand. A few other issues with the API:

  • The Address type is completely opaque, and hard to gain intuition about. However, it's really the only thing tying the API together.
  • That is, there's message to create a Message, and send to create a Task, but neither of these seem to fit very well.
  • forwardTo : Address b -> (a -> b) -> Address a is really confusing. It took me several tries to get my head around it. The average JS or Ruby dev is not comfortable with the phrase "contravariant functor".

Proposed Change

I'm not 100% on the names yet; please focus on the types.

type alias Dispatcher a = 
    { dispatch : a -> Message -- the big change
    , signal : Signal a 
    }

dispatcher : a -> Dispatcher a -- just renamed mailbox
send : Message -> Task x ()

Let's start by what's not in the API. There's no explicit message function aside from dispatch itself. This encourages people to use messages, which are safer (they can't perform arbitrary actions e.g. Http). And forwardTo drops out entirely, replaced by dirt standard function composition.

Finally, send takes a Message. Currently, send and message are both Address a -> a -> ..., implying parity. Now, it's clearly extra work to use a task. This implies (correctly) that a message is the primary type at play here. The exposed function plays very well with Graphics.Input, every function in which takes either a Message or an (a -> Message).

Next Steps

This proposal had support from both newcomers and regulars on the thread, though you're welcome to oppose here. As mentioned above, I think the biggest remaining hurdle is to find the right names. I think implementation will be fairly straightforward, although it's important to get the docs right.

@mgold mgold changed the title Proposal: mailbox revamp Proposal: Mailbox Revamp Jul 14, 2015
@rtfeldman
Copy link

Yeah this just looks flat-out incredible. I really hope I'm not missing something, because I've read this a couple times and just love it. 😻

@evancz
Copy link

evancz commented Jul 14, 2015

I believe this is a more unwrapped version of the definition of Address. I initially decided to wrap it up because you need to pass it around everywhere. My feeling was that it'd feel nicer to hold a thing instead of a function.

So as always, you gotta show code examples!!!!!!!! So here is the basic change to the Elm Architecture pattern:

-- current way
view : Address Action -> Model -> Html

-- proposed way
view : (Action -> Message) -> Model -> Html

Is that simpler? Maybe experienced users would want to wrap things up as an Address to make the types look nicer? I have lost perspective here.

With the effect routing stuff, you get this:

update : (Action -> Message) -> Action -> Model -> (Model, Effects)

I'll play with this idea as I continue exploring with the-social-network. Anyway, seems like it has potential, nice proposal!

@rtfeldman
Copy link

Some Dreamwriter code cleaned up and rewritten in this style: (before), (after)

(Action -> Message) is 5 characters longer than Address Action, but I really appreciate the simplicity and clarity of "I know what an Action is because I defined it, I know what a Message is, because that's an extremely familiar term, and I know what a function is because I use those all the time." I also love the way onClick dispatch (SetFullscreenMode targetMode) reads.

Even as an advanced user who knows what Address is, the (Action -> Message) signature feels comforting to me and worth the extra 5 characters. I expect I would keep writing it that way even if there were an alias available!

@mgold
Copy link
Author

mgold commented Jul 14, 2015

Thanks guys.

Maybe it's a matter of style, but I typically don't parametrize my update or view over the address/dispatcher. I'd just have the dispatch or the mailbox at the top level. More like:

update : Action -> Model -> Model
view : Model -> Html -- or Element
reaction : {- Action -> -} Model -> Effects

If you choose to parameterize, that's not a big problem. The type signature for view becomes slightly more complex, but also more explicit. It's just a higher order function, hardly unprecedented. [EDIT: Richard makes a case for this in the preceding comment.]

But let's look at the call site. In my Socket.io chat app, written before this proposal, I unwrapp the address immediately:

stateMB : Signal.Mailbox (State -> State)
stateMB = Signal.mailbox identity
sendState : (State -> State) -> Signal.Message
sendState = Signal.message stateMB.address

Notice that sendState has the same type of dispatch that this proposal would give me out of the box. Another use:

submitMB = Signal.mailbox ()
submitButton = Graphics.Input.button (Signal.message submitMB.address ()) "Submit"

With the proposal in place, the parenthesized expression becomes submitMB.dispatch ().

Let's look at a standard text field:

--reminder:
Field.field : Style -> (Content -> Message) -> String -> Content -> Element

type alias State = {content : Field.Content, submit : String}
field : Signal Element
field =
    let base = Field.field Field.defaultStyle
                   (\fc -> Signal.message stateMB.address (State fc "")) ""
    in Signal.map (.content>>base) stateMB.signal

The second argument to Field.field becomes

(\fc -> stateMB.dispatch (State fc ""))

If State only took one argument:

(stateMB.dispatch>>State)

For checkboxes, one might have

type alias ParamID = Int
type Action = Checked ParamID Bool | NoOp | ...
actions = mailbox NoOp
-- later
myCheckbox2 = Input.checkbox (actions.dispatch>>(Checked 2))

That is, by carefully choosing the order of arguments to tags, and by treating tags as curried functions, you can make it really pretty. (And I don't think that's a particularly advanced language feature; it's similar to record constructors.)

@Apanatshka
Copy link

👍 LGTM

@mgold
Copy link
Author

mgold commented Jul 14, 2015

One more code example, this time from the SocketIO library itself:

connected : Address Bool -> Socket -> Task x ()
connected : (Bool -> Message) -> Socket -> Task x ()

I think it's clearer from the second signature that the function will obtain a boolean using the socket, and then send it as part of the task.

@TheSeamau5
Copy link

I don't really have a strong opinion on this one because I got really used to the ideas of addresses and I like the concision in the type annotation it offers. When it comes to container components (whose type annotations can get super hairy at times), the change would look as follows:

view : (ChildContext -> (childAction -> Message) -> childState -> Html) 
     -> Context -> (Action childAction -> Message) -> State childState -> Html

as opposed to

view : (ChildContext -> Address childAction -> childState -> Html) 
     -> Context -> Address (Action childAction) -> State childState -> Html

The really weird part about this is that, in the proposed version, view takes a function which itself takes a function. This could very easily get weird from an API design perspective. (by the way, I have way uglier type signatures for container components if you're interested).

@mgold
Copy link
Author

mgold commented Jul 14, 2015

You could always add you own type alias. Better still might be to rework things. I would partially apply view's first argument, rearranging its arguments if necessary, and use function composition so it already has the dispatcher in its closure. So you might have

render : ChildContext -> (childAction -> Message) -> childState -> Html
view : (childState -> Html) -> Context -> State childState -> Html

Where render is a general purpose template, view is part of the component framework, and you're expected to apply render down to the first argument of view.

@TheSeamau5
Copy link

Ok, fair enough. But then how do you deal with actions that are not necessarily destined to the children?

Consider the following example

type alias State childState = 
  { children : List childState 
  , ... -- some bells and whistles here 
  }

type Action childAction
  = ChildAction Int childAction 
  | Resize (Int, Int)
  | ... -- other container specific actions


update : (childAction -> childState -> childState) 
      -> Action childAction -> State childState -> State childState 


view : (ChildContext -> Address childAction -> childState -> Html) 
    -> Context -> Address (Action childAction) -> State childState -> Html

What I'm trying understand is how do you get both the children and the parent to send events (of type Action childAction) which is normally done via Signal.forwardTo. As in, could you please provide some example code to see how it would look like. You can use my grid component from my tutorial if you want. (it only reroutes actions to its children but you can imagine adding a dummy action like NoOp that is sent via onClick just for the sake of discussion)

@mgold
Copy link
Author

mgold commented Jul 14, 2015

Whenever you used to do

newAddress = Signal.forwardTo f mailbox.address

you can now do

newDispatch = f>>mailbox.dispatch

For example - the Int in ChildAction is an identifier, right? -

makeChild : childState -> (childAction -> Message) -> Child

actions : Signal.Mailbox (Action childAction)
actions = Signal.mailbox NoOp

myChild = makeChild someState ((ChildAction 4)>>actions.dispatch)

This means that the children don't track their own identifiers; they are instead placed into the closure of the child's dispatch function by the parent.

@texastoland
Copy link

Thanks for this! So much more clarity than the original thread. Part of this conversation would be great feedback on Evan's new component architecture with effects. I'm especially curious about @mgold's dispatcher-less function signatures. I'm planning to fork and and experiment with doing that in update. Love to see you and @TheSeamau5 weigh in!

@mgold
Copy link
Author

mgold commented Jul 14, 2015

Go for it! Implementation shouldn't be too hard, we're basically exposing an opaque type.

@texastoland
Copy link

Do you have examples of your dispatcher-less signatures somewhere? Does it okay nice with nested components?

@TheSeamau5
Copy link

Mmmm... i see. So, for example, one way I could do things for the children, is, when I do the List.indexedMap thing I could just do:

let 
    viewN index child = -- the view function that gets called on the children
      let 
          childDispatch = 
            ChildAction index >> mailbox.dispatch -- continue to use the index as an identifier
          ...
      in 
         ...
in 
    ... 
     ( List.indexedMap viewN state.children ) -- somewhere we call List.indexedMap

Is that correct?

@texastoland
Copy link

Hopefully on topic: I don't understand Message (preexisting) at all (clearly I was using send wrong before). Intuitively I'm sending an Effect (I feel like such a type should exist) for processing out of band? Is there a reason for it to be a proper type rather than an alias? What is the significance of blowing away its type params with empty tuples? How should you manage errors then?

@mgold
Copy link
Author

mgold commented Jul 14, 2015

Hassan: Yes, that looks like it would work.

Texas: A message is not an arbitrary effect but an effect that will, when run, produce a value on a signal. Which value is determined by the argument to dispatch (or Signal.message); the signal is the one associated with the dispatch function (or address). So I can create a message for a UI button to send whenever it's clicked, and it can send that message multiple times, and the type system knows that it's not allowing a button to do anything more than make an event on a signal. Which explains the task underneath: messages are tasks from an implementation standpoint, but not from a safety standpoint.

@mgold
Copy link
Author

mgold commented Jul 15, 2015

I have implemented the change in a branch here. You should be able to drop in the new Elm file (and delete the cache) and play around with this. Still open to suggestions on names and docs; discuss here or send a PR.

@evancz
Copy link

evancz commented Jul 15, 2015

I think you can just do your proposal as a library on top of the existing stuff. No need to modify core.

@texastoland
Copy link

I understand his proposal is to modify Core? Motivated by questions about unfamiliarity of Mailbox, opaqueness of Address, and incongruency of signal with the mailbox metaphor. Codifying it just gives us the opportunity to play with it. I'm particularly curious how to surface errors.

@mgold
Copy link
Author

mgold commented Jul 15, 2015

Yes, the proposal is to modify core... in the thread you started you can see that the confusion that Address and Mailbox have caused. Richard has attested that he's walked through it with newcomers and they've found it confusing. This API makes things more transparent, and drops two functions, one of which is one of the least intuitive currently in core (forwardTo, and IMHO).

That being said, as a prototype it may work better as a separate module than as a monekypatch. However, I was also seeking feedback on the docs. The current docs are pretty unhelpful and the examples are just wrong in a few places (port actions : Mailbox Action being the most egregious - why is it a port?).

@mgold
Copy link
Author

mgold commented Jul 15, 2015

I updated my Socket.io chat client to use dispatchers defined in a separate file. I renamed the module to Signal on import, to better simulate what a change to core would look like. I stopped short of changing the socket.io code itself; this was all Graphics.Input.

Turns out it's not possible to do as a 3rd party library because I can't implement send : Message -> Task x () using the current implementation which takes an address. I think this is part of the weakness of the API - you're given an address, but the address isn't good for anything directly, since you need to make either a message or a task. Some third party libraries take addresses, including mine, and Evan called doing so a "trick". I think taking a message (or function to make one) is a much better approach (even if it is the same thing that an address wraps). Elm-html takes addresses and functions; elm-svg takes messages.

@mgold
Copy link
Author

mgold commented Jul 22, 2015

So what are we blocking on here? Examples? Not time yet? A PR? Something else?

@evancz
Copy link

evancz commented Jul 22, 2015

Time and additional exploration. I am doing some work that led to an alternate way to achieve the same goals. I am not ready to share that work yet, but it needs to be finished before we can make decisions here.

@mgold
Copy link
Author

mgold commented Jul 22, 2015

Okay, please ping back when you're ready.

@rgrempel
Copy link

@mgold said "Turns out it's not possible to do as a 3rd party library because I can't implement send : Message -> Task x () using the current implementation which takes an address."

I think I must have been unconsciously remembering this when I submitted this pull request:

https://github.com/elm-lang/core/pull/356

... which would create the function:

Signal.sendMessage : Message -> Task () ()

Now, the new function I am proposing there would become irrelevant if the more profound proposal here is implemented in core. However, I think that accepting my pull request would make it possible to implement this proposal as a 3rd party library. So, it might be a useful transitional step. Since the new function in that pull request would be a smaller change to core than the proposal here.

@skybrian
Copy link

@mgold said: "A message is not an arbitrary effect but an effect that will, when run, produce a value on a signal. Which value is determined by the argument to dispatch (or Signal.message); the signal is the one associated with the dispatch function (or address). So I can create a message for a UI button to send whenever it's clicked, and it can send that message multiple times, and the type system knows that it's not allowing a button to do anything more than make an event on a signal. Which explains the task underneath: messages are tasks from an implementation standpoint, but not from a safety standpoint."

This is a clear and useful explanation, but for me it raises other questions. If clicking a button sends an http request directly, then you can locally see what the button will do without reading any other part of the program, and you know the button isn't going to do anything else. If it sends a message to some arbitrary signal, to determine the effect of the button click, I need to figure out which signal it is and find all usages of that signal to figure out what effects it might trigger. If it's harder to determine the effects, is it really a safety guarantee?

So it seems like sending a message to a signal might be useful as a form of architectural decoupling, but putting an effect object directly on a button might be simpler to understand, particularly for a beginner who is already familiar with event handlers in regular HTML pages.

The risk is that, though I don't think it technically violates purity (an effect is a value), it looks suspiciously unlike functional programming. But I'm not really a functional programmer; I just dabble from time to time. Perhaps someone can explain the safety guarantee better?

@skybrian
Copy link

It seems like if the goal is to restrict what a view can actually do, view types such as Html shouldn't be able to send arbitrary messages, because it's analogous to declaring a function of return type Object. We could use a parameterized type to narrow it down, perhaps something like:

{- An Html view whose event handlers all send values of type a -}
type Html a

onClick: a -> Attribute a

div: List Attribute a -> List Html a -> Html a

{- Plug the view into a particular signal -}
bindEvents: Html a -> Address a -> SomeUnrestrictedViewType

@mgold
Copy link
Author

mgold commented Aug 22, 2015

@rgrempel Yes, it sounds like adding that feature would allow this proposal to be implemented as a third party library. But that will complicate things further, by adding another function to the standard library. (Notice that this proposal reduces the size of the standard library!) If one wanted to test the proposal before it was accepted, one could do that by either patching core with your PR and writing the 3rd party lib, or patching core directly, as I've done.

@skybrian Yes, it's possible to "inline" things somewhat even today, using an address of a function type. I explain this approach in the Transformations section of this document. The problem is that, in a sufficiently complicated program, you want an explicit action type and a centralized step function. What if the button needs to behave differently based on state (e.g. not resend a form submission)? What if you want multiple buttons to do the same thing, which you want to describe only once? What if you want to make a game where keys can be rebound, so you can't assume the same key always means the same action?

The problem with your second solution is that it restricts all event handlers to sending values of the same type to the same address. With messages--which include both the value sent and a reference to the mailbox to send it to--you don't have that restriction. Polymorphism is not the answer to every problem.

@skybrian
Copy link

@mgold I think these are valid design choices depending on what you want to do. I'm comfortable programming with languages that allow unrestricted effects in a view's event handlers. I also think the traditional Elm architecture (with a single model and action type) is a pretty good way to go.

re: "The problem with your second solution is that it restricts all event handlers to sending values of the same type to the same address."

Yes, that's a feature. It would be a way of intentionally restricting designs to the traditional Elm architecture. You can still can add whatever alternatives you like to your action type, including a Transformation function as you describe. You could also derive whatever signals you like from the top-level view's signal. But it would prevent triggering an effect when there' s no action declared for it, and ensure that all view-triggered events pass through the main signal.

So I think the high-level design question for Elm is what level of safety the language should require for view-constructing functions. It seems like a odd middle ground to have a fairly loose restriction (views can contain any message, but not any task) if it doesn't place any restriction on effects. If it's okay to allow unrestricted effects, why not have all event-sending views accept a task parameter, since it would simplify understanding? On the other hand, if the goal is to improve safety by forbidding any actions that aren't declared in a view-constructing function's type signature, it seems like we need another type constraint to actually accomplish it? Just declaring, "it returns a view, which can trigger any event" adds little safety.

@mgold
Copy link
Author

mgold commented Aug 23, 2015

Aside from not being able fire off arbitrary tasks (directly), a Message also ensures that a UI element will always send the same action value for an event. (The value can be based on the UI's state, like a checkbox, but a button can only send one message.) This makes it much easier to handle these action values, and decide what if any effect to take, in one place. The philosophy is that UI inputs shouldn't be particularly intelligent (which makes them very safe), as anything they report happened has to be judged by the step function, which has access to the current state.

Yes, views can send messages to different mailboxes, even though it's usually best to have one action type and send it to one mailbox. I don't think it's worth "intentionally restricting designs to the traditional Elm architecture" as sometimes you need to do something a little odd. Rather, the idea is to make views simple and easy to understand, so when you write the step function, you're working with the abstracted action types rather than button presses.

This is getting really far afield from the original proposal, which is a comparably small change from the status quo compared to your proposed bindEvents idea. I will also point out that the original proposal makes the library much simpler, both conceptually and by reducing the number of functions.

@skybrian
Copy link

I don't think I'm explaining myself all that well, but I'll try one more time and then let it drop.

If I can choose the mailbox when I create a view, there's no difference in power between embedding a message into the view versus embedding a task into the view, because I can convert the first into the second. For example, see [1].

In a library, I can change mysteryButton1 to mysteryButton2, ship a new version of the library, and the type doesn't change. Anyone who uses the modified mysteryButton in their view is none the wiser; it went from doing nothing to logging clicks under the covers and the caller's step function never saw the event.

Even though the logClick task is not technically embedded in the button, from the caller's perspective, the button acts exactly as if it were. So there's no real architectural restriction at all, just some extra steps you have to take when creating the button to make it do a task.

If sandboxing views is a non-goal and the status quo is fine (which is a valid choice), It seems like we could stop worrying about illusionary safety and simplify even further? Perhaps ports and mailboxes could be merged somehow? Why couldn't a mailbox execute a task?

mysteryButton3 = Html.button [
Events.onClick someAddress (logClick "button3")
] [ Html.text "Click me" ]

But that's also a different proposal.

[1] https://gist.github.com/skybrian/233c69449abc23c48e21

@mgold
Copy link
Author

mgold commented Aug 24, 2015

It's a somewhat contrived example in that mysteryButton1 didn't have a click handler, although it's certainly possible to get around that. However, ports aren't allowed in modules other than main, so it would be tricky to do something impure in a library. If you have further concerns, please bring them up on the mailing list.

@skybrian
Copy link

Aha, the restriction on port creation is the piece I was missing. Thank you for your help, and sorry to be a bother.

@evancz
Copy link

evancz commented Aug 25, 2015

I am closing this repo down. This idea makes sense. I don't want to keep messing with this API again and again and again. Especially when it's the same group of people who are involved in the naming each time. We are on iteration 3 now, and each time it felt like we really had it this time. I think this is something to consider in the "changes to core" issue though.

@mgold, can you move the key idea to a gist? It's fine if it's super short. I get the idea. Once you do that, chat me or email me and I'll add it to the thread about potential additions / changes to core.

@evancz evancz closed this as completed Aug 25, 2015
@mgold
Copy link
Author

mgold commented Aug 25, 2015

Okay, I've made the gist and will email you the link. Thanks to everyone who commented.

@ghost
Copy link

ghost commented Sep 13, 2015

One more "outsider" voice -- yes, please. Anything that consolidates the terminology around task, effect, address, mailbox, port, and message is an improvement.

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

No branches or pull requests

8 participants