Remix.run Logo
wesselbindt 2 days ago

In general, I find that explicit code is more easily read than implicit code. I prefer static over dynamic typing, I actually _like_ the explicitness of async/await or the IO monad. If something allows me to find out information about my current context without having to move up or down the stack and reading the code in other functions, I'm pretty happy about that something, because reading code is slow and tedious. What is it about implicit code that makes you feel it's more readable?

TeMPOraL a day ago | parent [-]

> What is it about implicit code that makes you feel it's more readable?

It's more readable when I don't care about the implicit parts at the moment. It's less readable when I do. The key thing is, whether or not I care changes from task to task, or even within the task, possibly many times per day.

The problem with our current paradigm is that we want to work on a shared plaintext artifact (codebase), and we want it to simultaneously:

1) Express everything there is about the program;

2) Be easy to read and understand and modify by humans;

3) Be the same for everyone at all times - a shared single source of truth.

"Success path" logic, error handling, logging, async/await, authentication, etc. are all cross-cutting concerns. Now, 1) means we're forced to include all of them in code simultaneously, but this goes against 2). Like, when I'm trying to understand the overall logic of some business process, then error handling and async/await are irrelevant. They're pure noise. Yet 1) forces me to look and think about them at all times.

So the issue is, 2) is best achieved when you can "filter out" concerns you don't care about at a given moment, and operate on such simplified view of code. But 1) and 3) requires us to spell the all out, everywhere, at all times. The way this is mitigated today, is through ever more complex syntax and advanced mathematical trickery. Like your async/await keywords, or the IO monad. They're ways of compressing some concerns (or classes of concerns) into terse notation, but that comes at the cost of increased complexity and mental demand, too (I mean, explain to me what a monad is, again? :)). I believe that at this point, all modern languages are hitting the Pareto frontier of readability, so whether you use $whatever-routines instead of async, or result types instead of exceptions, it's all just making some cases more readable, at the expense of other cases.

In the same category of problems is also another "holy war": lots of small functions, vs. fewer big ones. There is no right answer here, because it depends on what your goal as a reader is at the moment - e.g. understanding the idea expressed by some logic may benefit from small functions, but debugging it often benefits from the opposite. This is, again, a faux problem, created by our tooling and insistence on the "shared plaintext single source of truth" paradigm.

Compare with code folding in IDEs. It's a view feature that lets you hide blocks of code - like loop bodies, classes, or function definitions - that you don't care about at the moment. Now imagine a similar feature existed, that would let you "fold away" error handling entirely. Or "fold away" the try/catch blocks, or all the mess of dealing with Result types. Or fold away logging. Or async/await. This is the solution we need - the ability to view the shared code through various lenses. Sacrificing 3) lets us get both 1) and 2) at the same time.

It's way better than what we do now, which is precommitting to relative importance of various cross-cutting concerns, by encoding them in how easy or hard they're to write in a given language.