39 comments

[ 6.1 ms ] story [ 90.0 ms ] thread
Nice, looks very clean.

As in Elm, I’ve never fully understood why the events emit a value of some message type that gets interpreted into a function from ‘state -> state’ instead of handling with a state transformation directly.

Feels like boilerplate to me. I can see situations where this approach is useful, but should it be the default?

I guess you can always change your message type to ‘a -> a’ and interpret with ‘id’ if you want to.

> I’ve never fully understood why the events emit a value of some message type

In Elm, the Model can be updated faster than the DOM. In other words, the view is rendered at the speed of requestAnimationFrame but the model can change in between animation frames. This means that if you handle model changes inside the view you might get out of sync with the model.

Here is a program [1] that shows this problem. If you click "SnapShot" you will sometimes get the same number but sometimes get different numbers.

[1] https://ellie-app.com/dmddFd9whtYa1

Events are always serializeable, inspectable, and can be rewinded backward/forward in Elm. It is indeed producing a lot of boilerplate from my experience, but there is a certain complexity guarantee that comes with it in terms of keeping things simple that I, personally, enjoy.
Yeah, so the trade-off is you get some stuff for free that you might not need. Paying with some boilerplate. Fine design choice obviously.

Writing a lot of react (without redux) I never really miss serializeability and it’s easy to bolt on locally if you need it.

My personal outcome on this is that I am afraid to touch React after some time or some number of other devs were working on the codebase, while with Elm I’m not afraid to take over a large codebase that was written by someone else. This is very anecdotally and is my personal taste, but I totally stand by it and am having one of these experience right now.
This has been my exact experience. After not touching a project I can come back to it pretty much immediately. Also code location (files, one module per file) stops being so important, because the type system just catches everything. Once I learned how to use the occasional escape hatch, with ports, I could mix the random quick hack with the very solid system of elm.
The whole state is immune so it is not possible to handle state change locally in a sub-component. The parent component that embeds it needs to call it and place a new updated component state into a new whole state. But then the parent needs to know which events go into the component. The messages nicely allow for that.

Besides already mentioned ability to serialize messages it also allows to preprocess or filter messages in the parent.

Elm handicaps my attempts of "making impossible states impossible" because:

- I can't have Sets of arbitrary Types they must be `comparable`. I cannot create a custom comparable type.

- Records, Bool and custom Union Types are not `comparable`

- Bool and Simple Union Types could have been comparable if their comparison worked like they were Enumerated Types. Elm does not have Enumerated types.

Basically https://github.com/elm/compiler/issues/1008 made me look for better alternatives.

Did you find better alternatives? I'm currently trying to learn a bit of Elm. So far it's basically the only "Javascript framework" that I've been able to get up and running, while still understanding how things actually work.
This post links to a PureScript project that is probably the easiest PS framework around.

ReScript + rescript-react is a good alternative. Less safe, waaaay more verbose; but backed by Facebook.

This is quite cute (in TypeScript though): https://github.com/cyclejs/cyclejs

And Yew is super cool, it goes the WASM route (in Rust): https://github.com/yewstack/yew

> while still understanding how things actually work

was, methinks, the salient part of his point.

(I emphasize, I too think Elm is coherent enough so you understand how it works, which other Javascript frameworks lack.)

There is a library for Elm style apps in F# called Elmish. Unlike Elm, F# compiled with Fabel can call JS directly (and vice versa).
PureScript's build system is very good (Spago), and the language is too, but it is tougher to learn than Elm veering much nearer Haskell.
I can’t express how useful using Dhall as the configuration format for Spago has been compared to the standard JSON, YAML, or TOML files have been. Having typed configuration with function and can grab environment variables opens a lot of possibilities. One place that has been nice is changing target locale to build with an env var pointing to a different source folder for translations. Overriding packages is just merging two records. Normally this would have to all be handled in a in the build tool or using a non-terminating config language, as a result Spago and PureScript’s package system get to be much simpler.

The fact that Dhall’s aesthetics mirror those of PureScript (including Unicode) is a cherry on top.

I'm also this kind of i-want-to-know-how-things-work person and also never managed to stay at learning React, Vue etc. But a few months ago, I discovered Svelte and I absolutely love it. I recommend you to give a shot to their excellent tutorial which you can finish in < 1 hour https://svelte.dev/tutorial/
Elm is a walled garden (as advertised). Agreed this is annoying (for me coming from Haskell). But it does have a great learning experience for those new to FP.

All trade offs.

Can you (or anyone) point me to the files where this `comparable` logic is implemented in elm compiler? I'll try my hand at least making Bool and Record types Comparable. Or maybe implement comparable Enumerated Type in elm which behaves mostly like custom Union Type.
Even if you find the place in the `elmc` code base, fix it and make a PR: it will not be accepted. This is the walled garden aspect of Elm.

