Hacker News new | ask | show | jobs
by TeMPOraL 521 days ago
I believe this problem isn't solvable under our current paradigm of programming, which I call "working directly on plaintext, single-source-of-truth codebase".

Tenant ID, cancellations, loggers, error handling are all examples of cross-cutting concerns. Depending on what any given function does, and what you (the programmer) are interested in at a given moment, any of them could be critical information or pure noise. Ideally, you should not be seeing the things you don't care about, but our current paradigm forces us to spell out all of them, at all times, hurting readability and increasing complexity.

On the readability/"clean code", our most advanced languages are operating on a Pareto frontier. We have whole math fields being employed in service of packaging up common cross-cutting concerns, as to minimize the noise they generate. This is where all the magic monads come from, this is why you have to pay attention to infectious colors of your functions, etc. Different languages make slightly different trade-offs here, to make some concerns more readable, but since it's a Pareto frontier, it always makes some other aspects of code less comprehensible.

In my not so humble opinion, we won't progress beyond this point until we give up on the paradigm itself. We need to accept that, at any given moment, a programmer may need a different perspective on the code, and we need to build tools to allow writing code from those perspectives. What we now call source code should be relegated to the role of intermediary/object code - a single source of truth for the bowels of the compiler, but otherwise something we never touch directly.

Ultimately, the problem of "context" is a problem of perspective, and should be solved by tooling. That is, when reading or modifying code, I should be able to ignore any and all context I don't care about. One moment, I might care about the happy path, so I should be able to view and edit code with all error propagation removed; at another moment, I might care about how all the data travels through the module, in which case I want to see the same code with every single goddamn thing spelled out explicitly, in the fashion GP is arguing to be the default. Etc.

Plaintext is fine. Single source of truth is fine. A single all-encompassing view of everything in a source file is fine. But they're not fine all together, all the time.

1 comments

Monads but more importantly MonadTransformers so you can program in a legible fashion.

However, there's a lot of manual labour to stuff everything into a monad, and then extract it and pattern match when your libraries don't match your choice of control flow monad(s)!

This is where I'd prefer if compilers could come in.

Imagine being in the bowels of a DB lib, and realising that the function you just write might be well positioned to terminate the TCP connection that it's using to talk to the database with. Oh no: now you have to update the signature and every single call-site for its parent, and its parent, and...

Instead, it would be neat if the compiler could treat things you deem cross-cutting as a graph traversal problem instead; call a cancelable method and all callers are automatically cancelable. Decisions about whether to spawn a cancelable subtree, to 'protect' some execution or set a deadline is then written on an opt-in basis per function; all functions compose. The compiler can visualise the tree of cancellation (or hierachical loggers, or OT spans, or actors, or green fibers, or ...) and it can enforce the global invariant that the entry-point captures SIGINT (or sets up logging, or sets up a tracer, or ...).

So imagine the infrastructure of a monad transformer, but available per-function on an opt-in basis. If you write your function to have a cleanup on cancellation, or write logs around any asynchronous barrier, the fiddly details of stuffing the monad is done by the compiler and optionally visualised and explained in the IDE. Your code doesn't have to opt-in, so you can make each function very clean.

Yes, there's plenty of space for automation and advanced support from tooling. Hell, not every perspective is best viewed as plaintext; in particular, anything that looks like a directed graph fundamentally cannot be well-represented in plaintext at all without repeating nodes, breaking the 1:1 correspondence between a token and a thing represented by that token.

Still, I believe the core insight here is that we need different perspectives at different times. Using your example, most of the time I probably don't care whether the code is cancellable or not. Any mention of it is distracting noise to me. But other times - perhaps next day, or perhaps just five minutes later, I suddenly need to know whether the code is cancellable, and perhaps I need to explicitly opt out of it somewhere. It's highly likely that in those cases, I may not care about things like error handling logic and passing around session identifiers, and I would like that to disappear in those moments, etc.

And hell, I might need an overview of the which code is or isn't protected, and that would be best served by showing me an interactive DAG of functions that I can zoom around and expand/collapse, so that's another kind of perspective. Etc.

EDIT:

And then there's my favorite example: the unending holy war of "few fat functions" vs. "lots of tiny functions". Despite the endless streams of Tweets and articles arguing for either, there is no right choice here - there's no right trade-off you can make here up front, and can never be, because which one is more readable depends strictly on why you're reading it. E.g. lots of tiny functions reduce duplication and can introduce a language you can use to effectively think about some code at a higher level - but if there's a thorny bug in there I'm trying to fix, I want all of that shit inlined into one, big function, that I can step through sequentially, following the actual execution order.

It is my firm belief that the ability to inline and uninline code on the fly, for yourself, personally, without affecting the actual execution or the work of other developers, is one of the most important missing piece in our current tooling, and making it happen is a good first step towards abandoning The Current Paradigm that is now suffocating us all.

Second one would be, along with inlining, the ability to just give variables and parameters fixed values when reading, and have those values be displayed and propagated through the code - effectively doing a partial simulation of code execution. Being able to do it ad hoc, temporarily, would be a huge aid in quickly understanding what some code does.

Promises are so incredibly close to being a representation of work.

The OS has such sophisticated tools for process management, but inside a process there are so many subprocesses going on, & it feels like we are flailing about with poorly managed process like things. (Everyone except Erlang.)

I love how close zx comes to touching the sky here. It's a typescript library for running processes, as a tagged template function returning a promise. *const hello = $`sleep 3; echo hello world`. But the promise isnt just a "a future value", it is A ProcessPromise for interacting with the promise.

I so wish promises were just a little better. It feels like such a bizarre tragedy to me the "a promise is a future value" not a thing unto itself won the day in es6 / es2015, destroyed the possibility of a promise being more; zx has run into a significant number of ergonomic annoyances because this small world dogma.

How cool it would be to see this go further. I'd love for the language to show what promises if any this promise is awaiting! I long for that dependency graph of subprocesses to start to show itself, not just at compile time but for the runtime to be able to actively observe and manage the subprocesses within it at runtime. We keep building workflow engines, build robust userland that manage their own subprocesses, user user lands, but the language itself seems so close & yet so far from letting the simple promise become more a process, and that seems like a sad shame.