I feel like you can apply the same sentiment to many of the "big scary things" in programming.
Things that you don't want to build (as far as "common engineering wisdom" is to be believed):
- a compiler
- a programming language (not sure that there is a difference to compiler as stated in the article)
- a database (query engine)
- a CMS
- a ERP
But sometimes you actually _do_ want to build that (even if every alarm bell in your engineering lizard brain goes off), and then it's probably better to commit and learn the ins and outs of that specific problem domain.
I think especially the recommendation to commit to it is something that's missing from the article. It's "easy" to point a people and saying "look, you did the thing you said you wouldn't do", but far harder to suggest a course of action.
I personally hit the barrier of "things you shouldn't build" in the past, and I've always been happiest (and professional outcomes have been the best) when I decided to break through the barrier and prepare for what lies on the other side, instead of trying to dance around it for ages.
> and then it's probably better to commit and learn the ins and outs of that specific problem domain.
Even more, it's probably better to start off with a full understanding of these systems, so you can recognize when you're Greenspunning one of these abstractions, and just switch to using a robust off-the-shelf implementation of the same abstraction. (E.g. switch from writing a hand-rolled parser, to using a real parser-combinator library, or writing a formal grammar for a PEG compiler.)
Emphasis on "off-the-shelf": the most important thing to get from experience with these abstractions, is the understanding that you it will be less work to use the abstraction that has more power; and, in fact, that much of the tooling around these abstractions is built to assume that you want the full power of the tool by default, such that you have to add configuration to get less. So the workflow is actually "start off ordering 'one with everything', and then streamline your system over time, removing bits and pieces, as you notice properties of your particular situation that mean that you can do with something less powerful at particular stages."
eg: log4j -- so much power you can enable random bitcoin miners with your infrastructure (tongue in cheek)
ie: Sometimes it's not about more power but about the smallest/least powerful implementation that accomplishes the job.
I agree that using off-the-shelf is often important but it also comes with the need to vet the thing in the first place. That's not always trivial if you care about packaged licenses from dependencies or a lack of hidden features or actively developed or a plethora of other dimensions to what the "shelf" provides.
I think "tools that build tools" is a very different ecosystem from most others. When you pull in something like e.g. LLVM, you're not really having to deal with any of that. It's a clean bolt-in API, freely-licensed, no weird integration points, with many active users who've sanded off any weird expectations.
I think the difference is that something like log4j has no pressure to be "the least power it can while doing the biggest job it can" — it has mostly enterprise customers who want the power, but don't care about the scope creep. Something like LLVM, on the other hand, has both big enterprise use-cases, and tiny embedded use-cases, with big players invested in both. So its size and scope have been much more well-considered, with any components that do something other than "compile these data structures representing inputs, into these other data structures representing outputs" having been long factored out into additional "plugin" sort of downstream-dependency libs.
In the specific case of LLVM, I mostly agree but for different reasons. However, Node would be a good counterpoint to “tools that build tools”. There are plenty of large interests there, though maybe not so much emphasis on the embedded space. More importantly, a bug or vulnerability in Node is generally far less impactful than one in LLVM. Owning a compiler that compiles everything in an OS >> owning Node.
The problem, of course, is that it's impossible to know everything :)
IME the best compromise I've found is to learn enough to recognize the problems, so that when I implement a bastardized compiler, I'm aware that that's what I'm doing and that at some point in the future I'll need to decide how deep the implementation should go or switch to actually using some library.
I somewhat disagree — there really is a finite amount of Computer Science to know. The more you dive deep into different domains, the more you find yourself ending up in the same places, learning the same things with different names. What is a SQL query planner but a compiler? Is an OS scheduler really that different from an HPC workload manager, or your GPU's computer-shader arbitrator? Is RDMA against an NVMe storage pool fundamentally new to you if you've already written hierarchical chunk persistence+caching for a distributed key-value store? Etc.
At some point, it's all just Computer Science. And not too much of it, either! An amount that could easily be learned in a (good) 4-year undergraduate program.
Yes, but also no- I see what you're saying from a high level, but my point is that the devil's in the details and that you can't know all the details of each domain, the best you can do is be aware that those details are there. Just being exposed to even a subset of the high-level is pushing the limits of a 4-year undergrad curriculum.
> What is a SQL query planner but a compiler?
Kind of? Lexing, sure, and to some extent IR, but an execution plan is not built from an instruction set, especially when you start talking distributed. Even if you ignore the distributed scenario, there's still a huge difference between understanding indices/columnar-storage/row-storage and implementing PGO or LTO.
Plus, there's also a lot of domain complexity here- are you aiming for just ANSI compat, or are you going deeper and trying to be compatible with specific SQL Server / Oracle / Postgres features?
> Is an OS scheduler really that different from an HPC workload manager, or your GPU's computer-shader arbitrator?
Knowing little about HPC or GPUs, all I can say is that yes, there's absolutely a difference between the kinds of issues a kernel, hypervisor, and distributed scheduler have to deal with. Hypervisors need to integrate with higher-level RBACs, for one; distributed schedulers also need to be able to evict bad machines from the same system.
Telemetry also changes massively: if you're dealing with a kernel/hypervisor, stats from things like strace etc are useful. If you're distributed? You now have multiple cardinality problems (hostname, network devices, microservice release ID).
> Is RDMA against an NVMe storage pool fundamentally new to you if you've already written hierarchical chunk persistence+caching for a distributed key-value store?
Again, yes (at least I assume so, I'm not super sure exactly what "NVMe storage pool" is, I assume you're talking about the protocol that SSDs speak) - e.g. what's your resilience story against a network failure (someone cutting a fiber line, or losing a subgraph of a clos fabric)?
> but my point is that the devil's in the details and that you can't know all the details of each domain, the best you can do is be aware that those details are there.
IMHO you can do slightly better — you can know enough about isomorphic decisions you've made in other domains to drive design in a new domain, without necessarily understanding implementation.
(Especially "design" in the sense of "where does the shear-layer exist where I would expect to find libraries implementing the logic below that layer" — allowing you to understand that there should exist a library to solve constrained problem X, and so you should go looking for such a library as a starting point to solving the problem; rather than solving half of problem X, and then looking for a library to solve overly-constrained sub-problem Y.)
Or, to put it another way: knowledge of strategy is portable, even if knowledge of tactics is not. An Army General would — in terms of pure skill — pick up the job of being a Naval Admiral pretty quickly, because there's a lot about high-level strategy that isn't domain specific. And that experience — combined with hard-won domain experience in other domains — could guide them on absorbing the new domain's lower-level tactics very quickly.
Or, another analogy: linguists learn languages more easily than non-linguists, because 1. it turns out that, past a certain point, languages are more similar than they are different; but more importantly, 2. there are portable high-level abstractions that you begin to pick up from experience that can accelerate your understanding of a new language, but which you can also be taught academically, skipping the need to derive those concepts yourself. (You still need practical domain knowledge in at least some domains to root those concepts to, but you can do that in your own head when studying the concepts in a book, rather than "in anger" in the field.)
This belief — that there exists portable high-level strategic knowledge to software engineering — is exactly why some companies promote programmers into product managers. A product manager will be tasked to drive the strategy for writing a new piece of software, potentially in domains they've never worked in. They won't be implementing this software themselves — so they don't actually need to understand the domain with high-enough precision to implement it. But they'll nevertheless be relied upon to make architectural decisions, buy-vs-build decisions, etc. — precisely because these decisions can be made using the portable high-level strategic knowledge of software engineering.
(Though, once again, this knowledge must be rooted in something; which is why I don't really believe in CS / SWEng as programs people should be taking fresh out of university. The best time to learn them is when you already have 10 years' programming experience under your belt. They should be treated as "officer's finishing school", to turn an experienced apprentice 'programmer' into a professional Software Engineer.)
On to specific hand-wavy rebuttals, since my original rhetorical questioning didn't do the job of being particularly clear what I meant:
> but an execution plan is not built from an instruction set
What is a logical-level write-ahead log, but a long bytecode program that replicas execute? What does executing SQL do, but to compile your intent into a sequence of atomic+linearized commands — an instruction stream — in said write-ahead log?
(Alright, in a multi-master scenario this isn't as true. But you can get back to this property if your DBMS is using a CRDT event-streaming model; and AFAIK all the scalable multi-master DBMSes do something approximating that, or a limited subset of that.)
> Hypervisors need to integrate with higher-level RBACs, for one; distributed schedulers also need to be able to evict bad machines from the same system.
Perhaps you haven't dealt with machines of sufficient scale—the abstractions really do come back together! The IBM z/OS kernel knows about draining+evicting hotplug NUMA nodes as schedulable resources (and doing health-checks to automatically enable that), in exactly the same way that Mosix or TORQUE or kube-scheduler knows about draining+evicting machines as schedulable resources (and doing health-checks to automatically enable that.)
Even regular microcomputer OS kernels have some level of support for this, too — though you might be surprised why it's there. It doesn't exist for the sake of CPU hotplug+healthcheck; but rather for the sake of power efficiency. An overheating CPU core is, at least temporarily, a "bad" CPU core, and on CPU paradigms like big.LITTLE, the entire "big" core-complex might be drained+evicted as a response! That code, although different in purpose, shares a lot algorithmically with what the distributed schedulers are doing — to the point that one could easily be repurposed to drive the other. (IIRC, the Xeon Phi did exactly this.)
> e.g. what's your resilience story against a network failure (someone cutting a fiber line, or losing a subgraph of a clos fabric)?
My point with mentioning hierarchical persistence+caching for a distributed key-value store, is that these same problems also occur there. From the perspective of a client downstream of these systems, a network partition in tiered storage, is a network partition in tiered storage, and you have to make the same decisions — often involving (differently-tuned implementations of) the same algorithms! — in the solution domain for both; and, perhaps more importantly, expose the same information to the client in the solution domain for both, to allow the client to make decisions (where needing to capture and make this information available will heavily constrain your design in both cases, forcing some of the similarity.)
> you can know enough about isomorphic decisions you've made in other domains to drive design in a new domain, without necessarily understanding implementation.
That's certainly true!
I'm not at 5 years yet in the industry myself, but I've definitely started to notice these patterns - the strategy vs tactics is a nice way to describe it :)
> Perhaps you haven't dealt with machines of sufficient scale—the abstractions really do come back together! The IBM z/OS kernel knows about draining+evicting hotplug NUMA nodes as schedulable resources (and doing health-checks to automatically enable that), in exactly the same way that Mosix or TORQUE or kube-scheduler knows about draining+evicting machines as schedulable resources (and doing health-checks to automatically enable that.)
Oh, this is interesting. My experience has been as a Google-internal user of Borg, where although the fleet is large, most of the machines are individually small enough that it's easier for us to simply evict a machine from the fleet rather than block a core. (The whole "commodity" server strategy, whereas z/OS follows a very different architecture, but I see exactly what you mean there.)
> An amount that could easily be learned in a (good) 4-year undergraduate program.
i think any kind of undergrad program can only ever scratch the surface of some of these topics. people spend decades becoming proficient in just a single vertical (microchip architecture, distributed systems, asics/fpgas, signal processing, gpu shaders, network engineering, jit compilers, language design, color spaces, database engines, etc).
IMHO "programming language" would refer specifically to the grammar, type system, semantics, keywords, etc. and "compiler" would refer to the implementation that takes something written in that representation and outputs some type of executable. A pedantic distinction but probably a useful one, especially given how some languages have multiple implementations of their standard.
The Programming Languages course I took in college had us implement a toy subset of Ruby targeting the Lua VM. Easily the most interesting and useful class that I had the pleasure of experiencing; it completely changed how I think about languages. I think everybody should have exposure to the basic principles of compilers like syntax trees, flow analysis, calling conventions, and so forth (if not necessarily the more complicated stuff like operational semantics).
There are two neat things about compilers and interpreters:
1. They are one of the most researched areas of CS with a large amount of educational materials, tools, and community knowledge to help.
2. They map to any problem involving transforming an input into an output (hell, any problem transforming an input to an output is a compiler or interpreter).
I'd argue the idea of an "interpreter" is one of the most foundational concepts of CS.
It sounds really basic, but the idea of being handed a description, and doing/creating something based on that is everywhere. It is really quite beautiful.
It comes up often too. A few month ago I wanted to make a part of the code I was working on more declarative, so I had it take a javascript object that described a layout, and make actions depending on that object. That's a form of interpreter in a way.
To me it was the recursive mapping over a closed set of types, whatever you do you should end up with an element of your type/grammar. That and the graph walk space of the AST.
The closed set of types describes how each pass in a compiler pipeline tends to build an intermediate representation of some sort, analyze/modify it, and then output it to the next stage. The IR is closed in that it is not open for everyone to extend willy-nilly. It is this property that makes it even possible to analyze and manipulate in a sound manner.
In a sense, compilers literally build a bunch of linguistic 'worlds' within their pipeline, each one emphasizing something different.
I would also add a build system to that list, a tarpit many engineers have fallen into.
> not sure that there is a difference to compiler as stated in the article
It's hugely different.
If you're building a compiler for an existing language, you inadvertently signed yourself up for implementing and supporting the entire language, which is a pretty large engineering task.
But if you design a new language, you signed up for not just implementing the language, but also:
- Actually designing it, which is a very difficult task with hard trade-offs all over the place.
- Documenting it so that others can actually learn it. Technical writing is another hard task that takes years to master.
- Syntax highlighting and editor support so that it's as easy to work with as other languages. You will discover that users in the wild apparently use thousands of different editors, all of which you are expected to support.
- Static analysis tooling so that code navigation, automated refactoring, linting, etc. all work. Users today expect a rich IDE experience for every language they use. Also, you'll discover the hard way that the architecture of an interactive static analyzer is very different from your batch mode compiler, so now you're likely writing two front ends.
- Thousands of answers on StackOverflow, blog posts, introductory videos, etc. Users learn in many different ways and they expect to find education material in the form they prefer.
Note that this also happens pretty much whenever anybody has any sort of template system that they want to support substitution into. You decide that you want to support substitution and suddenly you are building static analysis and documenting and writing syntax highlighting and all this mess.
So for example when you want to create a validation system for something, and then you realize that you want to have generics/type-variables/polymorphism, you want validation containers. Bam, the complexity suddenly shoots up! Or maybe you just want to let someone configure with yaml, and then you find yourself with substitutions and substitutions in substitutions and all that mess. Why?
The abstract reason is that the underlying mathematics of template substitution is lambda calculus, which is Turing complete. It's hard to learn lambda calculus because it's very abstract, it takes place in a mathematical ideal realm where nothing happens, but this also means that the same pattern reappears in a whole bunch of different contexts, and it is best if we just regard it as a design pattern and conscientiously adopt it. “Oh, this is a Lisp, I have seen the 20 line lisp-in-lisp evaluator, let's just code that in whatever host language and someone else will have thought of the details.”
> Some of us might argue that you have to do that anyway, for any software you write.
Yes, but if your software is another implementation of an existing language, the burden is an order of magnitude smaller because almost all of the existing docs for that language apply to your implementation too.
> On the other hand, the last three points are really not requirements unless you are creating a general purpose language and have a large audience.
Even if you have a small audience, if you want them to be productive, they need good tools and resources. Part of the reason why it's hard for DSLs to be effective is that most of the resources get amortized across all users. When you have a small audience, you can't justify building all these tools and docs, but the users still need them and their productivity suffers in the absence.
If your audience is too small, I think it often just doesn't make sense in terms of cost to make a language for them. You're usually better off using an existing language that already has those resources you can leverage.
My first job out of university, I ended up doing one of those by mistake. My second job after university, I ended up doing another three of those, but at least we confronted the thing head on and deliberately, knowing that we were aiming to create them. Trust me, it's better to do it deliberately than by mistake.
Yes. Another way to put it is: engineering should be focused on what's needed more than what the Right People On The Internet Say.
Additionally, the dirty secret of all those things is they make orders of magnitude more sense to dig into and learn vs the innumerable arcane errors require Googling to fix (obscure Webpack errors, CSS hackery, etc). Your learning accumulates over time.
In situations of mutual dependence between software components in a big project, implementing a quick "scaffolding" solution for one of them can get development moving on all components independently. A long time ago, I was working on a new incompatible instruction set architecture at a specialized computer systems vendor, and had written a simulator and assembler for it. The O/S group was ready to start porting our UNIX variant, but there was no C compiler. So I wrote one; ANS C89 is a pretty small language. That got everybody unstuck, and the O/S guys could do their port while the compiler group could move at their own pace (and had a partial O/S on which to run compiler test suites). I guess my point is that there can be times when quick "scaffolding" components are reasonable.
Don't forget the paramount of the Mountain of Shit:
- a billing system
There's a reason why there are b2b companies that specialize in billing. When people branch out it's often billing and X, not X and billing. And even then sometimes they pull back. ADP bought a bunch of companies outside their wheelhouse and then spun them back out.
I mean I actually really want to build many of those things (the first 3 anyways), and I have. I just don't generally show the results to people or make them use them.
I can't vouch for the quality of what I've built, but taking time to attempt to build those kinds of things has always paid off in terms of overall betterment as a programmer.
Then I can go back to a day job of mundanely moving protobufs from one place to another.
I think the main thing is having respect for the mountain of engineering that went into existing solutions and not rolling your own unless you really really have to.
I think an Embedded DSL with post compilation constraint solver based analyzers could take the place of restricted mini-languages while taking advantage of the existing engineering and tooling available for modern languages.
So like a restricted subset? That's a really neat idea; you should develop it into a paper or something.
This restricted subset concept has the huge advantage that you can slip out of the harness in a pinch. So you can reuse a relatively developed Float-less Python program in Full Python and later walk back removing floats.
That's really clever. It essentially obviates the whole configuration DSL tango; it also gets people started where the rubber hits the road -- not with syntax and parsing, but with why the DSL exists and what it's supposed to do and not do.
You can restrict all sort of things, including of course a restricted subset of the language. Such solutions already have headcount at MS which is about as good as it gets for bringing a new thing like this to masses.
When it is released and as it matures we can extend it to do a bunch of other cool things like configuration DSL which would really help clean up Linux. Maybe even build scripts.
You could do stuff like this before with LISP, or Scala compiler plugins. But the tooling and accessibility will make a big difference.
For C# there is Roslyn Analyzers which was a foot in the door for a lot of things. F# has LiveCheck which AFAIK is a lot more powerful. I don't think there is much public info out there; Don Syme demos the use of it to add shape checking for Deep Learning models with full IDE interaction, but any arbitrary constraints can be added and constraint solvers are probably the easiest way to manage and compose those constraints. https://youtu.be/0DNWLorVIh4?t=3410
I had a very different reading of the article, colored by my own bad experience.
Sometimes you start off with this cool idea/hack to solve a problem that could have been solved some other way. But your cool hack solves the problem and more! Then your fellow team members all want it to be even more powerful, and your cool hack slowly grows into a monstrosity.
In these situations, often your manager did not particularly want you to do this, but he let you do what you want. He does want you to be productive, and this doesn't count for that much. But since the team is now dependent on you, he does expect you to maintain it. You now end up with two jobs: The one your manager hired you for, and maintaining this thing.
I would love to write my own DSL to solve a problem at work, and this post read to me as a cautionary tale on pursuing that goal - unless management is aligned with your vision. If they're not, solve it the boring (and perhaps nonideal) way.
I'm hit by this at the moment. I need to build something so that users can revert to a previous version of a record, but it's constrained to sqlite. I'm (re) building versioned document stores or temporal tables in various POCs now and I'm scared.
I work on a commercial ERP and this is 100% accurate. It features:
* A proprietary language
* A compiler for said proprietary language
* A way to define tables and views and turn these definitions into real tables and views that live in an SQL database. There is also a custom SQL dialect that gets translated to real SQL on the fly. I'm counting this as having its own database even though it offloads the actual database work to a real database.
* One of the products in this ERP suite includes a CMS built on all of the above.
Because if it succedds, it needs to be supported long term. If you're doing that inhouse, chances are slim of original designers staying 10-20 years at the same company.
Then what you have in your hands is a legacy monolith neglected over its life which might double as core of your stack and it's awful to move away from down the line.
Indeed, but maintaining software that is a core part of your business is a very different game than supporting (complex) internal tooling. They don't bring revenue, are a time/money sink and not many companies can spare the resources to do it properly.
Anedoctal experience: a company I used to work for had all the core stack developed inhouse: programming language (yeah), compiler, database, etc. No one worked on it apart from the founder and a couple of other engineer that were no longer there.
A few years down the line a big client asked if we were able to integrate our software with their PLC and this had to be developed from scratch, as it wasn't in the scope of the programming language. The quote sent to them was in the six figures and up to 2 years for it to be ready. Every major programming language has a library that does this. Eventually the company lost the client.
Writing your own scripting language is really not something game developers should be doing for the past decade or so. There are so many good off-the-shelf options ranging from Lua to C#, plus a litany of obscure (but good!) ones.
For performance-critical code, even a tiny DSL is not a solution I like unless you're transpiling to C++ or something.
Hooking LUA or similar can introduce a lot of unwanted complexity, be harder for an untrained implementation team member to understand, and be less of an exact fit to the purpose. There are other cases where it makes sense though.
Because the complexity of the problem tends to scale to your willingness to address said complexity. In other words, it likely won't stay small for long if it gets any users, and now you're the maintainer for a tool used by others.
Heavily depends on what specific incarnation of SAP you mean? If you want to have a SAP replacement for your specific use case, Rails CRUD is the only technological component you need. The much more important part to the recipe is knowledge of the domain model.
That's also where SAP's moat lies. Not with it's technological underpinning, but with the all the different industries and their processes which they've transferred into lines of code (and the ability to extend them with further LOC with the help of a "SAP consultant").
> …But sometimes you actually _do_ want to build that…
That’s not true for everyone. I’ve been a dev for 20 years and have never done any of those, and I have no desire to at all. That is ok! Programming isn’t some kind of progression to run from “n00b hello world” to “1337 compiler hax0r”, it’s a tool to solve problems with. Many of us will never have to solve these problems or have no interest in these problems.
So, if you’re like me, don’t worry because you’ve never made a brainfuck interpreter or a database engine. It doesn’t mean you’re a bad dev. You can have a successful career without building these! And if you need them some time, learn them at that time… you may not ever need to.
I don't think the parent was suggesting that the individual developer has a personal desire to build a compiler for the fun of it.
It's much wiser and far easier (assuming your goal is to have a successful career and run a profitable business) if you Keep It Simple, Stupid! Building a compiler/database/CMS/ERP instead of using an off-the-shelf one is, for well-selected business problems, a bad decision, increasing complexity, adding risk, and allowing scope creep.
But sometimes, in spite of your efforts to adhere to the KISS principle, the real world is hairy and some complicated new problems need complicated innovative solutions, and instead of turning a crank to glue together some off-the-shelf libraries, you'll find yourself, in my particular example, way off in the deep end getting PCBs manufactured or, in the article, writing your own compiler. It's not bad, it just means you've got a niche problem to solve! Or that sales did a terrible job of understanding what they were offering, and something basically equivalent to the customer is just an off-the-shelf Wordpress plugin, but instead you're stuck writing a custom CMS.
For some (many?) problems, there is a point beyond which it is more difficult and painful not to build a compiler, interpreter, or other "I have no desire to" piece of software.
The trick is to recognize that point before you have a giant, unmanageable ball of ....
I think I might have used the expression "do want" to vaguely here. Same as you, I have very little desire to write any of those systems, and still try to avoid them as much as possible (as those endeavors usually require more time/energy, etc. and come with higher risks).
Maybe it's better put as "But sometimes all your product requirements strongly suggest that you have to build X and there isn't really a way to avoid it" (and what's available off-the-shelf doesn't fit)?
Thanks for the clarification (even for some rando on the internet), that makes a lot more sense... I think I'll implement a rule for myself where I won't comment on HN posts after first waking up anymore. Agreed that could totally happen!
'Put self into read only mode until after the first pint of coffee' is a policy that has significantly reduced the number of times I look back at a comment I've made somewhere and judge myself for its quality.
I mean, even fully caffeinated, I still manage to beclown myself on a semi-regular basis, but the 'semi' part is still a net win.
Things that you don't want to build (as far as "common engineering wisdom" is to be believed):
- a compiler
- a programming language (not sure that there is a difference to compiler as stated in the article)
- a database (query engine)
- a CMS
- a ERP
But sometimes you actually _do_ want to build that (even if every alarm bell in your engineering lizard brain goes off), and then it's probably better to commit and learn the ins and outs of that specific problem domain.
I think especially the recommendation to commit to it is something that's missing from the article. It's "easy" to point a people and saying "look, you did the thing you said you wouldn't do", but far harder to suggest a course of action.
I personally hit the barrier of "things you shouldn't build" in the past, and I've always been happiest (and professional outcomes have been the best) when I decided to break through the barrier and prepare for what lies on the other side, instead of trying to dance around it for ages.