Hacker News new | past | comments | ask | show | jobs | submit login
The Documentation Triangle, or, why code isn't self documenting (sourceless.org)
107 points by Brajeshwar on June 21, 2022 | hide | past | favorite | 133 comments



The article discusses the structure of documentation as three pillars: the what, the why, and the how.

I prefer to think about documentation in terms of four audiences:

- the student (tutorials)

- the chef (how-to guides)

- the theorist (explanations)

- the technician (API references)

Divio [1] has an excellent set of articles that explains this approach to documentation.

[1] https://documentation.divio.com/


> - the theorist (explanations)

Further it's explained that this is where the "understanding" bit comes in. Ugh, so this school of thought is where some of the terrible modern documentation comes from...

I want the explanations/understanding first COMBINED with a minimal how-to guide serving as illustration for the explanation.

Then a quick link to an API reference that INCLUDES snippets of explanations and usage examples (that you'd find in tutorials or how-to guides).

Separating thinking about documentation this way leads to horrible documentation where you start with dumbed-down tutorials missing lots of crucial information, and then you have to put in tons of unnecessary effort putting disparate things toget ther in your head. FFS, programmers are rarely students since most of what we learn is learning-on-the-job and never chefs or theoreticians. Tutorials always waste your time so you'd prefer to start with something much denser... only that the only thing denser you easily find are the API docs, and you lack the explanations to understand them and usage exmaples so you have to hunt them for yourself... yuck.

As an example of GREAT documentation that combines things well (the API docs contain concise math theory / formular / explanations AND usage examples), take a look at some of the Pytorch docs: https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.ht...


I disagree that writings for all these audiences should be dumped into a single disorganized pile. If I'm looking for API docs I don't want to parse through your contrived how-to combined with a shallow tutorial combined with a long treatise on your principles, just give me the damn API docs. Writing with a specific audience in mind is vital to get good results.

That said, I'll agree that these should be interlinked. A tutorial that doesn't ever link to practical how to's is terrible, and please include references to theory in your api docs so I can gain a deeper understanding when the design doesn't make sense to me, etc etc. It's easier to write and read when you are assuming the audience of a particular work; you can switch roles when you click on a how-to article, but trying to inhabit all personas at once is exhausting and unproductive.


> trying to inhabit all personas at once is exhausting

There's no personas, developers don't have three different brains they keep in the fridge and swap the ones in their head with.

Just admit that sure, interweaving information to get good quality docs is exhausting. And that we invented this "write for an audience" crap to simplify the work of the writers - "we're lazy or we have limited resources for writing docs, that's why we're doying it this way".

It's the same for writers and journalist, the "write for a specific audience" trick is way to lower the effort needed to get reviews / publishers' attention / clicks / views / shares etc. You do it when you're getting started if you ever want to get started I guess. But large scale it produces shallow literature, hard to fact check or contextualize journalism etc.

You do it for yourself (the writer) or to save resources for your companies (sure, interwoven/mixed docs are hard to maintain - I'm not even sure how eg. Pytorch's team manages to do that, but probably being the leading ML frameworks attracts top talent from the whole planet to one open-source project so it's doable for them...). But don't sell the bs that you're doing the reader/consumer any service, keept it hones to yourself and others!


Also

> a single disorganized pile

Nobody proposes a "single disorganized pile". Sure, it's hard to eg. put bite-sized examples + condesed 'why' explanations into API docs and to maintain such docs, but it's doable - in the ML community some people use notebooks for docs wiriting and sometimes even for development by generating the code from them (sure, this is utterly extremist and I don't like it myself, but it works for some), some kind of renaissance of "literate programming".

I don't say we revive literate programming, but going haf-way between that and "a disorganized pile" is what I meant.


I agree, the tutorial and the explaination should go in tandem. Start slow and help the users to form the right images in their heads in the beginning.


The same excellent documentation framework is also available on this company-independent website: diataxis.fr


...this is questionable advice that goes against the interest of programmers having stuff to do - we're not segmented into audiences.

We want multi-level docs combined/spliced/interwoven together so we can get our work done without wasting gandalfing the docs.

> While redesigning the Cloudflare developer docs

...oh, so this is the reason for CF's docs totally uncomprehensible and confusing mess. They've made getting to anythin so comfusing that AWS seems a breath of fresh air in comparison.

I dunno, maybe other peoples' brains are wired totally different than mine, but boy, these new fever of "writing better docs" seems to produce the exact opposite effects whenever applied.

The few enojoyable docs nowadays seem to be written by old-school unixy geeks and/or people that are not programmers or worse, "documentation developers": physicists, mathematicians, biologists, quants, data analysts etc. seem to do a nice job when they have the time.



Wouldn't all of those audiences still need to know the what, why, and how? In different amounts, sure, but they're all still important.


Just watched Daniele's talk. Brilliant, practical advice. Thanks so so much for the link.


The problem I wrestle with is “comments lie.” Not all the time of course, but often enough that I often end up reading the code to make sure anyway. It’s those very “why?” and “how?” comments I’ve seen lie. My own have lied to me. As have my teammates. They go in at one point, the code evolves, the comments become misaligned. I’m not trying to justify no comments. I just wish there were a better solution to this very real issue.


I have experienced it, and never have I found it anywhere near as bad as no comments and "self documenting code".

IME it's mildly obvious when the comments are obsolete or out of whack. It's testing one hypothesis. Whereas with no comments, there are basically an infinite number of possible hypotheses as to what the code does.


I think the best solution is having a few tactical comments. Imo the best setup for a project is:

1. Some high-level documentation explaining how the key systems work and interact conceptually, to give context.

2. Comments for anything non-standard (i.e. we're using some weird encoding here instead of JSON because of XYZ reason)

3. Comments for the tricky extra complicated bits

For everything else, well-written code following standard conventions and good naming practices is enough.

Also choose a statically typed language, so that your function signatures will give you a useful contract to work with, and it will mostly be pretty easy to trace the intent of your code.


> Also choose a statically typed language, so that your function signatures will give you a useful contract to work with, and it will mostly be pretty easy to trace the intent of your code.

Furthering this, avoid:

- "Stringly typed programming": where everything has a static type, but they're all `String` (or `Int`, `Float`, etc. instead of something with domain meaning). This gives very little information to the reader, and makes it easy to mix-up values.

- "Boolean blindness": where we query a bunch of booleans to determine what data we can extract, e.g.

    Result::hasUserId : Boolean    Whether this result contains a UserId
    Result::getUserId : UserId     Returns a UserID if hasUserId; otherwise error

    log("Request came from " + myResult.hasUserId? myResult.getUserId : "anonymous guest")

Compare this to e.g.

    Result::getUserId : Option[UserId]

    log("Request came from " + myResult.getUserId.orElse("anonymous guest"))


This. I regard comments as something a bit negative. You use them if you have to but you see if you can write the code so you don't need them. If it's internal to my code I can almost always get rid of the comment, I end up with most of my comments referring to external matters.


I had the opposite experience. I never read a comment worth reading.

Reading code was always strictly better. Truthful and often easier to parse and understand.

And with code you can actually converse by putting in breakpoints, disabling parts of it and observing it what it is actually doing, how and infering why.

Documentation was always dead almost always outdated prose.

Even when it was up to date and accurate I often understood what the person writing a comment actually meant only after I read and understood the code it was referring to.


> infering why

To me, this is where the most valuable comments are. The more that your code is a representation of business logic, the more you have weird "whys" to fill in.

Any kind of plan that involves everyone on the team inferring the same "why" is a terrible plan in my book.


Perhaps it comes down to a preference of reading code depth-first vs. breadth-first. Reading code only is best for depth-first I guess. Suppose function A has a call graph that is 3-6 levels deep, with 3 immediate children.

If you want to read it breadth-first, you'll want some context as to what the three children do. If you want to read it depth-first, the comments are somewhat redundant, because you're going down to the source anyway.

I rarely have a desire (and even less often a strict need) to understand the whole call graph of a function. If it works fine, tests pass, and I want to understand / change one specific behaviour, I need to grok a slither of the entire possible call graph. Comments help guide where I'm going then. If grokking the whole thing, I see how comments are at best a waste of space.


I think that if you want “what the code does”, detailed description in the commits is much better, since then you can at least see the snapshot with its original context.

You can even go through a particular file’s associated changes to see how it’s morphed over time.


This is an issue of bad habits. "I am just quickly going to change this tiny, little thing" — you are saying to yourself and forget about the existing comments.

Some tips for better habits:

- whenever changing existing code, read the comments

- if you know the changes are going to be big, just delete the old comment first

- if the changes are minor, but change the code add a TODO: check comments on top of the existing comments

- try to keep comments tied to the thing you are commenting, comments that describe interactions between components go to the outside scope, this way there are less surprises where comments need to be updated

- consider writing comments describing what you are going to do before writing the code. On top of writing the comments this acts as an additional reality check and quite often leads to better code as well


Why cant we have comments that are tied to a hash of the block of code the comments are refering to?

Have editor plugins that highlight if the hash no longer matches, then you can evaluate if the comment needs to be updated or not and update the hash.

This would eliminate the problems raised about working on teams and people not following procedures.


This is fine if you're the only person to ever touch this code. But if other people will modify the code you can generally not rely on them following your idea of "good habits"; therefore it's sensible to help them not shoot themselves in the foot.


> This is fine if you're the only person to ever touch this code. But if other people will modify the code you can generally not rely on them following your idea of "good habits";

Teams of two or more individuals can establish norms with only a little more difficulty than individuals, and can provide accountability to those norms better than an individual.


It should be easier to keep comments in code multiple work on up to date, since there would be code review that lets everyone keep each other honest.


Unless you are editing somebody elses code, any team working on code can (and in my opinion: should) agree on some common standards. If you are not taking a day to discuss this, you are saving time in the wrong space.


The solution is to just let it be. A why comment encodes the programmer's belief at the time. This is very useful.

Just because the comment is at odds with what the code is doing doesn't mean that the code is right. The code wins by force because the comment isn't executable.

Sometimes the comment is at odds with the code because the code should be the way the comment says. It's not that way because (1) someone changed it. Or (2) it never was that way; the comment is just wishful thinking.

If you want to know which of those two it is, you do the archaeology.


I'm more concerned with the fact that code lies. Everyone knows comments lie. Only some people know that code lies, and the biggest liars don't seem to get what the big deal is. I understand the code, they'll say, and then give a several minute explanation about how everyone else could 'easily' understand it.

Nobody wants to memorize your code. And you shouldn't want to either. You can't memorize code that other people are contributing regularly to. So either you won't know it when you come back, or you're subtly pressuring everyone to move their changes outside of your code, and leaving your code alone. Even if it has a choke hold on data flow in the system.

But that almost misses the point, which is that yes, code that doesn't look like what it actually does can cause people to miss the real problem. But code that looks like it's a potential source of the problem being investigated steals attention from the real culprit. And no amount of memorization is going to completely solve that problem. I think people mistake the notion of 'code smells' as the act of an overly fastidious mind, but the neat freak is forever asking the slob "how do you find anything in this mess?" and that problem plays out in code, but amplified. Like if you were looking for your credit card and it wasn't a matter of whether you put your wallet where it 'belongs' or if it was in your bedside table, but instead you have 9 wallets scattered around your room full of customer loyalty cards.


I saw a lovely example of this a few months agp. We had a list of ids representing completely different business cases, and code like this:

  // Because of [sane business reason] we need to remove all 89's
  something.removeall(75);
I found this because people in case 75 were logging a never ending stream of tickets for months that the program was doing weird things. No shit, sherlock. Talk to business, everyone agrees 75 makes no sense at all and it really really should be 89. So before we change this back to sanity, let's check the git history:

  Date: 3 years ago
  Message: Urgent fix for major downtime.  (No detail of course)
  Commit by: someone who left a little bit later 
  Change: 1 line patch, changes 89 to 75
Asking around, nobody technical knows anything anymore, the whole team got laid off around that same time 3 years ago and the new team basically rediscovered everything from scratch. Some business people do remember it, it was Very Bad, they don't know exactly what it was or how it got fixed, but anything that might cause it to happen again is strictly forbidden.

What would you do? I got reorganized to another team, lucky me.


This is a great example of why documentation is important though. If that comment had documented why 75 had been removed and what the impact of that was you would know how to address this situation.

In fact, even the fact that the comment is now wrong tells you that the change was ill-considered and needs to be revisited.


I have to disagree here.

Any fix should produce at least a test to guard against regressions of the bug it's trying to fix and encode the correct business logic.

The test name would then explain the why. The test variables present the story of the what. And finally, the API calls tell the story of how how.


Most commonly, the comments end up lying about the "what". Because so much of the "what" comments are boilerplate code that we unconsciously tune out, it's the first to go stale and out of date since we also don't remember to change them when the "what" changes (and also it's a two-place change, which always falls out of sync).

Next up is the "how", although it's not as likely as the "what" since it's not as common (as it should be - you don't need to write a "how" about every list iteration).

The least likely to be a lie is the "why", since the circumstances leading to a "why" rarely change, and when they do, you usually end up replacing the whole section.

And so, your "what" should be almost entirely in self-documenting code, your "how" should be used judiciously, and your "why" probably won't be a problem.

The more unnecessary comments you write, the greater the risk of them going stale due to maintainers glossing over them. When they're a rarity, you take more notice.


Like TFA suggests, right at the end, this is where code review shines. Keeping documentation valuable is a discipline challenge, and we all fail from time to time at keeping ourselves disciplined. If you have team agreement on the level of documentation that gives good ROI for time spent maintaining it, you can rely on your team to help catch your slips just as they can rely on you to help catch theirs.

Using something like ADRs[0] can help provide the structure the team can look for when it comes to "why" documentation. I recommend a standardised README across projects. If it's "what" comments look for language tooling that will execute documented examples as test cases. If it's "how" then make sure there's value in it - do you have disaster recovery exercises, does your incident response team use your "how" documentation to triage issues, do stakeholders outside your development team get consulted on contextual changes in your application, etc.

[0]: https://brunoscheufler.com/blog/2020-07-04-documenting-desig...


Good "why" documentation can sometimes be fairly localized though. ADRs are great for overarching design "whys", but are not really the best tool for highly localized "why" documentation.

For example: sometimes you may have a codebase where whenever "X" is done, there is an automatic retry mechanism of failure. But then you have one spot where there is no automatic retry. And it turns out there is a very subtle and non-obvious reason why an automatic retry here would be a problem.

The code then deserves a comment explaining what this subtle issue is, and and why it means automatic retries must be avoided here. Without such a comment, the next person to touch the code may well just assume the automatic retrying was forgotten, and add it, causing say the painful data corruption bug to return.


Even outdated comments I still find useful in that they tell me how things used to work, and why they used to work that way.

Which is important context for figuring out how it works now.


It is important tho to note that wrong documentation is significantly worse than no documentation.


I disagree with that. Sure, wrong documentation can mislead and confuse, but it can also be very helpful.

Lying comments have often helped me because it's often useful to know that a particular person at a particular point in time believed something to be true, even if it isn't true any more and even if it were never true. In a pile of spaghetti code, a good lying comment can point to the needle in the haystack where the root cause of a bug is hiding.


I have not found that to be the case. usually it’s easy to tell when documentation is outdated, and then i just use it as reference to figure out how it works now.


Maybe not so easy. I would say every documentation page needs a "Last Modified" date.

Similarly, in a production environment, I found that when there was no time to keep the docs up to date with the latest changes, I marked them on the cover page as PRELIMINARY just to put readers on alert. And so, similarly, should a document not getting the attention it needs be marked as NOT WELL MAINTAINED ? I get the feeling this idea would not fly. Not in a commercial environment anyways.


Shouldn't code-reviews make sure that comments are relevant?


Theoretically, but humans are less reliable than a compiler at catching issues.


I guess, in an ideal world, our tools could ~pair code and comments introduced adjacent to each other in the same commit, and then ~visually surface how many commits have touched the code without touching the comment (or whether it has been orphaned).


I've always wished you could put function tests in the comments that would compile time evaluate (or at least let you run a localized version of just that function), and could then WARN YOU when you've broken parity with whatever test/example you put together right there.

Yes a unit test will show you that something is changed, but often it's the comments that don't get updated.

Hell maybe just let us flag the comments that will need to be changed if X or Y unit test fails. Just some way to better link comments to code adjustments.


If your code is versioned, it's still better to have outdated documentation with an obvious older timestamp than no documentation.


Apparently-meaningful identifiers in code can also mislead and lie, to precisely the same extent as they can inform.


By that same token, code can lie too.


1. "Comments are bad" is bullshit.

Definitely put the comments in the docsting or equivalent for your language, not in the middle of the function.

The best place for comments is nicely grouped under (or above, an elegant C++ convention) the interface. If the function signature is 200 LOC away... the problem isn't the comments.

But they should be there.

2. People, need, diagrams. People NEED a picture.

At the minimum, the bare minimum, your application needs a high level and low level diagram explaining how it works.


Diagrams have their place, and a high level diagram can be very useful, but I find that low level diagrams usually tend to be a lot more trouble than their worth. They get complex and both hard to understand and maintain, which means they tend to get ignored when changes happen and become out of date.

I've found that if I'm struggling to create a diagram for something in Mermaid then I've gone too deep in my explanations and it's time to step back.


Maybe I need a better way to phrase it.

High level architecture diagrams, user journey diagrams, low level diagrams, are probably all distinct topics.

I feel all three are extremely important, but that's bordering on my opinion.

> but I find that low level diagrams usually tend to be a lot more trouble than their worth

Out of date low level diagrams are surprisingly useful.

One of the things that I've found is that, with extremely high LOC projects (LOC definitely measures complexity), you'll find that low level diagrams are worthwhile, at the cost of a time investment in generating them.

When/how to do them is a certainly a fine art. There's no right answer.


It sounds like our experiences differ when it comes to low level diagrams. While I'm dealing with a lot of complexity in systems I'm developing, they don't tend to have a high number of lines of code - or at least they don't once we get the architecture right :)

Perhaps there is a correlation between complexity due to a high number of lines of code and benefits provided by low level diagrams. If there is then it does pose the question of when you should invest time in creating and maintaining low level diagrams and when it would be better spent reducing the size of the codebase.


I do not usually use diagrams. Therefore... I'm not a person? :)


Have pity on us mortals.

It your first week onboarding significantly easier in my experience, the first time I encounter a codebase. I'm sure people make do without.

Lotta science behind it, humans work much better if there's a diagram with coloured shapes/lines involved.


I see tests as documentation. They show you how to interact with a component and what the expected behavior should be. In BDD, you go one step further and write down a formal description of inputs and expected outputs.

Would agree that the higher level documentation (diagrams, process interaction, etc.) can be written down once the dust settles.


Totally agree: tests are the living documentation of usage!

And if the team is properly collaborating, they are linking issues and merge requests with commits, providing historical snapshots of the ever evolving context!


I find tests to be terrible usage documentation. Better than nothing to be sure, but compare compiler tests to a tutorial or example code for learning a programming language. The former are usable, but often full of obscure edge cases, layers of abstract test infrastructure, and mocks. The same probably applies to the tests in your codebase as well.

They're just not designed for pedagogy in the same way docs should be.


> I find tests to be terrible usage documentation. Better than nothing to be sure, but compare compiler tests to a tutorial or example code for learning a programming language.

For example, Rust has documentation which is executed as tests (doctests). That way, when the code changes, the docs change automatically.

To your mocks counter-example: How exactly would you propose to do it otherwise? If you write down an example which works now, how would you guarantee it still works if i.e. you don't use mocks but a say, a real database, which you then change.

You would be in the case of 3 green parallel lines which are red and intersect. In a powerpoint/doc you can write whatever you want. Doesn't actually need to be true.


If your talking about learning and pedagogy, that's an entire different subject. When you are working on obscure code, you are not learning how the programming language works at the same time (I hope not). If you do, split the problem in two: learn on one side and apply on the other one.

And yes tests are not the best candidate to learn how to do stuff. The best candidates for this are out of band documents / media / training.


The language thing was simply an example of an interface where it's very clear what the difference between proper documentation and tests is. It's the same sort of problem, but a slightly different context.


Ideally, your tutorial/example code should also be among the test cases.


This. Yes! Tests are documentation. And sometimes documentation and tests are called specs.

Naming is hard. So is documentation. I did that just to trigger fallacy trolls, but sometimes smart naming is documentation. Maybe that hard work of naming is worth it.

Verifying that code flow makes sense via code review guarantees that code is documented well enough.

As does handing it to the intern, without any "training," and asking for a new feature. Or an expert in a different part of the stack but not this code. As does playing "spot the bug" and having most people understand the code, even if they don't see the bug. Heck, just passing through some linters and style analyzers can get you pretty close to not needing documentation.

There are many things that count in different ways. What does your team want/need/desire? Does the answer change when it's someone else's code and their own?

The flip side is a lot of prose in the human lingua franca wherever you code. (Probably American English but that's another screed.) That means people with coding chops also need literary chops. Or a friend who has them. It means that you're not only judged on how well you solve problems with code, not only with how easy that code is to inherently understand, but also judged on how well you can explain to someone else what it is and how to use it.

If you really need that heavy documentation, hire a tech writer. Just like you probably specialize in a kind of dev, you can find specialists who specialize in making things make sense to devs.

(I often recommend diagrams just to make boundaries clear to everyone "at a glance" but don't think those really count as documentation in this context.)


Article is confusing and gives zero actionable advice + a fancy "triangle".

In general, after you get to a piece of code you need to have thre questions answered:

- what problem does this solve? - this is actually the problem how-to guides solve, in practice your particular use case is NOT even close to the example use cases, so you'd just read the how-to guides as the no-bs higher level explanations because you know anything higher level is often bs

- how do I get something done with this? - API docs combined with dense explanations combined with dense usage docs (I don't want to have to piece them together myself through my own effort, I want them all in one place)

- what is the reasoning for it being implemented/architected/etc. this way? - design docs + "philosophy" section of docs + architecture diagrams


I think the author's point is that code by itself inherently can't fulfill all three sides of the triangle. The cleanest code in the world might make it immediately obvious what a program is doing, but not why it's doing it, or the context in which it's doing it.


Here's an example of a very useful comment:

  # using heapsort instead of quicksort
  # because we care about the worst case

  heapsort(data)
Code alone cannot tell the reader why heapsort is chosen, nor whether that's how the code should be. And tests are unlikely to capture this either.


I agree, these are cases when you need comments inlined in the code.

Describing what a single code is doing is only useful, if the comment is easier to read than the code itself. That can be useful for arcane cases, such as dirty regular expressions, bash scripts with several layers of pipes, or non-standard pointer manipulation in C.

Much more important, is the why, as in your example.

But there is also the what when it applies to code design that spans more than a few lines of code. This requires documentation. As long as we follow a design pattern, the documentation is covered by the pattern description.

But if we invent a novel design, either a one-off or a new design pattern, it needs to be described in text and figures, not just inline in the code.

Similarly, and even to some extent when staying inside a design pattern, object models need a conseptual design, where the ideas and purposes behind each class or interface is explained, as well as where the relationships between classes are visualised.


Commit messages and calling people out for breaking commit message history works to an extent. If you can keep someone from breaking commit history badly enough.

I still maintain we're awaiting another generation of version control where commentary on code is cumulative, instead of just set at the time of commit.


Yeah, it's hard to retrospectively unbreak commit history on legacy projects. Git notes[1] might be a possible solution, but I've not had that be particularly useful in practice (due to lack of support in various other systems).

[1]: https://git-scm.com/docs/git-notes


It is excessive to claim that code cannot express the "why". You can almost always express the "why" with code if you really want to, though whether or not that's a good idea is another matter.

For example, you could express the same information that the comment in your example provides in the following manner with self-documenting code.

    enum PerformancePrioritization {
      FastWorstCase,
      FastAverageCase
    }

    function sort(data: List<TypeOfData>, performancePrioritization: PerformancePrioritization) {
      if (performancePrioritization == FastWorstCase) {
        heapsort(data);
      } else {
        quicksort(data);
      }
    }

    ...

    sort(data, FastWorstCase);


Okay, here's some code I wrote [1], with the existing comment:

    /*------------------------------------------------------------------------
    ; For the RR OPT, the class field is actually the size of the UDP payload,
    ; and the TTL field are a bunch of flags.  We have a separate field for
    ; the UDP payload size, so here we make the adjustment behind the scenes
    ; so you don't have to know this crap.
    ;-------------------------------------------------------------------------*/
    
    if (answer->generic.type == RR_OPT)
    {
      answer->opt.class = answer->opt.udp_payload;
      answer->opt.ttl   = ((data->rcode >> 4)  & 0xFF)    << 24
                        | (answer->opt.version & 0xFF)    << 16
                        | (answer->opt.fdo ? 0x80 : 0x00) <<  8
                        | htons(answer->opt.z & 0xFFFF)
                        ;
    }
I'd like to see how I could do away with the comment and make this more "self-documenting".

[1] Code to encode/decode DNS packets: https://github.com/spc476/SPCDNS/blob/05aead581acf050edf610c...


I feel that this code is its own counter-example.


An extra nine lines and extra runtime complexity just to avoid putting it in a comment.


I did explicitly mention that it might not be a good idea, though this is an exaggerated case since situations like these realistically constitute a very small portion of actual codebases.

But a sibling comment [0] did provide an example that would give a much simpler approach to accomplishing the same thing in this particular instance, with zero extra lines and runtime complexity.

[0]: https://news.ycombinator.com/item?id=31822657


And then you have an automated checker which tells you FastAverageCase isn't used..


How about:

    import heapsort as worstcase_nlogn_sort
    worstcase_nlogn_sort(data)


While it is useful to have a short comment like this in the code as a quick reminder, such design choices belong in a more detailed technical design document which explores multiple design options and provides rationale for making the final choice. Usually, there will be a trove of useful stuff in code review comments etc that should find its way into such a technical design document. To ensure it gets done alongside code, I prefer wiki docs that sit alongside code and doc changes can be part of the same code commit as well.


If comments can lie then design documents can lie even more. There are three tiers of documentation in my mind. Code, comments, and documents.

Code can't lie (although it can be misleading with improper naming or bad abstractions).

Comments can be completely false (although they're easier to catch because the comments sit alongside the code)

Design docs almost always lies and it's hard to catch the lies because a design doc written 2 months ago is never going to match the resulting code perfectly. In my experience, people rarely update documentation unless there's a dedicated employee/developer advocate who is helping enforce and drive the culture.


You got it backwards. You have design documents and they should be source of truth. If you change your code and code now contradicts design docs, it's the code that is wrong. Said that design docs should not be super detailed, they should capture general assumptions and conventions, code can have all the details, but code cant contradict design docs.

But when you too lazy to update design docs, code and docs get out of sync. And it's always easier to just leave docs unchanged, because "hey, code works".

It's a bit similar to word processor compatibility issues on linux and windows. You can have perfect implementation of specification on linux, but Microsoft does some changes that are not specification compliant and now a doc created on windows does not open on linux. And from users perspective it's linux fault that file can't be opened, even though it's actually microsoft that is not following specs.


Your last paragraph is pretty much why in practice the design specs go out of sync. The reward systems reward correct code not correct docs. Unless the team is very strict and code reviews doc changes.


> a design doc written 2 months ago is never going to match the resulting code perfectly.

That depends how you're writing design docs. If a design doc contains arbitrary flowchars about what happens in the code, it will change.

But if you come up with designs similar to design patterns, you can document the pattern once, and use it in your code the way you use a design pattern.

Concepts that span a large codebase is also enormously paintful to figure out from reading the code. For instance object models / microservice architectures, complex state machines or security frameworks are hard to catch implicitly from reading the code.


In my experience, source code tends to survive forever. Things outside it don't, whether through attrition via document retention periods, the IT department noticing nobody uses that document archive and shutting it down to cut costs (not before being told "nobody uses that!"), or hundreds of other possible causes.

I've worked on code older than I am and those little comments that probably should have been design docs were a godsend. Nothing else had survived.


> such design choices belong in a more detailed technical design document

I agree with you but in my experience unless you are writing this documentation in a regulated industry (with external incentives to keep the documentation up to date) the technical docs will eventually go stale or out-of-sync with the actual implementation.


> such design choices belong in a more detailed technical design document which explores multiple design options and provides rationale for making the final choice.

We're talking about a line of code that has 2 words here. "A method to sort something" and "something to sort".


As someone who recently started working on a project, that requires a lot of domain knowledge (database internals), I can definitely say that the code may be perfectly readable (variable names, patterns, even comments), but still not self documenting, because you need to have so much context, crammed in your head.


No silver bullet. It helps to write thoughtful comments. It helps to pick thoughtful names. It helps to weave a story out of the names you've chosen.

I have been picking at some thoughts on the ~naming side of this recently... https://t-ravis.com/post/doc/what_functions_and_why_function...


What often frustrates me is that a lot of doco isn't read: it's "easier" for people to ask (I don't exclude myself from this).


I'm using "What", "Why" and "How" here as defined in the article.

Problem with documenting in the code itself: The "Why" is further from the code than the "What", and the "How" is further away still.

Explaining "What" a function does is close to the code, I just put some comment lines before the function/type, etc. and maybe some into the definition. They are just to make the code better readable for the human.

But where do I put the "Why"? I could put it with the "What", but often the "Why" depends on somewhere completely different in my code (or someone elses code). eg. "Why am I using a map here instead of a list? Because the remote API expexts a JSON object and the map is easier to Marshal into that".

But what if the remote API changes? Okay, I change the code that deals with it, and update the comments for that code. Do I also change the comments everywhere else where this map is used? For that matter, is it explained anywhere else in the code why that is a map instead of a list? The "What" says "this code iterates through the maps values and appends an underscore to each", but is the "Why", which explains why that is a map there as well? In every function that works on that map?

My point is, not only is it difficult to explain everything in the code, it's also very very difficult to keep this documentation up to date, because the relationship between a comments location, and the code it belongs to, becomes more complicated the further up we go from the "What".


Wish the article went into a bit more detail on the thought process.

Code absolutely can be written in a self documenting way, just that most people don't structure their code like English/natural language. Also some languages have too much syntax bloat to be written effectively in a self documenting way.

I agree with the principle that some things are better expressed through documentation, such as motivations and higher level diagramming. But the average code base is so far from being self documenting, that the gap there should be closed first before moving on to external documentation.

External documentation creates a second source of truth that often falls out of sync with the code, so becomes a form of technical debt/maintenance burden.

The best way would be exporting docs from your codebase, and have all documentation related artifacts expressed alongside the code. Diagrams expressed in a programmatic DSL etc


> The best way would be exporting docs from your codebase, and have all documentation related artifacts expressed alongside the code

I LOVE Golang's approach to this [1]:

- Documentation generation from code is built in to the language toolkit

- There is a standard for writing the comments so all of them look the same across every project

- It even supports code examples which are visible as a REPL in the generated docs

When people ask "what's so great about Go?". It's stuff like that which is hard to succinctly describe but makes a huge difference in overall quality of Go code in the wild.

[1]: https://go.dev/doc/comment


So basically what Common Lisp, Smalltalk, Java, Perl, Python and Ruby have provided before Go was a thing.

Many of the Go features are only a win when the competition is C.


I can’t speak for the others, but Godoc is IME way better than JavaDoc, which is both hard to write, and hard to read inline due to all the f*ing HTML.

More to the point, just because some platforms provide equivalent functionality doesn’t make the GP wrong.


A nice Javadoc touch that Golang could use is to break out the documentation of arguments and return values.


So here's an example of the string package: https://pkg.go.dev/strings

In what way would the JavaDoc @param/@return/etc annotations actually help? What Godoc has is more than suitable for the vast majority of cases.


Have you seen the IDEs that show that documentation for the variables due to the position they take on the function call?

What is on that documentation site you linked is just a text formatting guide, it's absolutely not enough to get a powerful documentation system. But yeah, it's simple.


I have. My editor also gives me documentation for every function that I need. Plain text is more than enough for that use.

What kind of function documentation are you reading inline lol? Does it have like 30 function arguments?


They provide the parsing engine with better clues for the AST that is given to the JavaDoc agent (aka doclet), which allows to customize the document generation process.


Which didn't answer my question at all. Adding more structure to the AST with @param/@return/whatever doesn't make for better documentation. It's useless crap in a real world scenario.


Let me guess, another ACME fan that like Rob Pike doesn't use anything fancy as editor, including syntax highlighting.

That crap allows for tooling that Go will never have thanks community culture.


With respect, the general level of day-to-day tooling for Go is way better than for Java. Go is designed to be easy to parse and easy to build tooling for.

“That crap” is at least in part a reaction to the endless stream of DocTagFactoryFactoryFactories patterns that came out of the Java world. While I think Java is a decent language with a good ecosystem, I’m completely over the idea that the only way to do things is the Java Enterprise(tm) way.


I use VIM and didn't even know ACME editor existed.

Keep building strawmen, I'll be building software.


This is why I said it's hard to succinctly explain. Non-programmers and bad programmers can't tell the difference between Go and Python (for example) in this context. The actual difference is very very significant.


You see one screen's worth of code at a time. You can't glance the big picture from code because code is about here and now.

With some error handling code, for example, briefly commenting what conditions lead to what exceptions goes a long way.


You can certainly get the big picture if you abstract the codebase in such a way that top level operations are abstracted by functions.

But of course it depends on the context and what you're trying to do.

React/Vue is a good example. You have some Application component that has a set of sub components representing different regions of the page, which have their own subcomponents and so on.

Makes it very easy to dive in from top down and drop into lower level abstractions as needed.


Of course documentation, even when present, even in the code, is often of the "This does X" sort. When you want "You need this to accomplish {larger task}".

It's like walking through the forest and wondering where you are, but every tree has a sign on it that says "tree".


After years of struggling with the question of how to write "good comments" I've come up with a solution I'm currently happy with.

The main problem with comments is they make code less readable because they interrupt the flow of code-reading and thus code-understanding.

But now, I write all my multiline comments in this style:

    /*-------------------------------------------------

      This is a comment

    */
In the IDE I am using (WebStorm) there are keyboard shortcuts for collapsing and expanding sections of code. For comment-sections like above it collapses them into:

/*--------------------------------------------- ... */

What this does is it turns a code-reading distraction (the original multi-line comment) into .. a SINGLE LINE SEPARATOR.

Single-line separators do not negatively disrupt the flow of reading the code. In fact they actually help code-reading by dividing the program into semantically coherent code-sections separated by what looks like a simple horizontal line.

I'm no longer very concerned about out-of-date comments, I accept them as fact of life. They are ok as long as they don't impact my code-reading negatively. I don't need to ponder: Is this comment too long? It is not when it is collapsed.

And comments will still contain much useful information. If I don't understand why some section of code is there or is written the way it is, I can expand the comments around it and maybe I find some helpful information there. Often I do.

I do wish the IDE would offer more support for this style of commenting. It would be great if all multi-line comments would be collapsed by default. I haven't figured out if that is possible in my IDE. I do know how to "collapse all sections" which is what I frequently do. It is a good way to start reading a code-file anyway.

So, the trick is really simple: Start every multi-line comment with a line that consists of a line of dashes.


There are types of software where rushing product is the norm. Everyone read stories of long death marches of computer games. These games have millions of lines of code. I would put my 2c that code in these projects is part of the people who made it. You should not expect to be able to understand code without talking to people. Yes patchwork can be intricate and some things may be lost in countless requirement U-turns. What I think was common in successful projects is that people kept some understanding of the code. They might have no incentives to share it with you ^_^. ymmv ofc.


    import random
    comments = [' ',' ',' #this is a loop',' #is this tehcnically a quine?',' #small optimization']
    code = ['import random','comments = ','code = ','code[2] = code[2] + str(code)','code[1] = code[1] + str(comments)','for i in code: print(i + random.choice(comments))']
    code[2] = code[2] + str(code)
    code[1] = code[1] + str(comments)
    for i in code: print(i + random.choice(comments))


It is a quine if we neglect the comments.

Interestingly, two of the comments are descriptive (answering "What"). The one asking whether this is a quine is in fact a type of communication widespread in code, when authors use comments for communication.


>It is a quine if we neglect the comments.

Sure. Obviously. But if we don't neglect the comments, is it technically a quine?

If one program is different from another in a way that no one can discern by looking at the output or runtime, is it actually different? Are comments part of the code per se? Does "don't interpret this text as commands" count as a command?


Oral Tradition in Software Engineering[1] by Bryan Cantrill really helped me appreciate that long comments in code can be a treasured part of a software space.

I started my career in a pretty low-comment Ruby codebase, but now count myself as a person partial to adding lengthy 'why not how' commentary in programs.

1. https://www.youtube.com/watch?v=4PaWFYm0kEw


Hmm. I mostly agree with the author's statements like "documentation exists so that code can be easily used" and "it [the code] is the most honest account of what actually happens when you run the code." But introducing the triangle as a metaphor infers each member of the triangle is the same type of thing. The "WHAT" vertex is the code itself while the "HOW" and "WHY" verticies are natural language text. "HOW" and "WHY" are not easy to distinguish and seem to be used to mean "context outside the immediate function" and "programmer's intent."

I think the triangle metaphor muddies the water a bit.

But... I agree with most everything else the author says. Context is important and is not present in the code (usually.) The developer's intent is generally absent from the code and should be supplied somehow.

I might even go further to explicitly mention documentation at the top of a function traditionally describes that function's context in the greater scope of the application or module or class while documentation inside the code body often describes the programmer's intention with respect to the function's implementation. (This is just an observation from code I think is well documented. Maybe there are counter-examples out there.)

I have a bit of an architectural bent, so I might have talked about documentation and it's relation to interface/implementation and problem domain and solution domain. Which is to say, documentation should be clear about which domain you're documenting.

Though I don't understand why the triangle is important, props to the original author for saying "no. code is not self documenting."


I blame the OOP culture for the idea that code is self-documenting (and also for the sibling idea that unit tests can replace documentation).

See, OOP worldview is a priori based on the idea that all programs can be written as collections of small, independently reusable, objects. In that world, there is no place to put any high-level explanation of how the parts interact as a whole, or what are any emergent properties of the system, because it is (on the ideological assumption of object independence) not needed.

It's very visible from e.g. Java package system design, there is no place to document what a particular package is for, or what classes are part of its public API and what are internal. And in Java in general, every entity must be assigned to some object, the things that lie outside (and can be shared) are frowned upon.


It seems like some of these issues with Java are out of date.

Java has had a well-defined place for package-level documentation for close to 20 years, since 1.5. It goes in package-info.java (docs: https://docs.oracle.com/javase/6/docs/technotes/tools/solari...).

As for documenting which classes are part of the public API, you've been able to make classes package-private since Java's initial release. The module system introduced in Java 9 in 2017 takes that further; you can explicitly control which classes are exported to consumers.

"Everything must be an object" is still more or less as true as it ever was, though.


Thanks, wasn't aware of package-info.

Yes, the module system is an improvement. Java is becoming more pragmatic as the ideological dominance of OOP is dying out.


Looks like that the answer for most is tools, or some new code convention. While that can be useful, the solution to facilitating code comprehension can only be documentation. And to create documentation, the key component are not tools; people are. Technical writers, editors, and others.


> No program exists in a vacuum. There is always some environment, some organisation, some process that it lives in or serves, and taken out of said context, it is entirely useless.

> Failing to recognise this is the number one leading cause of new users and junior devs being unable to do 'this simple task' (don't quote me on this, I have no data).

This. At all three startups I've worked at, founding/senior devs err on the side of assuming that any process not involving writing code is trivial and self-explanatory when in fact those things are some of the biggest time-eaters. Big organizational wins can be had by documenting them well.


We have an Excel report generating code at work.

It's extremely straightforward to read... once you understand the requirements and limitations, and some intricacies of Excel format.

So there's a 5-page accompanying document that explains those.

Code is very, very rarely self-documenting on its own. Because more often than not you need to know most of the code, most of the context and history of the code, and the business domain this code is intended for to understand it.


I would have thought that “what = context” and “how = code”


I think there's a perspective slip here.

I assume it means: What does the code do, why does it do that, and how do we leverage it.

These perspective issues are tricky. There's a difference between "what does the code do" (low level) and "what does the program/command/API do" (high level). There's a difference between why we need the code to do x, and why the code does x in one way and not another.


Documenting the ‘how’ part, i.e. the documenting the assumptions that the author had on the context this code runs is important; you guess just enough by grepping calls to the function in the codebase, add add some code that works now… and then the original author or another person adds a call that breaks the assumptions you made, and breaking everything… subtly.


Code should explain the what, tests should explain the why.

Done correctly comments should be redundant.

That doesn’t mean actual documentation isn’t useful, but it only captures a point in time, rather than the actual living code.


The triangle has What, Why, How. But don't forget Who & When. Make sure the footer has a last-modified and a contact/feedback email address. Maybe Where is not so important tho.


Where is important too. I like if I can look at the codebase and see where the (particular) action is. There is too much ravioli code that hides this behind interfaces and factories.

As an aside, I think the article has what & how swapped. The code tells you how, but not what (is the correct thing to do).


I meant Where geographically. But yes, Where in the maze of twisty winding function calls and class hierarchies and delicious concurrent ravioli threading architectures.


Isn't that what the commit history is for?


> Why (comments)

That's what we do in my current company, but I vastly preferred what we used to do in my previous company, which was:

> Why (commit description)

which prevents comments obsolescence and code dirt


And the latest commit is "shifted all indentation per linter" by someone who didn't write the code. Viewing the history of a file is vastly more a pain in the ass to a useful "why" comment.


> And the latest commit is "shifted all indentation per linter" by someone who didn't write the code

IME it's not like that at all, commits like "shifted all indentation per linter" are not so common. It's also easy to skip them.

> Useful "Why" comments

"Why" comments are not so useful to me, I guess that is the point. The add noise in the code that can be moved elsewhere.

Consider that I'm not talking about specific implementation choices comments, like

    # using heapsort instead of quicksort
    # because we care about the worst case

    heapsort(data)
which is perfectly fine to be in the code, I'm talking about "why" details that may be not present in the Jira/Whatever issue because they are related to e.g. why a bug raised up from a technical point of view.


IME commit descriptions almost never get read by anyone.


Im my previous company we used to write them and used to read them, and used to get frustrated when some old commit didn't have enough description.

In this company noone cares and we even have squash & merge mode with description truncation, but methods comments can be longer than methods themselves.

It's a question of team mindset I guess


Even api docstring comments are generally terrible. They aren’t sufficient and the infatuation with automatic documentation in our industry is terrible.


Code could be self documenting if it had constructs for everything we might want to say, but I don’t think that would be a practical language.


Often times I read the code instead of the documentation.

Why?

It's faster, but more importantly the documentation says what should happen. The code says what does.


[flagged]


The article is more of a reasoning than a question of why. I find it worthwhile to read and help me clarify when answering questions to junior developers on why they should focus on writing helpful documentation.


Why not tell them, "Newton and Einstein wrote documentation. How do you think we learned from them? Oh, and by the way, we fire developers quickly around here who don't document their code properly."




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: