It comes down to the essential difference between these 2 pieces of code:
class SomeClass {
String state;
void update(String val) { this.state = state }
}
and
type alias SomeClass =
{ state:String
}
update : String -> SomeClass -> SomeClass
update val this = { this | state = val }
Snippet one produces a new object and overwrites the old one with it. Snippet two produces a new object, but leaves it up to the caller to decide where to put the new one.
In that sense immutable FP is more powerful than OO because we don’t always have to take those 2 actions together.
If we are trying to take something away from this that could improve Elm, it might be that a little bit of syntactic sugar to help with updating nested records could be a welcome addition.
There is no simple option. If it would have been it would have been discovered by now.
From my own investigation into this subject, there could be a way out of this by some kind of clever datastructure that would augment the current virtual-dom nodes with the possibility of declaring your own web-components-like structures. This is however no simple matter and a lot of thought needs to go into it.
A partial solution for accidental widget state would be a concerted effort to make sure web components work well with the virtual DOM including making sure they aren’t accidentally created, destroyed, or reused because the DOM diff either didin’t recognize continuity of identity or misrecognized it. It’s not hard to implement Elm code that exhibits the continuity issues with input fields. (Misrecognition is always addressable by using Html.Keyed but since there is only a one element look ahead in the DOM diff, Html.Keyed isn’t always sufficient to preserve identity and hence accidental state.)
A further solution would look at how to write components using Elm presumably as something like independent MVU programs.
I know people have demonstrated proofs of concept for these things but to really call it a solution takes pounding through the issues to eliminate the subtle dangers.
Of course, the first step in solving a problem is admitting that you have one. In this case, it’s admitting that accidental state is a thing and we are already using it every time we create an input field.
You know what’s simple? Go to statements are simple, The structured programming strictures against using go to statements made programs harder to write — i.e., more complicated — because you couldn’t do things like just exit out wherever you wanted to.
You know what’s simple? Monolithic programs and global variables rather modularized programs and heap allocated data. Modularized programs require going through APIs to get to data and you have to pass around more data references when you don’t just use global variables. Monolithic programs with global variables generally have significantly simpler code.
You know what’s simple? Mutability is simple. How do I increase the count of the number of cats? Increment the cats counter is a lot simpler than return a new model with a different value for the cats counter.
I am actually all for simplicity. I prefer C over C++. I prefer Scheme over Common LISP. I prefer Elm’s semantics over ML’s. But I also know that “simplicity” is often an argument for all sorts of practices that have proven problematic over time when looked at in the context of building large, reliable programs.
Don’t confuse simple with easy. Mutability is easy NOT simple. It introduces time into the equation because you suddenly have a time before and a time after the mutation. I highly recommend Simple Made Easy presentation by Rich Hickey.
Mutability is like go to statements. It makes the immediate code “simple” because it reduces the number of “extraneous” details and just let’s us focus on the immediate task. In the small, “simple” and “easy” often look the same. My point was that pushing back against structure because it is less simple can lead to the immediate task being simpler, easier, more approachable, pick your term, but it can lead to complexity when considered in the large.
Now, this is not to say that structure is always good. There are many structural patterns that exacerbate complexity or impede comprehension and those should be resisted.
I don’t think MVU suffered from those problems, however. It did tend to introduce a lot of boilerplate but the boilerplate had a rhythm. It didn’t really provide an answer for how parents should interact with non-opaque children. If applied from the perspective that everything had to be an MVU component down to the lowest detail, it was grossly excessive and the antidote of asking “could you just use a view function here?” is very useful.
While I think that program combinators are highly unlikely to help, I also think that an argument for always using the simplest API possible is likely to lead to a lot of programs that are locally simple but globally complex when looked at from more than just the “how much code did I have to write to do this particular thing” standpoint.
Yes, remember we tried that on the old elm-discuss list at least a year ago? I was able to write a web component in Elm, and consume it in Elm too.
It got a bit hairy in places, because I ended up having to duplicate some state on the JS side that was also on the Elm side, but that was largely down to not having some mechanism whereby it could be more neatly integrated into Elm.
Thinking about what is a web component, or the input tag example: you can set such a thing up with properties and attributes (init), it has an internal state (Model), it may have internal events (Msg), some of those events can be tagged with your own message type to capture them using onX functions (onX : someProp → msg → Attribute msg), and of course you have a function to render the thing (view).
I proposed that Elm add a new Program type that can wrap all of this up and just let the consumer see the init, view and onX functions. This is really option 2 from my message above, where bigger programs are structured as smaller programs communicating through messages.
I now think that what works better, is to only use the primitive events that the Elm platform gives us, and use the model instead to communicate between parts of the program. So for a data picker UI component, the model could be like:
type Model =
NoDatePickedYet InternalModel
| PickedADate Date InternalModel
The consumer of this can see enough of the model to know when a date has been picked, and what it is. There is no need for the consumer to model that state itself, which there would be if the output of the date picker were an event in the form of a message. Accidental state can be kept hidden in an opaque internal model.
As a beginner I was excited by the idea of web components. Now I am not interested in them any more and think they are a nasty idea.