12 ms·
How I write Elm applications
- k_bx 6y agoOne nice trick I use to avoid boilerplate is SetModel msg which just sets new model state (passed in message itself). Good for simple and stupid messages like text input that just updates the model.
- Latty 6y agoThis seems like it would introduce bugs, where the model changes before that message is handled, and your old model you put in your message overwrites those changes.
- pyrale 6y agoIf the model changes, a new view is generated, so your message should take that into account. I can't remember of any concurrency issue arising from handling multiple messages from the view, to be honest. I don't know if there's a specific mechanism blocking them, but it's simply never happened in 3 years of building elm apps for a living.
- Latty 6y ago> If the model changes, a new view is generated, so your message should take that into account. You don't have any guarantees about when that will occur though. Updates and view rendering are run independently. I have an application that tracks mouse movement using pointer events for the user to draw, and I've definitely had a message from that come through after my model has updated and I'm no longer on that screen (and so nothing in the view generated from the model should produce that message), so it definitely can happen, even if you haven't encountered it. Even if you don't encounter it that way, there are a bunch of footguns using this approach (e.g: doing this with tasks, HTTP requests, anything that has a defined delay before the message comes back).
- k_bx 6y agoYes, the message needs to never be sent in a batch with other messages which change the model.
- dwohnitmok 6y agoBut it's not just that, you may e.g. be racing against messages from a subscription (this one is really dangerous if your subscriptions are things like listening for resizing, where that can mean your resizing message is effectively dropped and users see a really needed up UI).
- k_bx 6y agoCorrect, you shouldn't do that. On the other hand, event of simultaneously resizing and writing input is kind of rare, and this will be fixed on next resize/input event, so I wouldn't be too bothered.
- dwohnitmok 6y agoWell the tricky thing is you can't "not do that" because your Elm code doesn't have control over when subscriptions fire. Note that this applies to any subscription and that it is effectively dropped forever; the next input event won't recover it (so in the case of resizing only the next resize event will change things, not the next input event). So e.g. you could have a long running operation in a Web Worker over ports and then if its result comes in right when your user happens to be inputting something then it is dropped and never returns.
- bife_sans 6y agoJavascript is single-threaded so those sort of bugs can't ever happen.
- dwohnitmok 6y agoNo they still can. You can still have concurrency bugs in the absence of parallelism (the classic example of why concurrency and parallelism are not the same thing) as long as you have a concurrent API (which the Elm architecture is an example of). The basic problem is that there is an undefined delay between a message is issued and when `update` is called on the message which can be caused by other messages being in the queue. Here's an example with subscriptions and views. Time 0: A subscription arrives with message `A` which is added to the message queue in Elm's runtime. Note that one "tick" in Elm's runtime has not yet passed so we don't process the queue just yet (to see why this can happen we can either look at Elm's source code or note that the fact that Elm doesn't lose a subscription even if updates are busy processing forces there to be a message queue of length > 1). Time 1: Your view sends a message `B` which contains a snapshot of the model `M0` at Time 1. `B` is added to the message queue. Time 2: Your update function pops off the message queue and processes `A`, resulting in a new model `M1`. Time 3: The Elm runtime processes another tick and sees no new messages from either views or subscriptions. Time 4: Since the runtime is now done examining subscriptions and view messages, your update function pops off the message queue and processes `B`, resetting the model back to `M0`. At the end of this sequence you effectively haven't handled the subscription at all, and all this is on a single thread.
- jasim 6y agoTo add to that, there is a profusion of fine-grained actions like SetFirstName, SetLastName is endemic in Redux-inspired codebases. They are a lot of noise, but more importantly, they force us to keep all our domain logic in a single massive reducer, with no encapsulation between different sections. This can be improved by using coarse-grained actions like say `UpdatePerson(person)`. And this new `person` value can be obtained by the event handler itself when the user changes the name. However, the actual logic can sit inside a Person module, allowing us to do something like: `onChange=dispatch(UpdatePerson(Person.setFirstName(newName, currentPerson))` This approach lets us keep the behaviour close to the view while encapsulating the actual implementation inside a rich domain model.
- Latty 6y agoAs I note in my other comment, this approach is fundamentally unsound in Elm. The view is generated in its own thread synced to the browser's rendering system. It is entirely possible for the model to have changed without the view having been updated yet to match. If your model changes under your feet while you use this approach, it result in you silently overwriting that change with the old data.
- dwohnitmok 6y ago@Latty points out a very valid point that this is a concurrency bug. However, you can recover it in the same way that other concurrency system use optimistic concurrency (e.g. most STM implementations). You have a `numberOfOperations` field in your model that is incremented every time a message is processed and then check that your incoming new model has a `numberOfOperations` value that is exactly one larger than the current value. If not you then fire off a `Cmd` asking for a recomputation of the original message. However, that can get pretty hairy pretty quickly (since you may not have a nice way of asking for recomputation) so I'm not sure if it ends up working out in the end.
- ivanbakel 6y ago>I will at this point sympathise with people who have criticised Elm in the past for some of its most vocal proponents being frustratingly unhelpful. As always, Elm's biggest flaw is the sanctimonious nature of its design committee. No native modules, no custom operators, no lenses (would they really ban them?), etc. That second one even comes back to bite the code here, because compared to e.g. Haskell lenses, the Elm lens code is ugly and verbose. Having to manually write lenses instead of being able to auto-generate them with macros is yet another Elm failing, though to be fair, I don't know that macros are opposed by the committee.
- pyrale 6y ago> compared to e.g. Haskell lenses, the Elm lens code is ugly and verbose. Monocle isn't the only game in town for lenses. Arguably, you're still going to need to write the code that isn't generated, but the composition of lenses can be done using the generic composition (<</>>) too, which makes them much more elegant.
- karmakaze 6y agoWasn't there some controversy about being unable to enumerate Sum (aka Custom) types? Has this ever been resolved?
- rapind 6y ago“custom operators” is where you lost me. Elm for me is like a refuge from JavaScript where anything goes (good and bad). It’s not a hammer for very nail, but it’s simple and predictable for the most part. There are other frameworks like bucklescript / reason, purescript, closurescript, etc. with all the bells and whistles. Ultimately, I think Elm is in a good place. That being said you have to pretty quickly jump into the deep end once you reach a fairly moderate sized code base.
- ghayes 6y agoThe only feature I miss in Elm is do-notation. Idris does a great job of introducing do-notation without requiring inclusion in monad and simply being sugar for the bind operator.
- 6y ago
- klenwell 6y agoI've only recently started to pay attention to Elm after hearing some good things about it so this article was timely for me. I've also dealt repeatedly with the example the author uses, a wizard or multistep form, so I was definitely interested to see how it played out. And I was nodding my head in rhythm with the author until he got to the section on lenses. He writes: That’s 14 lines of code to update one single field. Not only is this single example somewhat confusing to follow, you also need to imagine how this update function will look when taking into account the five or so other fields just in the address record! This is — quite frankly — pretty terrible. The trick here is not to stare out of the window and contemplate rewriting everything in ClojureScript. Instead, the thing to do is write a whole bunch of lenses. To my eye, the lenses code he ended up writing didn't look all that much prettier or shorter to me. I guess it's more neatly structured and could even be organized its own file? Is the advantage that you're providing access at each level of the nested record as you code it out? Are there other approaches to this nested field problem?
- auslegung 6y agoI did elm for about a year professionally, and loved it. Reading the discourse thread he links, I''m guessing one of Richard Feldman's recommendations could be to flatten the structure. I'm sure there are times when that's no better than 14 lines to update 1 field, but it should probably be used more than it is. As for why his lenses code can be better than the 14 lines, you're exactly right, it can be extracted into its own file making it an easy-to-use API made of stable code. That lenses code is simple, though unusual at first glance. Once it's an established pattern in your codebase it'll take 5 seconds to grock an entire file of it.
- pyrale 6y agoSince 0.19 banned operators, I would avoid using monocle. If you don't need prisms or isos, I would suggest looking at https://github.com/bChiquet/elm-accessors https://github.com/bChiquet/elm-accessors, which uses basic composition to make composable lenses/traversals.
- dwohnitmok 6y agoIf Elm had extensible unions I would advocate as much as possible staying away from nested hierarchies. They lock you into a single access pattern. Unfortunately Elm does not, so nested hierarchies it is. Relevant Discourse thread that discussed this: https://discourse.elm-lang.org/t/idea-extensible-union-types-and-benefits-they-bring-for-real-world-elm-code/6118/5 https://discourse.elm-lang.org/t/idea-extensible-union-types...
- noncoml 6y agoEvan’s iron grip of how users should use the language and how they should write the code killed Elm.
- hawkice 6y agoI mean, this seems pretty harsh. Could I recommend just sharing your own experience and leaving judgment to the reader?
- hitekker 6y agoThe GP may be referring to this previous discussion https://news.ycombinator.com/item?id=22821447 https://news.ycombinator.com/item?id=22821447
- hombre_fatal 6y agoLazy takes beget lazy responses: Evan's iron grip on Elm is why Elm is good and simple and not a kitchen sink. Other solutions like Purescript already exist if that's what you want. But when you don't like how something is done that other people do like, there comes a time when you just need to pinch it off and leave the pot so other people can enjoy it. For some reason it's small niche languages that really bring out the hubris in some HNers. Maybe because they have a sense of ownership over it by virtue of merely using it, something they know is silly when it comes to larger languages.
- noncoml 6y agoYou are right that I was too lazy to even bother explain my statement. But you are wrong about the sense of ownership. The bitterness comes from Evan deciding to make backwards breaking changes to the new version of the language and forcing you to spend days to update your code base. It’s been so long that I don’t even remember what the changes were and can’t bother going back and look. One example that stuck in my mind is when he decided that users should not be allowed to use the prime(‘) character in variables name. It was not because that this character was now used in some other way. It was just him deciding he didn’t like that in variable names, so he not only forced it to the language codebase but to all the users as well. Of course this was easy to change but gives you an idea. The rest of the changes where in the same spirit but much more difficult to change. So it has nothing to do with small vs popular language. Imagine if Stroustrup had come out and said that since C+11 variable names would be case insensitive and operator overloading was forbidden.
- philh 6y ago> These functions almost inevitably end up needing something from the top-level application-wide state, so you’ll often see some type signature like this: > updatePersonalInformation > : Model > -> (PersonalInformationModel, Cmd PersonalInformationMsg) > -> (Model, Cmd Msg) > This is way too complex already, and this approach doesn’t even actually buy you anything. Is this accurate? I feel like this may not be what the author intended to write, since the update function isn't given any kind of message. The author's approach has data for every page of your app stored in the model at once, and available to every other page. Also, any page can edit the data for any other page and send messages to any other page. That makes me nervous. We went with having a small amount of global state that was visible to every page, and that every page could update (returning (Model, GlobalState, Cmd Msg)). That seems... basically fine? It's still a bunch of boilerplate, but not that much, and I don't think we've had significant problems with it. We need Cmd.map and Html.map but I don't know why the author thinks this is bad. (The boilerplate is mostly turning `(model, cmd)` into `(model, globalState, cmd)`. On the other hand, we don't need to wrap each of our messages with `ThisPageMsg`. Most pages don't need global state, and we let them use a regular Msg -> Model -> (Model, Cmd Msg) update function.) (Oh, I think very occasionally we do need to send a message up to the top-level as well. I don't remember exactly how we do that. It might be something like... those pages' update functions take a `PageMsg` and return a type `PageMsg PageMsg | GlobalMsg GlobalMsg`, and the top-level update function interprets `ThisPageMsg (GlobalMsg ...)` and passes through `ThisPageMsg (PageMsg ...)`. Again, boilerplate, but not significantly problematic, and only on pages that need it.)
- yakshaving_jgt 6y agoAuthor here. You're right, there was a line missing. That's now fixed. Thanks for spotting that. To address your other points: in just about every Elm application I've ever worked on, there was an initial ambition to separate and encapsulate things nicely, for reasons of safety or Clean Code or some other philosophical ideal. Then requirements change (which is normal and good — businesses change and evolve, and the product should mirror that). Then you discover that oh, you need a bit of state from one page in another. Oh, and modifying the state of something on one page should affect this other page. Hmm… Why does this view function now take seven arguments? Your final paragraph hints at the kind of engineering that inevitably begins to appear as a workaround to solving the wrong problem in the first place. All of it is extra complexity that doesn't need to exist at all. > The author's approach has data for every page of your app stored in the model at once, and available to every other page. Also, any page can edit the data for any other page and send messages to any other page. That makes me nervous. Why exactly this emotional reaction specifically? Does prior experience with bad surprises in Elm give you this intuition? Or is it from experience from a language like JavaScript where global mutable state is indeed something to be wary of?
- robertlagrant 6y ago> complex React/Angular applications, where invented complexity is the status quo and this kind of misdirection is simply what you have become desensitised to This. Redux is the worst for this. "Compose your reducers, which magic a state object out of defaults, and split your switch statement across a zillion files, and isn't it amazing how it's just Javascript?" - feels like a direct quote from every Redux tutorial and blog post ever. I yearn for "make your state an object, and have a big switch statement. Oh, and if that ever becomes unmanageable, only then think about reducers."
- ng12 6y agoThe second quote is what Redux actually is, the first is the result of massive amounts of cargo culting. It's actually why I don't mind Redux's boilerplate. It's punishment for using Redux when you don't have to.
- robertlagrant 6y agoWell, combineReducers etc are all in Redux.
- mrh0057 6y agoIt’s pretty odd right now that most languages are trying to add more ml features to classic oo and imperative languages. Design frameworks that are much easier to use in functional languages. Then try to make it work with imperative language or classic oo language and wonder why it seems to get add unnecessarily complexity as the app gets bigger. Seems like we are at a start of a transition period where we will move to more functional languages over the next 20 to 30 years. I wish a large company would design and modern ml and not try to make it backwards compatible with an existing runtime.
- valenterry 6y agoWhat are "ml" features? Do you mean FP features?
- 6y ago
- seertaak 6y agoIt's astonishing how much category theory jargon is required to build a frickin wizard. Wizards could be made almost entirely in a GUI - double click a field or button to associate code - way back when I started programming in the mid 90s. I really struggle to see how this is progress.
- yakshaving_jgt 6y agoCould you point out which part of the article uses "category theory jargon"?
- seertaak 6y agoThe whole part about lenses.
- yakshaving_jgt 6y agoThe use of lenses here is just one approach to solving a specific problem. It is still possible to write code with the same behaviour without lenses. Do you have a better suggestion for updating deeply-nested record fields?
- pyrale 6y agoStill lenses, but have you looked at https://package.elm-lang.org/packages/bChiquet/elm-accessors/ https://package.elm-lang.org/packages/bChiquet/elm-accessors...?
- yakshaving_jgt 6y agoI hadn't seen it until you linked to it from one of your earlier comments. I have no particular attachment to elm-monocle, and I appreciate you sharing :)
- ImprobableTruth 6y agoLenses are not a concept from category theory and to understand them no knowledge of category theory is required. Category theory just gives you generalizations (other 'optics') and better encodings, but none of that is mentioned in this post.
- tcldr 6y agoRight. So we need to pass the model for the entire app to our sub-update functions. This is the problem with the elm architecture; it requires a massive amount of boilerplate for anything more complex than a wizard.
- bife_sans 6y agoIt's easily arguable why lenses are not needed. The author himself admits that getting a value from a nested record is easy, so we don't need lenses to do that. Setting a value from a nested record can be easily achieved using plain functions -- no need for a complex abstraction. updatePersonDetails : (PersonDetails -> PersonDetails) -> Model -> Model updatePersonDetails fn model = { model | personDetails = fn model.personDetails } updateAddress : (Address -> Address) -> PersonDetails -> PersonDetails updateAddress fn details = { details | address = fn details.address } updateLine1 : (String -> String) -> Address -> Address updateLine1 fn address = { address | line1 = fn address.line1 } Notice that these functions are almost identical to the ones he wrote in order to build the Lenses. This update functions can be composed easily: updatePersonDetailsAddressLine1 : (String -> String) -> Model -> Model updatePersonDetailsAddressLine1 fn model = updatePersonDetails (updateAddress (updateLine1 fn)) model or, if you are feeling fancy: updatePersonDetailsAddressLine1 = updatePersonDetails << updateAddress << updateLine1 On the other hand, nesting records is discouraged because it brings no benefit at all and it creates the problem of having to write extra boilerplate. He wouldn't even have the problem that lenses solve if he had just stuck to the "flat" record. Btw: I think it was a great article! I didn't mean to sound too harsh criticizing; just wanted to clarify that point.
- dwohnitmok 6y agoIt's not perhaps a surprise that they're similar to lenses; what you've written is a part of the lens hierarchy (they're setters). This is why people often say you end up just recreating the lens hierarchy anyways if you work with nested data structures a lot. Lenses just combine getters and setters (so they're just a pair of two functions; libraries like Monocle just give them a name instead of making them a literal pair). They're nice if you have nested data structures that aren't records (e.g. naked product types a la "MyType Int Int Int"). But yes in general flat records are the way to go (but what about nested message types? Turns out that the lens hierarchy helps there too in the form of prisms. Or it would be nice if Elm had extensible union types so we could have flat message types too...)
- yakshaving_jgt 6y agoI don't think it's so easily arguable. 1. What you've written looks quite similar to the lenses approach. 2. I disagree that having nested records brings no benefit at all. What about when the types are generated on the backend so the shape of the model never goes out of sync across that boundary? For a non-trivial size application, I believe it's cheaper to use code generation to derive the type definitions, JSON encoders, and JSON decoders than it is to manually write all of it and try to make sure everyone on your team is disciplined enough to always make those changes.
- matsutsu 6y agoNice article. One counter point though, OP uses Elm-bridge to generate Elm types from Haskell. I'd advise not to. Elm-bridge is not maintained anymore, lacks documentation and some useful feature (no phantom types for example). https://github.com/Holmusk/elm-street https://github.com/Holmusk/elm-street is probably a more viable solution though I've never tried it!
- boxed 6y agoIt's probably better than to roll your own. And you do need code generation for elm.
- yakshaving_jgt 6y agoWhoops! I misremembered. I think we originally considered elm-bridge, but we're actually using haskell-to-elm[0]. I am familiar with Veronika's work and it is consistently of high quality, so I don't doubt elm-street would be a solid choice. In fact, elm-street looks like it supports generating exhaustive sum type lists, which might be the killer feature over the alternatives that leads me to switch. I'll make a correction to the article. [0]: https://hackage.haskell.org/package/haskell-to-elm https://hackage.haskell.org/package/haskell-to-elm