Refactor XObj pattern matches
- Dominant language
- Haskell
- Stars
- 6k
- Forks
- 187
- Avg merge
- 8d 22h
- Merged PRs (30d)
- 4
Description
Currently we pattern match against `XObj` nearly everywhere throughout the codebase. This abundance of pattern matching and the ubiquity of `XObj` comes with pros and cons:
*Pros*:
- Pattern matching is (usually) easy to understand and read.
- Pattern matching helps support equational reasoning.
*Cons*:
- Pattern matching breaks encapsulation
There is really only one con, but it's a big one. Here's why, imagine we want to extend the definition of `Obj` with a new form/constructor `New Int`. Since we match on `XObjs,` not `Obj` directly, a level of indirection allows us to flexibly extend `Obj` without updating our pattern-matches. That's great, but *any existing function that pattern matches on `XObj` needs to be updated to handle the new form (if necessary).* Not to mention, since we match on `XObj` the compiler won't force our functions to be exhaustive over `Obj` which has already led to `non-exhuastive patterns in x` errors at runtime.
Not to mention, aside from occasional use of as patterns, we don't benefit much from the pros of pattern matching. The readability offered by matching begins to disappear once we have nested `case` statements. The equational reasoning benefits are not really applicable for us. In most cases we're only interested in one component of an `Obj` in a given function, leading to a ton of extra matches over `_` which is somewhat noisy.
Not to mention, pattern matching everywhere also greatly hampers our ability to modify existing `Obj` constructors, leading to a proliferation of new constructors in some cases. Imagine, for instance, that we discovered some benefit to changing `Mod` so that it carries, in addition to its `Env` the `SymPath` that designates the `Mod`, `Mod Env SymPath`. In order to make such a change, we'd have to update *every single pattern match on `Mod`* throughout the entire codebase.
## Another way
We can recover the benefits of encapsulation by doing two things:
- Making the `Obj` constructors inaccessible outside of `module Obj` (the Type is still exported)
- Providing functions for manipulating objs
If we use this approach, every pattern match would turn into a guarded statement, as an example, here's what `expandList` might look like:
```haskell
-- assume this is still within the context of `expand`
expandList xobj | isList xobj = expandListInternal $ listObjs xobj
| otherwise = error "can't expand nonlist"
expandListInternal xobjs | isExternal hd = return (ctx, Right xobj)
| isInstantiate hd = return (ctx, Right xobj)
| isDeftemplate hd = return (ctx, Right xobj)
...
| isDefn hd = do expand eval ctx $ fnBody xobjs.....
| otherwise = error ...
where hd = head xobjs
```
This not only eliminates tons of required but unnecessary matches (all those `_` in our XObj matches) but it is far more independent to potential changes to `Obj` and its. constructors, and the `otherwise` cases handle new additions flawlessly. It's also easier to read in my opinion.
The downside of course, comes in the form of extra cost of having to know what all the predicative functions and accessor style functions mean `isDeftemplate`, `fnBody`... etc. --but I personally find the regained encapsulation to be worth it. Currently I'm reluctant to make any major or essential changes to the compiler as it's difficult to anticipate all the possible locations one is impacting. I think encapsulating `Obj` from the rest of the codebase will help significantly.
Let me know what you think!
## Another alternative
There's also no requirement that we be absolute about this. We could still expose `Obj` constructors, allowing us to pattern match, while additionally providing predicate and accessor functions so that we may mix and match uses of pattern matching and guarded forms--that way we can pattern match when it's convenient, but use guards where it makes more sense to do so (e.g. places where the match would have tons of `_`).
Contributor guide
Research direction
Start by reviewing the Obj module and the expand/expandList example, then inventory the existing XObj pattern matches across the codebase. The issue does not name files or tests and presents multiple alternatives, so first establish the intended encapsulation approach and scope. Done would require an agreed design and a coordinated migration of affected matches.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- haskell
- Domain
- compilers
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100