Either you accept the walls or you go with PureScript (which could be described as "Elm without the walls"; but then there is a lot more to learn and a whole plethora of frameworks where Elm has the one-to-rule-m-all-FW baked in).

Or create a fork of Elm, which I would support. I'm very happy using Elm for work over the past 3 years, but it's too limiting for advanced usage.
Maybe a good elm-to-purescript transpiler, that couples with the framework this post links to, is all we need :)
I think if someone implements this thing without breaking backward compatibility, then it will be valuable even as a fork of the original elm compiler. I think the License allows this.

Also, Evan has not closed this issue since 2015. Most probably it is about "too much work to get it right" rather than "I'm not convinced that we need it".

I think type classes is how one would want to fix this. Haskell and PureScript have 'm. But them come with a lot, A LOT, of work. Especially to get the std lib to use them!

With comparability you can use the operators (<,>,<=,>=,==,/=,etc.) on various types. This is not how Elm usually works. I.e. the `map` function is different for `List.map`, `Dict.map`, `Maybe.map`, etc.

In Haskell and PS you have type classes. Some types can be mapped, some can be compared, etc. Then the type class defines what functions should be implemented for such type in order to be part of the type class.

You think it's a small change, but it is pretty much what sets Elm and PS apart. Errors can also get a lot more complex with code that leans heavily on type classes.

It simply won't be merged.
FWIW this post is about PureScript, not Elm. The library is an implementation of the Elm Architecture in PureScript. Elm is a language. The framework that goes along with it is called "The Elm Architecture". The Architecture can be implemented in any language. Here's an implementation in TypeScript: https://github.com/gcanti/elm-ts
Also PureScript addresses all of the issues you've complained about above :)
You replied to OP, so I suppose they know.
Amazed to see PureScript is still around, I guess.

It was everywhere on conferences like 5 years ago.

Misread that as "PostScript" and was confused but curious.
Purescript-Concur is simpler, and allows you to use elm-architecture as well, but also allows you to use your existing functional programming toolkit to the fullest. It also has react bindings - https://github.com/purescript-concur/purescript-concur-react.

Here's what a counter example looks like -

    counter count = do
      button [onClick] [text (show count)]
      counter (count + 1)
Check out a comparison with elm-architecture at https://ajnsit.github.io/concur-documentation/ch03-01-replic....
That Elm comparison is so nice. I felt like half the time I was writing Elm, it was spent on the manual work of the things described in there.
Thank you! All the manual wiring in Elm can be very frustrating!

Also not having to expose all the action datatypes when they are only being used in a single place can really improve code structure.

Two questions that I didn't see answered in the docs:

- How do I manage state that isn't local to one widget? A counter's nice and local, but I don't see how application-wide state gets handled.

- How are data fetching and other asynchronous actions wired in?

I was wondering that myself. Here's an Ajax example: https://github.com/purescript-concur/purescript-concur-react...

It could use some type signatures, but it makes sense.

As for managing state, my understanding of the Elm Architecture is that there is one "global" state data structure, and various parts of it are handed down from parent to child. So my question would be the opposite of yours: what if I want local state? Is that possible? There are situations where some toggle being on or off isn't very important and keeping track of it in a global data structure is burdensome

Well elm architecture is just a pattern in Concur. You don't need to store things globally at all.

Currently the only thing to take care of is - when a child widget returns some value to the parent, if you want to reinvoke the child widget and somehow restore the previous local state of the child widget, you need to have sent that local state out to the parent previously. This means that you either add the local state to the child widget's return value, or use something like a wire to directly send the state to the parent or something else.

I am working on adding automatically persisted local state to Concur which would greatly simplify this. It would provide an API similar to hooks' useState (and the react bindings will actually use hooks for this).

1. Application wide state is easy since you can always access state from parent components in child components. The problem occurs when you want to update parent state from child components.

Fortunately, you can make your own abstractions in Concur. For example, you can use a "Wire" - which lets multiple child components share and update parts of the parent's state (https://github.com/purescript-concur/purescript-concur-core/...). Note how this abstraction just uses the high level Concur API, and doesn't depend on the UI backend. With a wire the code might look something like this -

  -- A wire into parent's state, accessible using `with`, and updateable using `wire.send`
  counter wire = with wire \i -> do
    button [onClick] [text (show i)]
    wire.send (i+1)
And you can compose several counters that share state like this (the first two counters will increment together -

  -- Parent component creates a wire using `local` and an initial value
  local 0 \w -> div []
    [ counter w
    , counter w
    , some
    , other
    , widgets
    ]
  -- Beyond this point the local state is gone and can't be accessed
  doSomethingElse
2. Concur widgets can perform sync effect as well as async effects using `liftEffect` and `liftAffect`, and the results of those actions are available within the monad just like other widget results. Does that answer your question?