How Elm Code Tends Towards Simplicity
A common trend I’ve observed in Elm projects is that Elm guides you towards simple solutions. I’ve found this to be true even for people who are new to Elm. I was coaching a team in an enterprise environment through adopting Elm for a new project. They had built a previous project with AngularJS and had really struggled with it. Or rather, they hadn’t struggled with it. The framework would accept whatever solution they came up with. When they started working with Elm, it might have taken them a little longer to figure out “how do I store the input from an input field in my Model.” But because Elm didn’t allow them to use temporary hacks, their approach was totally different. Instead of copy-pasting code from StackOverflow that uses a global variable, or tweaks the DOM, “just to get it working for now”, Elm forced them to figure out a solution without relying on any hacks. The result was that months later, instead of fighting against an unstable codebase that is scary to change, they were working with remarkably reliable code. They could make changes easily and without fear. The difference was night and day.
I recently had a similar experience that illustrates how Elm guides you towards simplicity. I’m the author of an Elm package for type-safe GraphQL queries,
dillonkearns/elm-graphql (see my recent Elm Conf talk to learn more). I was starting to get the same feature request from several different users. It was a reasonable request, but when it came up I would explain the technical challenges and how significant the redesign to support it would be.
The Challenge: Unique Keys
GraphQL queries get JSON responses, with the responses having the same “shape” as the query. So this query:
would return JSON like this:
The only caveat is that if you request multiple fields with the same name with different arguments, you need to give field aliases to explicitly tell GraphQL what key to pass that data back under.
# ERROR - Field 'avatar' has an argument conflict: is size 12 or 48?
The above query results in an error because it is ambiguous.
dillonkearns/elm-graphql handles the low-level details of building up and deserializing GraphQL queries so that 1) you can use high-level Elm language features instead of a complicated library, and 2) it can guarantee that any query you create is valid. So the library needs a way to avoid creating ambiguous and invalid queries like this.
The Original Solution
My previous algorithm for ensuring that fields were aliased when necessary was to increment a counter and use that number for each field alias.
Using this approach, my library would generate a query like this:
avatar2: avatar(size: 48) # this alias tells GraphQL to return this data under the key "avatar2"
This worked quite well. But when you created a Selection Set like above grabbing the small and medium avatars, it needed to build up the corresponding Json Decoder based on what it knew at that point in time (Json Decoders are how Elm deserializes JSON into typed Elm data). So I couldn’t expose an API to combine two Selection Sets like this together because it would have built the decoder to extract the data from its alias
avatar2. But if we merged in another set of fields that included a medium and full-size avatar, their Json Decoders would try to get that data from a field called
avatar2 when it should instead be looking for them under
avatar4. I could fix this problem by redesigning the code to generate the deserializers (Json Decoders) at the last minute, but this would mean creating a custom data structure so I could delay this. It would add a lot more complexity and points of failure where I could introduce a bug.
Looking for the Simple Approach
I really dreaded the idea of doing this redesign. Sure, it was doable, but it didn’t feel right to introduce so much complexity to solve such a simple problem. I wanted to think through the design of the library, both in terms of its public-facing API, and its low-level implementation. This is something I find myself doing often in Elm. If I were solving this problem in another language like TypeScript, it would be easy to add some global state to keep count of how many times fields were created with a certain name. From past experience with this approach, this would lead to bug reports, tests with false positives and negatives, and other people touching the code and not understanding where this strange behavior was coming from or what they needed to do to preserve it properly. That’s the problem with implicit state. It’s really handy to solve problems quickly. But in the long run, you start to feel the real cost. State is really the thing that makes code difficult to reason about and change, and reading/changing code is what we spend most of our coding hours doing. So if we can minimize state, it’s a huge win!
I could have created some state in my Elm application to solve this problem, but it would need to be explicit and it is therefore more tedious than in other languages. There are of course plenty of places where you do need state in an Elm application, but you are more attuned to how much state you have and how many places depend on that state because of its explicitness. Ultimately I’ve found this to be an excellent tradeoff, as frustrating as it can be when you want to “just get something working quickly.” When it comes to maintainability, this explicit approach pays even in the short-term.
The Simpler Solution
I eventually came up with the idea of using hash-based field aliases:
avatarABC: avatar(size: 12)
avatarXYZ: avatar(size: 48)
By generating a unique hash based on the field’s arguments (imagine that
ABC is a hash representation of
(size: 12)), you are guaranteed to avoid ambiguous queries. And you don’t need to know which fields are used in the surrounding query! This solution is much easier and less error-prone because all you need is a given field to determine its alias. No need to peek at its siblings or worry whether it will get more siblings later. You can read more about the details of the final implementation on this Github issue.
The Bottom Line
So why is this so important? One of the key qualities I look for in a language, a framework, a coding technique, etc., is whether it trends towards simplicity. For example, if refactoring with a language or framework is tedious or error-prone, you will do it less. If it feels bulletproof and easy like in Elm, then the code will trend toward being more maintainable. Over time, Elm codebases tend strongly toward having minimal state. And less state means fewer moving parts, less expensive changes, and fewer bugs.
I’m offering free intro Elm talks. Drop me a line if your team is curious to learn more about Elm! Or share a comment if you’ve had a similar experience with a language or framework guiding you towards a simpler solution.