Hacker News new | ask | show | jobs
by herval11 1086 days ago
Once you become super senior you actually realize what the author said here is not completely correct. This guy has experience, but he hasn't reached nirvana.

There is a singular high level design pattern/abstraction that you can use in actuality to start off your projects.

There is no name for this pattern but it is essentially this:

Segregate io and mutations away from pure functions. Write your code in modular components such that all your logic is in pure functions and all your io and mutations are in other modules.

Why does this style of organization work? Because delineation and organization of every form of application you can think of benefits from breaking out your program organization along this pattern.

Your pure functions will be the most modular, reusable, and testable. You will rarely need to rearchitect logic in pure functions... Instead typically you write new modules and rearrange core functions and recompose them in different ways with newly added pure functions to get from A to B.

The errors and organizational mistakes will happen at the io layer. Those functions likely need to be replaced/overhauled. It's inevitable. Exactly like the author says this section of your program is the most experimental because you are exploring a new technological space.

But the thing is you segregated this away from all your pure logic. So then you're good. You can modify this section of your project and it remains entirely separate from your pure logic.

This pattern has several side effects. One side effect is it automatically makes your code highly unit testable. All pure functions are easily unit tested.

The second side effect is that it maximizes the modularity of your program. This sort of programming nirvana where you search for the right abstraction such that all your code reaches maximum reusability and refractors simply involve moving around and recomposing core logic modules is reached with pure functions as your core abstraction primitive.

You're not going to find this pattern listed in a blog post or anything like that. It's not well known. A software engineer gains this knowledge through experience and luck. You have to stumble on this pattern in order to know it. Senior engineers as a result can spend years following the hack first philosophy in the blog post without ever knowing about a heavy abstraction that can be reused in every single context.

If you don't believe me. Try it. Try some project that segregates logic away from IO. You will indeed find that most of your edits and reorganization of the logic happens with things that touch io. Your pure logic remains untouched and can even be reused in completely different projects as well!

1 comments

IME, separating I/O in the code works because it literally is separate -- it usually happens at the boundaries of the application. It rarely makes any sense to connect two code components (running in the same process) with a disk-backed information path. And where it does make sense -- well, your I/O code moves deeper inside the application.

The "boundary" argument works even better where it's about network I/O or video output, because applications are even less likely to connect code modules using such types of data.

Other than that I don't buy much into that I/O vs "pure" at all, it's pretty much an arbitrary categorization since there really isn't much of a difference between writing to disk or memory. I think the idea that writing to memory is somehow purer comes mostly from Haskell and similar languages that somewhat enforce immutability at the language level. Given that it is still an artificial categorization I don't take this too seriously. The one benefit I see is that it often does improve clarity of architecture to have mostly construct-consume-discard data access patterns without any mutability after construction. Oh, and potentially you can handle errors from disk I/O, while you cannot really handle errors from memory I/O.