-
Notifications
You must be signed in to change notification settings - Fork 90
Description
I know I'm posting this issue on a repo full of people who would love to see this proposal go in, so I'm sure I'll get a ton of dislikes for bringing this issue up :). But I think it's an important conversation to have.
Does pattern matching pay for itself?
I'm generally fond of pattern-matching syntax, especially in functional languages. There's a certain cleanliness to it. It's nice to see many languages starting to adapt various functional features such as this.
That being said, I've recently been pondering the concerns many people have about JavaScript's syntax getting too large, and I can't help but think about pattern matching and the problems its trying to solve vs the amount of syntax it's using to solve those problems. I'm becoming increasingly convinced that pattern matching may not be a good fit for JavaScript - it works well in languages that were built from the ground-up with pattern-matching in mind, but when it's tacked on after-the-fact, it just doesn't pay for itself. To expound on that last point a bit - most functional languages don't have a separate syntax for destructuring and pattern matching, you just destructure via pattern matching. The fact that JavaScript will have to have both means it needs to bring in much more syntax, semantics, and most importantly, conceptual overhead to pay for this feature. (and yes, I know pattern-matching tries to be a superset of destructuring, which helps, but it still deviates in commonly used scenarios, e.g. pattern matching with array syntax does a length check, but you can destructure an array of any length no problem)
How much does pattern matching really improve your code?
I'm going to do a side-by-side comparison of some of the motivating examples this proposal provides, along with how those same examples could be written without pattern matching. While I don't feel it's strictly necessary, I will make use of a couple of other smaller proposals as I make this comparison:
- Do expressions
- a
noelse"proposal" I'm making up right now. If you put "noelse" at the end if an if-chain, then an error will be thrown if none of the if/else-if blocks were entered.
// with pattern matching
const res = await fetch(jsonService)
match (res) {
when ({ status: 200, headers: { 'Content-Length': s } }):
console.log(`size is ${s}`);
when ({ status: 404 }):
console.log('JSON not found');
when ({ status }) if (status >= 400): do {
throw new RequestError(res);
}
};
// without pattern matching
const res = await fetch(jsonService)
if (res.status === 200) {
const s = res.headers['Content-Length'];
console.log(`size is ${s}`);
} else if (res.status === 404) {
console.log('JSON not found');
} else if (res.status >= 400) {
throw new RequestError(res);
} noelse;
// -------------------- //
// with pattern matching
function todosReducer(state = initialState, action) {
return match (action) {
when ({ type: 'set-visibility-filter', payload: visFilter }):
{ ...state, visFilter }
when ({ type: 'add-todo', payload: text }):
{ ...state, todos: [...state.todos, { text, completed: false }] }
when ({ type: 'toggle-todo', payload: index }): do {
const newTodos = state.todos.map((todo, i) => {
return i !== index ? todo : {
...todo,
completed: !todo.completed
};
});
({
...state,
todos: newTodos,
});
}
default: state // ignore unknown actions
}
}
// without pattern matching
function todosReducer(state = initialState, action) {
return do {
if (action.type === 'set-visibility-filter') {
const visFilter = action.payload;
({ ...state, visFilter });
} else if (action.type === 'add-todo') {
const text = action.payload;
({ ...state, todos: [...state.todos, { text, completed: false }] });
} else if (action.type === 'toggle-todo') {
const index = action.payload;
const newTodos = state.todos.map((todo, i) => {
return i !== index ? todo : {
...todo,
completed: !todo.completed
};
});
({
...state,
todos: newTodos,
});
} else {
state; // ignore unknown actions
}
}
}
// -------------------- //
// with pattern matching
<Fetch url={API_URL}>
{props => match (props) {
when ({ loading }): <Loading />
when ({ error }): do {
console.err("something bad happened");
<Error error={error} />
}
when ({ data }): <Page data={data} />
}}
</Fetch>
// without pattern matching
<Fetch url={API_URL}>
{props => do {
if ('loading' in props) {
<Loading />
} else if ('error' in props) {
console.err("something bad happened");
<Error error={error} />
} else if ('data' in props) {
<Page data={data} />
} noelse
}}
</Fetch>Did pattern matching improve the quality of these examples? Absolutely.
Did it improve them by a ton? Not really. The code is still fairly easy to read in either version.
Now considering the size of the pattern-matching proposal and the huge quantity of syntax it's adding, you can (hopefully) see why I'm starting to get skeptical about it.
But what about...?
There are other use-cases that pattern-matching brings to the table, that I'd like to briefly discuss.
Pattern matching is supposed to be a better replacement for "switch", and while it would be nice to have a good "switch" replacement, honestly, "switch" probably didn't need to exist in the first place either. Using if/else works good enough. The syntax cost for "switch" probably didn't pay for itself either.
Pattern-matching also shines with certain types of patterns. Take, for example, a pattern like this:
match (value) {
when ({ aReallyLongPropertyName: { x: 0, y: 0, z: 0 }}): do {
...
}
}A brute-force conversion of this code sample would be fairly ugly.
if (
'aReallyLongPropertyName' in value &&
value.aReallyLongPropertyName.x === 0 &&
value.aReallyLongPropertyName.y === 0 &&
value.aReallyLongPropertyName.z === 0
) {
...
} else if ...But, with a little bit of thought, it's not that hard to clean up this sort of thing.
const maybeCoord = value.aReallyLongPropertyName;
if (maybeCoord?.x === 0 && maybeCoord?.y === 0 && maybeCoord?.z === 0) {
...
} else if ...The pattern-matching solution is nicer, but the alternative isn't bad or unreadable either.
There may be other pathelogical patterns you can think of where you'll find that pattern-matching can really shine against any other alternatives. But, if we focus on the common day-to-day use of pattern-matching, I'm sure we'll find that most uses can be fairly easily replaced with "if/else" and maybe "do expressions", without much of a readability cost.
Conclusion
I think my opinions are summed up pretty well in the above blurb.
I'm wondering if others have reasons they love pattern-matching that I didn't touch on here. For some of these "loved features", I wonder if there's other smaller proposals that could be introduced that would accomplish the same objective in a simpler way (similar to the noelse idea I tossed around earlier). I do feel like, with this proposal, we started with the solution "let's add pattern-matching syntax" as opposed to a problem statement like "lets fix switch's pitfalls" or "checking many fields of an object/array can be verbose, can we find a more concise way to handle it?"