Previously on Learning Elm The Wrong Way:
I'm back, everybody, and I've got more sins to atone for. Buckle up.
The choice to use Elm was a decision made with the rationale that I've employed to make most major decisions in my life so far - by the seat of my pants, with hopes for the best. I'd never studied functional programming, and I'd never owned a whole product before end-to-end; but the client wanted what they wanted, and I was their best hope, so I did what I had to do.
We were not happy, then; those were simpler times.1
Anyway. I remember the first time I reached for Elm's regular expressions library. I remember reading Evan's helpful "nag screen" on the elm/regex
package docs:
Generally speaking, it will be easier and nicer to use a parsing library like elm/parser instead of this.
"Hmm. Alright. I like 'nicer' and I like 'easier'." I clicked over to the docs for elm/parser
.
Regular expressions are quite confusing and difficult to use. This library provides a coherent alternative that handles more cases and produces clearer code.
I was sitting there, on my front porch, in the brutal, early-morning Michigan summer sun - all sixty-nine degrees of its feeble fury - and I thought: "Oh boy! If these people think that regex is hard... I'm gonna be so good at this. I'm the God of regex." And it was true - early on in my career, I'd heard that programmers despised and struggled with regex, so I'd made it my business to use Vim only ever and study regex diligently. And, y'know, I was... not half-bad.
Smash cut to:
point : Parser Point
point =
succeed Point
|. symbol "("
|. spaces
|= float
|. spaces
|. symbol ","
|. spaces
|= float
|. spaces
|. symbol ")"
... and, record-scratch.
"Huh. Well, looks like I'll be using regex today, after all."
Some time passed, and I "learned" how to use decoders; except, I didn't really learn how to use decoders, I just fumbled around with them enough to get work done.2 Decode.Pipeline
was particularly nice; I wasn't sure what it was doing with all of those |>
s, but it clicked on a visual level: "this is some sort of a pipeline, and every Thing that Happens in this pipeline goes into my value".
One day, someone in Elm Slack said something along the lines of, "Yeah, Parser
's just like Decode.Pipeline
". I looked at Parser
again. I read the docs again. My brain did that thing that it does when it not only does not Understand, but also does not Understand why it does not Understand - and promptly powered down.
If you've made it this far, here's a tip: Learn to identify the difference between "I don't understand this", and "I don't understand why I don't understand this", and format your questions accordingly. You'll get further, faster.
More time passed. Years, actually. I made a few more attempts at trying to learn how Parser
worked, but every time I made it around to the docs, I was seized by that same feeling of unfamiliarity and confusion. But along the way, I learned something that seemed, at the time, to be unrelated.
I had finally decided to try elm-review
3, and I chose to use Elm Slack regular SiriusStarr's review config as a starting point for my own4. I was intrigued by some of the rules that were enforced; one of them yelled at me for the way that I was using Decode.Pipeline
to decode some JSON into a type alias. It turns out that there's a rule that prohibits you from using a type alias as a constructor!5 This means that given a type alias:
type alias Point =
{ x : Int
, y : Int
}
You're prohibited from ever calling it like so:
origin : Point
origin = Point 0 0
Instead, you must:
origin : Point
origin = { x = 0, y = 0 }
Which meant that a decoder as follows, was invalid:
pointDecoder : Decoder Point
pointDecoder =
Decode.succeed Point
|> Pipeline.required "x" Decode.int
|> Pipeline.required "y" Decode.int
And that instead, you must:
pointDecoder : Decoder Point
pointDecoder =
Decode.succeed (\x y -> { x = x, y = y })
|> Pipeline.required "x" Decode.int
|> Pipeline.required "y" Decode.int
"Well that's pretty verbose, but I get it. Since the type alias is a positional constructor, you could get yourself out of whack if you passed in multiple same-typed arguments out of order, so this forces you to be a little more explicit about what goes where."
"Oh. And I guess that means that I can put any function after the Decode.succeed
in a pipeline, and that the Decoder
will be a Decoder
for whatever type that function outputs; this follows from knowing that a type alias is just a positional constructor for a record."
I didn't realize it, but I was getting closer to unlocking the mysteries of the Parser
.
Earlier this week, I was refactoring some code from the project that I mentioned at the beginning of this post. It's been through a lot over the years, as have I, but the client still loves it - and I've learned a lot from stubbornly fixing things over the years, rather than throwing everything out and starting over. I found a branch of my main update
function where I was taking a string value, using regex to split it based on a delimiter, and doing a bunch of nonsense to verify that the string contained two and only two pieces after splitting; that they were both integers; etc.
"Man. This looks like a great place to stick a Parser
. If only I understood them."
"Maybe this time will be different." You said that the last time. "Yeah, but I'm a little smarter now." We'll see about that, won't we?"
I pulled up the docs. I saw the |.
and the |=
, and I panicked and froze. But this time, I tried something new.
I pulled up Martin Janiczek's Ellie Catalog6, and typed in parser
. Bingo: https://ellie-app.com/f2smkjHfN26a1
As soon as I saw the code in the Ellie, without even running it - I recognized what had kept me from ever understanding before, and then I immediately understood the code.
My prior lack of understanding was due to a mental disconnect between the two true sentences, "record type aliases come with implicit constructors"7 and "all constructors are functions".
You see, most of my application development experience has been in csharp. In csharp, we don't have type aliases or anything like that, but we do have classes. Classes have a type, and types have a type (and the type of a type is a type called Type
, obviously). After marinating for over a decade in a type system where type names are "special" and have to be invoked only in certain special-case contexts (with operators like typeof()
, or as function signatures) - I couldn't see what was literally right in front of my eyes:
Type names, just like everything else in Elm, are Not Special. They're constructors for a value.
And this was obscured for so long, because when I saw
Parser.succeed Point
|= ...
I didn't see "a function (Parser.succeed
) taking another function as an argument (Point
) and then passing it values collected from a parsing operation" - I saw "Parser.succeed for a Special Type Name
".
So from this spiraling diatribe (spiral-tribe?8), three lessons:
Great pains have been taken to ensure that everything with Elm is internally consistent; if you don't understand one Thing, that's based on another Thing - there's a good chance that you don't understand the "another Thing" as well as you think you do.
and
Never be afraid to re-visit ideas that you didn't understand the first time around. You're smarter now.
and
Nothing in this language is special; everything is a function with zero or more arguments. If you don't understand something, try to think about how it could be a function with zero or more arguments, and what that might mean.
Never did hear back about the yacht job9. Oh well. More yachts in the sea, I suppose.
-
To everyone in Elm Slack that's ever explained decoders to me - had to have been upwards of 20 times until y'all wore a groove in my smooth brain to give the information a place to live - thank you so much. ↩
-
Spreading the gospel of how and why
elm-review
is important falls outside of the scope of this post; but for the love of God, if you don't know aboutelm-review
, learn aboutelm-review
. All hail Jeroen. https://github.com/jfmengels/elm-review ↩ -
https://elm.dmy.fr/packages/lue-bird/elm-no-record-type-alias-constructor-function/latest/ ↩
-
After I posted this, lue helpfully pointed out some instances where a type alias is not actually a constructor for a value - you can read more about this here: https://elm.dmy.fr/packages/lue-bird/elm-no-record-type-alias-constructor-function/latest/ ↩
-
https://dev.to/jmpavlick/hungary-for-the-power-a-closer-look-at-hungarian-notation-282d ↩
Top comments (0)