> Consider this real world example: a_crszkvc30LastNameCol.
> It took a team of maintenance engineers nearly 3 days to figure out that this whopper variable name described a const, reference, function argument that was holding information from a database column of type Varchar[30] named "LastName" which was part of the table's primary key.
Most likely tracking down what originally populated the variable and how it's used. I'm assuming the code base was large and spaghetti like.
I'd say it's somewhat akin to proving a negative. Sure you get that it's a const, reference and function argument which likely refers to the Last Name Column. However is that all it is? Or is there more meaning in the name? Meaning that could be crucial to understanding how the application functions? Does one of the characters mean function reference or does it refer to something else? Apparently not in this case but how can you know without investigating?
Spending time to understand the author’s intent of some code is one of the hardest parts of reading code.
It’s like when you find a nil-guard in a function. You can’t truly refactor it until you know whether it was added defensively just-in-case or whether nils can still exist in the system at the function’s callsite, calling for further upstream examination.
There’s a certain hell in trying to follow a code base where every function is infected with ‘if (!arg) return’.
After dealing with code absolutely plagued with that pattern, languages with non-nullable types are amazing (Flow, Typescript in strict mode, Kotlin, Rust, ...) and I don't think I'm ever going to voluntarily switch away from them now.
Well, since the repo more or less took the contents verbatim from the original author (unless they are the same), and here presents it _more_ clearly than the original, I'd say the repo owner failed.
At the very least it should be presented in a more unmaintainable way.
This is suppose to be funny , unfortunately this is the sad reality of what you'll find in 90% of Enterprise / Startups environment as well as THE majority of open source repositories of popular open source projects.
I've noticed this especially true for JavaScript and Go.
In JavaScript , because the language is still evolving and there is no real standard about how to architect code with this language unless you are using a entire "platform" like Angular / Vue / React witch will highly influence your coding style.
In Go because Go developers are performances obsessed , coming from C/C++ background and do believe that naming their variable with one letter will save 1GB of memory allocation , 1000 CPU Cycles and make their code "Clear and Concise"
Ultimately , because there is very little standard in this industry in terms of code governance and a large percentage of projects are outsourced to other companies , we end up with what is described in this repository.
It's also perfectly acceptable (and preferred) if the code maps cleanly onto some existing mathematical terminology. 'v' is more readable than 'velocity' in the correct context.
The problem is that you get too close to the code you write, and make assumptions about its obviousness that simply aren't true. Far better to be explicit at the cost of a few extra keystrokes.
I would always prefer the first one. My general rule of thumb is: If a variable has the scope of exactly one line, or if it is an incremental counter, use one letter.
I’ll admit to a few larger funcs where I used one letter variables. It always starts with me thinking it will be a three-line routine, then It turns into a 30 line monstrosity that I don’t realize until I get a code review comment like “wtf is ‘zz = z’ about?”
I find Go code ideal for maintainability, with the "one way to do things" philosophy. Developers often don't like Go precisely because it does not allow them to do "clever" things.
Go is also not performance obsessed - else it would not have opted for garbage collection by default.
As far as variable naming goes, Go convention says - the further from its declaration a variable is used, the more explicit and qualified the name should be to disambiguate it. This to me strikes as being eminently sensible.
For embedded C work I do like to mark global variables with a Hungarian g_, all other prefixes are generally handled pretty well by the IDE's type hinting. Other than that I agree Hungarian can go!
I had a funny now/frustrating then story where I worked with his guy who named five variables in a class a, aa, aaa, aaaa, aaaaa. I was trying to refactor his code and I had no idea what anything did. The company is out of business now, but it was definitely unmaintainable.
"Hungarian Notation is the tactical nuclear weapon of source code obfuscation techniques; use it!" - I'm laughing so hard. This whole article is genius.
> In C, functions are supposed to be idempotent, (without side effects).
What is meant by that statement? Is "idempotent" a mistype/mistakenly selected word? If the author meant "pure" instead, should I file a PR, or consider this some sort of meta-meta-obfuscation? If the author meant "pure", is that actually true of C specifically, or just generally good programming advice (localize side effects to known locations, the fewer the better, don't sprinkle them all over and document where they occur)?
C functions shouldn't change behavior based on state, they do the same thing every time. Instead of changing a particular global variable, any variable of that type should be passed in and changed, then returned. It's good programming advice for functional languages. In OO it makes sense to make state changes in object methods, but not usually the best functional approach.
Idempotent is concept borrowed from mathematics.
It just means:
f(f(x))=x
assuming f is a function (binary operators are just functions (maps) in mathematics, too, e.g. +:NxN->N (read: "plus" is a map from the Cartesian product of N by N to N))
I had a colleague who used to use variable names from his local language in India (we were working in Europe at the time). So hwndParent would become hwndBaap. This combines both the hungarian and the local language strategies for job security.
Fortunately, he stopped programming and became very successful in agriculture. Good outcome for both fields.
Any time I read C/C++ code, I am under the impression it was written to be illegible.
I realize that with time, functions like `malloc` become second nature, but why not call it `allocate_memory`? (bash suffers from this too) It's even worse when reading the source code of old video games due to the abstract nature of the operations being performed on virtual representations of visual elements.
The argument of time constraints when authoring doesn't hold much value. Java is verbose AF but it's also one of the most used languages. Second, most code is read, not written and should be written to be read by others, not just the author.
There may be a reason to be terse due to time constraints and lack of IDEs/autocomplete or memory constraints or w/e, but the need to shorten variables names and functions today only serves the whims of the author, not the 10+ future readers. tinyrant :D
There was a time where visual and actual compactness was important. So for instance things like `malloc` date to that era. I'd argue that changing it to `allocate_memory` now would be even more confusing than leaving it as `malloc`
I fully agree. This reminds me of how some text books will teach people to write `a = 10` rather than `age = 10` for the sake of brevity. From that moment on, it becomes a habit to write code in a terse manner. It took me a few years and some good IDEs to no longer be afraid of names like `ageOfUser`/`age_of_user`.
I know when I was in my CS program in the early 90s it seemed like that was transitioning. The older profs would teach us every trick in the book to reduce the actual file size & visual noise in our code. The younger profs would beat in our head to not do that.
There were a lot of reasons for concise, pithy code in the 1970s. Poor keyboards was not one of them.
(in fact, some of the best keyboards I've ever used were of the old mechanical-switch variety found on IBMs, teletypes, and various CRT terminals. Even the worst of the bunch were a lot better than the almost-legendarily-bad macbook keyboard I'm using at this moment.)
> I was watching videos about my favorite topic, UNIX history, and found a talk by Hendrik Jan Thomassen. He's the UNIX pioneer in the Netherlands and has worked with Thompson and Ritchie in the past. He says the real reason why UNIX commands like ls, cp, mv, etc., are short is because PDP11 console keys were super hard to press. You'd get knuckle pain if you typed too much. So to save your fingers, all the commands were designed to be as short as possible. True or not, that's a fun story.
That's a great contribution. Thank you! And all this time I thought they were mere stylistic choices.
As an undergraduate, I was relegated to the teletypes or a modem on one of the 10 dialup lines -- upperclassmen and grad students got first pick of the CRTs and there was rarely a free one available. I suppose I get the last laugh, as I didn't developer carpal tunnel syndrome.
Coding assembly on a teleterminal using greenbar paper now seems equal parts primitive, badass, and insane.
> There was a time where visual and actual compactness was important.
It still is; there is a reason people tended to shorten terms in the field they are deeply engaged in. Verbosity gets in the way, and there is always a tension with it (and where the optimum point for clarity is depends on the focus and expertise of the audience.)
I agree that Java makes reading hard with regard to the semantics and unnecessary data structures for simple things like anonymous functions. I would point to Python, which is semantically the easiest language to learn. It's still possible to write illegible code (primarily due to the lack of indicator of argument and returns types and, of course, bizarre variable names).
I mean, it's the same thing. If you're used to reading it you know what parts are relevant to what you're doing and what parts aren't. The difference is that if you don't know the language at all and look at some code you can likely understand what it does.
For the standard library and such that train has left the station... way too much legacy code and learned knowledge.
As to why it happened in the first place: it is from an era when some compilers only looked at the first 8 characters for uniqueness of a name. Toss on top of that that a screen was only 80 characters wide and short names made a lot sense.
Now it is just tradition, I feel, more than anything else.
> Toss on top of that that a screen was only 80 characters wide
There are still developers that insist on code only ever being 80 characters wide, even in today's world of 24+ inch, 1920x1080+ monitors.
I understand wanting code to be narrow enough to be able to have diffs side-by-side, but you can get 120-character wide lines and still achieve that.
I've seen others argue that if a line is too long, it's harder to understand what it's doing, and I would agree with that with one caveat: In this context, "line length" should be understood as the number of operations being done, not necessarily absolute number of characters. A line that is 110 characters long but has meaningful variable and function names is going to be easier to grok than a line with only 60 characters but has terse names.
> There are still developers that insist on code only ever being 80 characters wide, even in today's world of 24+ inch, 1920x1080+ monitors.
Resolution is largely irrelevant. The size of the font and screen size/distance is what matters for readability.
Think in terms of books that are pleasent to read. If the lines are too long it’s easy for some to lose track of where they are. If the font is too small people with poor eyesight will have trouble reading anything. Character limit is an accessibility issue. I am personally limited to ~80 characters for my use case.
Though the issue is largely irrelevant for people who are know of therecent AST aware formatting tools that can limit lines to whatever length you’d like.
I'm one of those dinosaurs who prefer 80 character limit, thank you. I have a 24-inch monitor, and I like seeing three different files, a terminal output, and a stackoverflow page on Chrome, preferably at the same time.
Regarding variable names, it's a matter of taste, but for example:
I find the second line much more pleasant to read, especially if these variable names repeat several times in a function, which they usually do. The problem with long lines is that the style inevitably drifts toward the first example, because nobody can say no to "Can we make the names more descriptive?" With 80-character limit there's a certain friction that acts as a counterbalance.
I have an addition to writing unmaintainable code.
Only have one struct with a member of every type. That one struct will be the type of every variable you have.
If the language you use support implicit operator overload abuse them to make sure all types can be implicitly converted to each other.
Ex.
struct Variable
{
byte a;
short c;
int d;
long e;
...
}
This will make it impossible to know what type a variable is until you find its assignment, which of course isn't on the same line as the declaration and even better reuse the same variable for multiple values.
> The design doc should break the algorithm down into a hierarchy of structured steps, described in a hierarchy of auto-numbered individual paragraphs in the document.
> then... (and this is the kicker) when you write the code, for each of these paragraphs you write a corresponding global function named:
> Act1_2_4_6_3_13()
> Do not document these functions. After all, that's what the design document is for!
Ideally, your company will also have several file shares into which old and unidentifiable documents are filed, in nested layers of folders named things like "Old documents", "John's Code", "Please Clean Up", and "Maybe Delete". However, for maximum effectiveness do not file your design documents here! Instead, put various old drafts (not identified as such, of course) in a folder on the server where your code used to run, somewhere like C:\backups\aurdbcl1\C\Documents and Settings\users\jdoe\Desktop\ (where aurdbcl1 is the name of the server where your program ran 20 years ago). This will ensure that they will spend as long of a time as possible hunting for something that could be useful. If possible, also ensure that there are some multi-Gigabyte zip files scattered about which have to be unpacked just in case there's something useful inside.
A related technique for compiled languages is to arrange for various copies of the source code to be copied into similarly diverse locations. Do not use version control under any circumstances; if you must, check in at most once a month with the comment "changed code". This works best for a program which is maintained very rarely. Anyone attempting to fix it will first have to identify which copy of the source code is the right one. Make sure you also have some copies that never made it to production, where you started changing things and never finished. If possible, the production version should also be subtly different from all copies of the source code that can be found.
(The above is, unfortunately, from experience. I have many other rants which similarly belong in this document, particularly around exception handling and copy-pating, but I feel that this comment is becoming too long already.)
An approach from one of my early employment experiences (1980s):
1. Write a CAD system. User-facing code in BASIC. Rendering engine written in assembly.
2. Run the interpreted BASIC code through an optimizer, which condensed all variable and function names to be as short as possible (one or two characters) and moved frequently-used routines to the top of the module before compiling it all into one executable chunk (no more interpreter). This optimized code is almost unreadable but executes a lot faster (25%-50%) than the human-readable interpreted code. Hurray!
3. Lose the original source code.
Build machine's hard drive died. The backups weren't. No version control -- only occasional snapshots. Source code that was on various floppy disks lying around the office was incomplete and badly outdated (like the "Old Documents/John's Code example, above).
4. Team panics and spends a month trying to piece "Humpty Dumpty" (the software) back together again. No luck.
5. Spend the next year trying to reconstruct function and variable names from "AA423()" and "aa_237" to such gems as "customerListIThink", "doesSomethingToInventory()", "NoIdea()", "BreaksWhenIChangeIt()" while struggling to add new features at 1/10th the rate before the loss.
I actually like using short variable names. Using "i" as an index variable has been widely accepted. It saves a lot of typing, the use case is clear and it is usually tied the a block which is visually identifiable.
I carry over the same principles to functions/methods. Functions are usually less than a screen which makes it rather easy to pick up the context and understand the program flow and the use of variables. I use "list" if there is there is a single list. The context usually defines what is in there. Maybe the function name, the type declarations or the fact that we are in a part of code handling lists of customers. How hard is it to guess what countCustomers(list) or incrementCustomersScore(list, n) is doing? Writing incrementCustomersScore(goodCustomerList, customerScoreDelta) is just redundant. Variable names just need to be descriptive enough to define their role within it's context. If the function is very small, the variable names can be very short.
Local variables are the smallest named things in our systems, whith a very small scope, there shouldn't be an excessive naming cult around it. Give advice how to properly name projects, packages, namespaces and classes and I start listening. We are really bad at this.
I'd suggest that `count(customers)` or `incrementScore(goodCustomers, delta)` are shorter, more readable, don't embed type information in the variable name (e.g. what happens if you realise your customers are actually a set, not a list), and don't end up embedding type information in your function name (e.g. what if I want to count employees).
Also, by not including semantic information in the variable, it also requires this to be conveyed everywhere else e.g. your function names. You rightly point out that hopefully there's tight context around this thing (e.g. it's only about customers) and maybe you can leave that out, but over time as code changes and grows in weird ways, this may not always hold.
Embedding types in the method names is a habit of using dynamically typed languages. If the method is private it can be shorter, if it is public it should be a bit more descriptive. With statically typed languages I'd agree with your suggestion, but i'd likely would remove the type info from variables as well.
To me, if you feel compelled to start putting type information in the variable names, function names, etc., what you're showing is that you really want a type system.
`i` vs `index` is whatever. `index` is better, but it's not like I'd bash someone for using `i`.
Using `list` is just bad. Your list (probably) holds a set of things that has a name. So name your list that. At the very least, name it after the type of things it's holding. Types are only declared once, but variable names are forever. Having to constantly look up the type of a variable because the programmer was too lazy to name the list of things as `things` instead of `list` is rather inconsiderate.
(Of course, there are times when you would use `list`. For instance, if you're writing a list sorting function, and you actually have zero context more than "yep, this thing is a list".)
Index isn’t going to be consistent when you need a second iterator in a generic algorithm; people just don’t like the sound of jndex. Use “i” and “j” and “k”, or “idx” and “jdx” and “kdx” if you must, but please don’t use “index1” and “index2” and “index3.”
As you say, list might be the best name for a variable if the use context is generic. The same with iterators and such. But you really have to think ahead to keep the names consistent, complicated names work against consistency.
You have more context (in that first example) and so the foo and bar indexes make great sense.
i and j really make more sense (as i and j) when used in their more mathematical connotation: indexes into a multidimensional array/object.
for (i = 0; i < thing.length; i++) {
for (j = 0; j < thing[i].length; j++) {
thing[i][j]; // this is very close to the mathematical use of indexes
}
}
Makes perfect sense because i and j are being used to index into the same thing.
i and j would be poor fits for your first example because they don't index into the same thing anymore. You've introduced additional levels of indirection, to rewrite your first example using i and j (and illustrate why this is an awkward construct):
for (i = 0; i < foos.length; i++) {
for(j = 0; j < foos[i].bars.length; j++) {
foos[i].bars[j];
}
}
It's not wrong, but it is awkward and unclear as the code grows larger it becomes easier to misuse i, j, and subsequent index variables.
=====
And of course, you're absolutely correct: foreach or equivalent is a better construct. It states the intent of the code, rather than the how of the code. This keeps your code closer to the semantics of your problem specification.
Well, yes and no. I can see your point, but simplicity and recognisability sometimes are sometimes a trump card.
lst is actually a very good example. If you are writing small functions of less than a screen (which you arguably should be), and you can see that lst is a list of FileInfo, and the code to populate it in a function called GetAllWebConfigsUnderPath, and you have twenty of thirty other types of list generating functions, then seeing lst as the primary thing that gets filled everywhere is comforting and recognisable.
Having said that, having lstWebConfigs and lstAppleFiles and lstUnixFiles might be useful, for example.
Also, I assume you mean "type" as in "the kind of things that it is", rather than its actual type, such as integer, string, etc. lstInteger is largely pointless in this age of IDEs, but lstUserIdentifiers is demonstrably better (You shouldn't compare UserIdentifiers with StockCodeIdentifiers unless you work in H.R.)
I don't say you should never use "customers" over "list", but the latter is often good enough. List is a good fit for a compact method which is doing something generic on a list or the context clearly specifies what is in it. It is of course a matter of what you are doing. The point is, that being very specific every time is likely overkill.
You keep saying if it's obvious in context, but context is a name for "one more damn thing I have to keep track of." Which is why I call it inconsiderate; you are forcing other developers to constantly have to build and maintain their mental context of what is going on.
"Oh, great... Here's another `list` variable, let me go see what it is this time."
I disagree. There's practically zero cost to writing "customers" instead of "list". The only time it really makes sense for it to be "list" is when you're writing generic methods that just know it's a list.
> `index` is better, but it's not like I'd bash someone for using `i`.
Thre's strong grounds for saying that `i` is better.
At least in a lot of the code I work with, `index` is most likely to be assumed to mean something akin to a SQL index similar - a data structure or module used for fast lookup.
Whereas `i` is universally understood to be an iteration counter or a position in a sequence. That's a well-known convention that goes back for hundreds of years, and that many (if not most) of us learned before we really learned to program, by virtue of its use in math notation.
That is strong grounds for saying that `i` is likely to be well understood. Which is why I would not even bring it up in a code review. (Unless there is nested looping with multiple indexes, in which case I very much want better names to help prevent mistakes.)
And yes, words can have many definitions. This is where you actually do rely on context for the reader to separate. And I have zero issues with local conventions overriding global conventions. So if you really do have a lot of confusion between whether an integer means an array index or an SQL index, by all means create convention to disambiguate.
(Also, while "hundreds of years" is technically accurate, it's probably not as old as you think. Euler was mid 1700's with his sigma summation notation.)
> How hard is it to guess what countCustomers(list) or incrementCustomersScore(list, n) is doing?
When you're as jaded as I am, very.
What is list? What are its elements? Is it a list of customers? Is it a list of something that has a customer as one of its properties? I've seen far too much bullshit in my time to make any contextual assumptions about something called "list".
Same goes for "n". Is that the number of customers whose scores to increment, or is it the amount to increment for each customer in the list? Or something else? What happens if it's negative?
Any time I have to look inside a function (no matter how small) to see what it actually does, I find myself wondering why I must endure this extra cognitive load (and then endure again later when I read the same code but have forgotten the implementation details). And any time I find even mildly ambiguous names, I MUST assume the worst, because people just do stupid things, and therefore I must assume for my own survival that YOU have done stupid things, even if you haven't.
It's very little extra effort to call it modifyCustomersScores(customers, scoreDelta) and eliminate as much guesswork as possible.
Remember: Code is read FAR more often than it is written. Why wouldn't you take steps to optimize throughput on the common path?
Explicit naming is a courtesy. It's developer U/X. It's telling your reader "Yes, I've been there in the trenches. I've seen the horrors. Here's a flashlight to help you on your way."
thats not the point, the point is that you cant assume that you really know what the function is doing, too often its doing something else thats really hard to figure out without looking closely. Good code will make it so that you dont have to look closely, you can read the variable and method names.
I don't think static typing is a silver bullet here. As I alluded to in another comment a moment ago, I've seen atrocities that were committed in part because someone didn't want to have to go modify a bunch of type references, or refactor the code to accommodate a new type.
Local type inference ("var") can reduce the friction here, but, you've probably still got to explicitly declare types for parameters and return values, so there's always an excuse to be lazy.
I'd say that full-on Hindley-Milner type inference might help, except that I suspect that the folks who do this kind of thing would find a way to create even more chaos in the kinds of languages that have one.
When you're as jaded as I am, names like customerList don't indicate anything. That could be a list of customers, or a list from the customer, or a list that used to have customers and now has god knows what. But if you gave all your variables really long names, now everything either scrolls off the side of the screen or takes multiple lines and I can't see anything.
But even code rot can be mitigated somewhat with this discipline.
Which of these changes is easier to justify:
countCustomers(list)
incrementCustomersScore(list, n)
// where "list" is to change type from "list of customers" to "list of sales domains"
VS
countCustomers(customers)
modifyCustomersScores(customers, scoreDelta)
// where "customers" is to change type from "list of customers" to "list of sales domains"
Code is a map to what the computer is supposed to do. Making things familiar and easily navigated just makes sense. Good code requires good U/X, just like good websites require good U/X. It's just something you do to make your users have a better time using your stuff.
Short names are useful when the definition of the name is only a few lines away. If 'n' is defined and then used within 5 lines, you have the definition right there in front of you.
I would invest effort in making the definitions of local variables easy to read, while keeping the names short. (And keeping the functions themselves short so there aren't many locals in scope.)
There problem here is that now you need to scan beyond the function signature to see what the parameters are for. It also makes it impossible for your text editor or IDE to tell you anything about the function call you're hovering over besides 'n'.
The idea is to reduce cognitive load as much as possible for someone navigating your code. Every line the reader has to read increases cognitive load, and slows him down.
To me, this is sort of like saying you shouldn't use pronouns in English because they assume you've read the previous few sentences. It's possible, but it results in rather stilted prose.
There are situations where text needs to stand alone (such as tweets) but I think a function call is too narrow a focus. The body of a reasonably-sized function seems like a more natural chunk for understanding a program.
> How hard is it to guess what countCustomers(list) or incrementCustomersScore(list, n) is doing?
The correct comparison is:
1. what is the cumulative time wasted when people guess wrong?
2. what is the cumulative time spent writing out a meaningful name?
When I'm conservative in my estimate of how good people are at guessing my intuitions, I always find that #1 seems to dwarf #2.
Autocomplete means I type less than six keystrokes for most of my uses of a variable.
“Fewer characters” seems to be the domain of people who place little to no value on good tools. In the worst cases they make choices that are actively hostile to anyone else using better tools.
See also the Toxic 10x-er, who is really a 3x who makes everyone else 2/3rds less effective.
Short variable names like `i` are often the most appropriate. It's a thoroughly understood idiom and to use anything else just because you don't like single letters is just stupid. It's fine if you want to use a language that let's you easily avoid using indexes at all, though. But only if it's idiomatic in that language.
"the context usually defines what is in there" is part of the problem. You really should need to know the context to know what a variable is.
Sure "countCustomers(list)" gives you an idea of what list is (well sort of, is list a list of customers, or is it a list of people, some of whom are customers?) But "list.count" doesn't give you any info.
The other issue is it makes it difficult to do global searches.
And code reviews are more difficult as I may not be able to glean the context from the few lines that show up in the diff.
Variable names should be succinct, as long as they need to be and no longer.
It really depends on the team coding conventions. Our team uses 'c' for sql.Connection, 'rs' for sql.ResultSet, and 'txn' for our ORMs transactions, and we do this absolutely everywhere. The short variable names absolutely increase readability. But without the solid conventions it isn't as much of a win.
What incenses me is this recent notion that because programmers fail to change comment with the code change, they shouldn't even write comments. They should instead write the self-documenting code.
This doesn't make sense on so many levels. Only a recluse sitting in some dungeon who never dealt with people can come up with something so inane.
Edit:
I understand that that every developer should aspire to become someone who writes code that reads like a children's book, and they should. However, actively discouraging writing comments is so wrong on so many levels.
At the last place I worked, we used to have a small at-home coding test before interview and none of the young developers who successfully completed the test did any sort of code documentation or commenting. When I asked why, they would tell me that their code is self-documenting, which was utter nonsense.
Obviously code is never entirely self-documenting, but I think the idea is that, given the choice between something obscure with comments and something obvious on its face, you should prefer the latter unless there is some compelling rationale not to.
I understand that that every developer should aspire to become someone who writes code that reads like a children's book, and they should. However, actively discouraging writing comments is so wrong on so many levels.
At the last place I worked, we used to have a small at-home coding test before interview and none of the young developers who successfully completed the test did any sort of code documentation or commenting. When I asked why, they would tell me that their code is self-documenting, which was utter nonsense.
Well, this is a classic problem with people taking a guideline and applying an extreme version without understanding the rationale. I had a meeting one time where someone suggested that a code review went much easier when the submitter used remarks on the PR to explain what it did, and I suggested that if they were helpful during review they'd be helpful later, so just using comments would be better. He replied to me that that didn't make sense because comments are a code smell (a term I've kind of come to detest).
I found this technique to work quite well, but this idea does not mean imply that one should simply "quit writing comments", but rather that comments can most often be replaced with actual structures that improve the code (e.g., extracting long boolean expressions into a function with the same name as the actual comment), but that also depends on the language and aptitude of the developer.
Would you really argue in favor of something like this?
```
class Account { ...
/// returns the accountId
public int d_nr() { ... }
}
```
```
class Account { ...
/// returns the accountId
public int getAccountId () { ... }
}
```
The first comment is obsolete when using a proper method name, the second does that and so the comment is redundant.
All you have accomplished really is to move the descriptive name from the variable's name to the variable's type, and at what cost? You now have to create a whole new datatype. The programmer must look up that datatype and see oh, it's just an int. And now you need conversion routines or worse yet casting to convert that type to a simple int for interop reasons (database, UI).
Experience has shown that it is very productive to have a small number of generally useful datatypes which are augmented by custom datatypes.
Creating a new datatype for something as simple as an int or String defeats that and makes it more cumbersome to work with. (It's especially unwarranted for a variable that is an immutable variable serving as a simple id.)
You haven’t just moved the descriptive name from the variable name to the type.
If you are using a strongly typed language, you can just right click on the accountId type and find everywhere in your codebase where the “accountId” is being used.
The “accountId” is not “just an int”. An accountId has semantics that would be different than an int. An accountId is not the same as a “customerId” that may also be represented as an int. The accountId also has different uses than an int. You’re not going to take the sum, average, etc of an accountId.
An accountId that happens to have a value of 1 is semantically not equivalent of a customerId of 1. A method that expects a list of accountIds that are just ints will just as happily take a list of customerIds.
But a method that takes as a parameter List<AccountId> will cause a compile time error if you pass in a List<CustomerId>.
Why use a strongly typed language and then ruin one of the benefits of it if you don’t use domain specific types?
I agree with @scarface74 and would go even further in that I would probably create a custom type for List<AccountId> to convey the purpose of what that thing is now.
And as for the "type shyness":
> Creating a new datatype for something as simple as an int or String defeats that and makes it more cumbersome to work with.
It is never "just an int". Soon you will ask questions about the int, and in a sufficiently large application you will forget the answers to the quesions easily, and scatter the same question (i.e., the same boolean logic) all over the place. Consider an application where you need to filter something by year. A year, you know? As in 1974, 2018, etc. An int is bad, because an int could be -5, which does not make sense. Ok, a uint then? Still bad, because then there is a year 0 and - if your application deals with birthdays of real people, 1800 would be an invalid birthday. The semantics (or pragmatics, linguistically speaking) emerge from how you interpret the integer, and this interpretation can happily live in that one file with that one class that encapsulates "just" that integer.
The AccountId probably shouldn't be < 0, and a specific type could be the one place where you make sure that this is always the case.
What if you start with a 32bit integer and suddenly you realize that your weird Internet-Of-Things-Sensor reading database has grown and the 32bit integer is getting too small? You could just use 64bits. With "just an int", you now get to change every single function that expects an int for the purpose of a SensorReadingId (or an account id or whatever) and change it to size_t, uint, int64 or whatever. OR, you just tell the object that its internal representation uses 64bits now, because the AccountId class is the one single place that deals with account ids. The type provides a consistent interface that allows you to infer the semantics of its use. An int doesn't do that.
This idea predates java by some ~20 years [1]. An int is not an account id. An int is not a year. An email is not a string. The result of working like that is considered a code smell [2].
Still, coming back to the example I made: It was primarily about commenting. I was taught to always comment, and StyleCop enforced comments on each and every field of a class, leading to noisy, superfluous comments.
/// Gets or Sets the Name
string Name { get; set; }
does not add anything.
As for returning an AccountID: I'd like to point out that the Account is a strong and independent object, who can make his own decisions and does not need to return anything. Returning its id like that is actually breaking encapsulation. Still if you do it, it probably shouldn't be an integer.
Why are people scared of creating types and objects?
You deal with a lot of data that is of only a few simple types, and as I said in my post, interoperability means you can't get too crazy with your class definitions. They need to stay simple or the SYSTEM will become more complicated.
As for a variable never staying a simple type, again, see my example in my previous post. A string id is going to stay a string id and its data and behavior is dictated by being an id to remain supersimple.
I am not against defining new types; I am against defining a new type for every single variable "just because."
For interoperability, you would want to keep the models that are sent out over the wire separate from the models you use in your program. You want the freedom of being able to change your database properties and fields, your domain models which could be some type of crazy aggregation of separate database tables are even information from other sources, without changing the structure that you expose to external clients.
Your example of needing to define a type because "int" is not good enough is very C/C++ centric.
Most languages don't have the plethora of integer types that C has. Java, for example, has just has 2 in general use: int (signed 32 bit) and long (signed 64 bit). (Nobody considers byte a general integer type.)
In C/C++, you have to define so many things about the types you use because so much is "implementation dependent." So for C/C++, you may be correct. Most other languages define their types more stringently and include a smaller number of general purpose types.
> It is never "just an int". Soon you will ask questions about the int, and in a sufficiently large application you will forget the answers to the quesions easily, and scatter the same question (i.e., the same boolean logic) all over the place. Consider an application where you need to filter something by year. A year, you know? As in 1974, 2018, etc. An int is bad, because an int could be -5, which does not make sense. Ok, a uint then? Still bad, because then there is a year 0 and - if your application deals with birthdays of real people, 1800 would be an invalid birthday. The semantics (or pragmatics, linguistically speaking) emerge from how you interpret the integer, and this interpretation can happily live in that one file with that one class that encapsulates "just" that integer.
Are you honestly suggesting making a Year class to represent years in a C++-like language? And you want to forbid Years from being -5 or 1800, because they "do not make sense"?
What if I want to interpret the integer in a way you didn't think of when you wrote your Year class? Maybe I want to know whether my friend John was older when his sister Sally was born than Jesus was when St. Paul was born?
> What if you start with a 32bit integer and suddenly you realize that your weird Internet-Of-Things-Sensor reading database has grown and the 32bit integer is getting too small? You could just use 64bits. With "just an int", you now get to change every single function that expects an int for the purpose of a SensorReadingId (or an account id or whatever) and change it to size_t, uint, int64 or whatever.
The work needed to push the buttons on your keyboard to make the changes is the least of your worries when you want to change the underlying data types used in a complex program. If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle.
It may not be practical to enforce "ints that represent sensor reading ids are SensorReadingId type" across all of your code. You'll need to unpack your type to a primitive type whenever you talk to an external library or serialise SensorReadingId's to storage or the network. Even if you did enforce SensorReadingId everywhere:
- Arrays of SensorReadingId now double in size. Perhaps your code crashes at startup because some stack frame grew too large.
- If you serialise SensorReadingId's anywhere, changing the type broke binary compatibility with your old code. Or the network protocol you're supposed to be speaking. Or the library you're using.
- You probably had to make a toInt() or unpack() or similar function anyway so that people who want to get at information inside the value of the SensorReadingId can do so. You still have to look at all the call sites of these functions.
In reality, you're going to do this sort of change incrementally. You push the 64-bit integers through more and more code, taking care that the high bits of the integers make it through everywhere you've made the change.
> As for returning an AccountID: I'd like to point out that the Account is a strong and independent object, who can make his own decisions and does not need to return anything. Returning its id like that is actually breaking encapsulation. Still if you do it, it probably shouldn't be an integer.
Why is an Account making its own decisions? This sounds like the "circle that knows how to draw itself" antipattern. My friend and I want to talk about a certain Account; how do I arrange things so that the Account makes the decision to put an identifier for itself into a certain field in a certain protobuf message before I send the message to my friend?
Are you honestly suggesting making a Year class to represent years in a C++-like language? And you want to forbid Years from being -5 or 1800, because they "do not make sense"?
What if I want to interpret the integer in a way you didn't think of when you wrote your Year class? Maybe I want to know whether my friend John was older when his sister Sally was born than Jesus was when St. Paul was born?
That’s why languages don’t represent dates as just as struct with ints - there is a Date class that knows how to semantically add days to dates taking leap years into account, time zones, etc. That supports our point....
Then will your class take into account that the calendar changed in the mid 1600s(?). If you need to calculate dates and compare dates.
If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle.
How so? All of your other code should be treating it like an opaque type. If I was comparing two sensor ids for equality outside of the class and I changed the underlying type, I should modify the equality comparison operator to work with the new semantics.
It may not be practical to enforce "ints that represent sensor reading ids are SensorReadingId type" across all of your code. You'll need to unpack your type to a primitive type whenever you talk to an external library or serialise SensorReadingId's to storage or the network. Even if you did enforce SensorReadingId everywhere:
You shouldn’t have to do it in but one place. The serializer
would call your .ToString() method anyway. You just override it.
If you serialise SensorReadingId's anywhere, changing the type broke binary compatibility with your old code. Or the network protocol you're supposed to be speaking. Or the library you're using.
Versioning and deprecating APIs and interfaces is a solved problem. But you shouldn’t be using the same model for internal use as your external model anyway.
Arrays of SensorReadingId now double in size. Perhaps your code crashes at startup because some stack frame grew too large.
I thought we were specifically not talking about low level languages and you mentioned Java. When would that be an issue in Java?
Why is an Account making its own decisions? This sounds like the "circle that knows how to draw itself" antipattern. My friend and I want to talk about a certain Account; how do I arrange things so that the Account makes the decision to put an identifier for itself into a certain field in a certain protobuf message before I send the message to my friend?
I wouldn’t be sending my Account class that probably has business logic in it across the wire. I would be mapping it to a dumb AccountModel POCO/POJO with something like Automapper.
> That’s why languages don’t represent dates as just as struct with ints
In C, `struct tm` is literally a struct whose fields are all `int`s. But people typically represent time in C programs as an integer of some width measuring time in some fixed unit since some fixed point in time.
> - there is a Date class that knows how to semantically add days to dates taking leap years into account, time zones, etc. That supports our point....
>
> Then will your class take into account that the calendar changed in the mid 1600s(?). If you need to calculate dates and compare dates.
I can't anticipate those requirements---maybe our date class has to bomb out if you pass in, say, 1800 or -5 as a year because someone on my team insists they're invalid.
People work around these classes when they have to do date arithmetic because they're too slow, or they depend on the machine's locale and the locale isn't under control, or they take timezones into account in fundamentally broken ways, or any number of other reasons.
>> If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle.
>
> How so? All of your other code should be treating it like an opaque type.
That's a fantasy. When you wrote your reply, you deleted all the parts of my post explaining "how so"---why not all of your other code is going to treat it like an opaque type.
> You shouldn’t have to do it in but one place. The serializer would call your .ToString() method anyway. You just override it.
I don't want to convert your boxed int to a string and then parse it as an integer just to do math on it. That creates a ton of syntactic noise, not to mention being slow.
> Versioning and deprecating APIs and interfaces is a solved problem.
Yes, and the solution is a pain in the ass! So don't create new interfaces to version and deprecate for no good reason.
> I thought we were specifically not talking about low level languages and you mentioned Java. When would that be an issue in Java?
I didn't mention Java.
> I wouldn’t be sending my Account class that probably has business logic in it across the wire. I would be mapping it to a dumb AccountModel POCO/POJO with something like Automapper.
And I'd just send an integral id over the wire, probably documenting that it's the id of an account using something like the field's name in the message. No extra tooling needed, and I don't have to make a fuss about how the id() function (that I had to add to the Account object anyway) is for the big boys and should be avoided whenever possible.
People rapidly get themselves into deep shit when they find out their overcooked codebase is too slow to be useful. The class names remain but their structure and function and any semblance of the clean abstraction they used to provide are the first things to go.
In C, `struct tm` is literally a struct whose fields are all `int`s. But people typically represent time in C programs as an integer of some width measuring time in some fixed unit since some fixed point in time.
Even C has a bunch of functions to work with the date struct as an opaque type....
I can't anticipate those requirements---maybe our date class has to bomb out if you pass in, say, 1800 or -5 as a year because someone on my team insists they're invalid.
People work around these classes when they have to do date arithmetic because they're too slow, or they depend on the machine's locale and the locale isn't under control, or they take timezones into account in fundamentally broken ways, or any number of other reasons.
And then you use a better class. In Java you use something like JodaTime in .Net you use NodaTime - a .Net library written and maintained by Jon Skeet (the person with the highest ranking on Stack OverFlow). If that’s not good enough you write a custom extension or mixin to handle your special snowflake handling and still let all of the consumers treat it as an opaque type.
I don't want to convert your boxed int to a string and then parse it as an integer just to do math on it. That creates a ton of syntactic noise, not to mention being slow.
We were talking about an accountId that just happens to be represented as an int. You’re not going to be doing math on an account id. Also neither .Net with generics or even C++ templates have the boxing and unboxing issue.
Yes, and the solution is a pain in the ass! So don't create new interfaces to version and deprecate for no good reason.
If there was a business case to change the underlying structure - by definition there was a “good reason”.
And I'd just send an integral id over the wire, probably documenting that it's the id of an account using something like the field's name in the message. No extra tooling needed, and I don't have to make a fuss about how the id() function (that I had to add to the Account object anyway) is for the big boys and should be avoided whenever possible.
Even so, you should still not send your internal representation that may change across the wire and do some type of mapping. Are you going to send the same model that came from your ORM that’s tied directly to your database across as the representation to the outside world?
> Even C has a bunch of functions to work with the date struct as an opaque type....
Yes, for conversion to/from time_t, formatting, and parsing. That's my point; you use the integral type whenever you don't care whether it was a Thursday because the integral type isn't going to waste your time.
> And then you use a better class. In Java you use something like JodaTime in .Net you use NodaTime - a .Net library written and maintained by Jon Skeet (the person with the highest ranking on Stack OverFlow). If that’s not good enough you write a custom extension or mixin to handle your special snowflake handling and still let all of the consumers treat it as an opaque type.
Stop with the hero worship. You should do things that make sense in context. Recommending that everyone use a library in all contexts because it's written by Jon Skeet is a ridiculous thing to do. (BTW, it seems like the Joda-Time people recommend you use the standard java.time package instead.)
> We were talking about an accountId that just happens to be represented as an int. You’re not going to be doing math on an account id. Also neither .Net with generics or even C++ templates have the boxing and unboxing issue.
Who are you to say I won't do math on account ids?
> If there was a business case to change the underlying structure - by definition there was a “good reason”.
In the situations the two of us have discussed, there is no business case to create these interfaces in the first place. Where is the good reason?
> Even so, you should still not send your internal representation that may change across the wire and do some type of mapping. Are you going to send the same model that came from your ORM that’s tied directly to your database across as the representation to the outside world?
What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
Yes, for conversion to/from time_t, formatting, and parsing. That's my point; you use the integral type whenever you don't care whether it was a Thursday because the integral type isn't going to waste your time.
What else are you going to do in C? But even most real world implementations of C have all sorts of structs and typedefs that you shouldn’t make any assumptions about and you should treat as an opaque type.
Who are you to say I won't do math on account ids?
Is that really what you are going to argue? That in a real world use case you’re going to be doing math on an accountId?
What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
So you’re going to pass a raw key/value record set around your code and program like it was 2006? So now you’re going back to mapping your sql resultset to an object that makes sense anyway. Going right back to my point that you’re going to have a mapping between your raw results -> domain model -> view model (where the view model is serialized for external use) and back again anyway.
> What else are you going to do in C? But even most real world implementations of C have all sorts of structs and typedefs that you shouldn’t make any assumptions about and you should treat as an opaque type.
Subtract two times? (BTW, struct tm isn't an opaque type.)
> > Who are you to say I won't do math on account ids?
> Is that really what you are going to argue? That in a real world use case you’re going to be doing math on an accountId?
You seem to think that's absurd? Maybe I want to store a set of accountids. So I sort them (bit-extraction or comparison!), take deltas (subtraction!), and encode the deltas sensibly. Maybe I have two lists of accountids and I want to check that none of the things in the first list show up in the second list. Some sort of hashing scheme (bit-fiddling!) is going to be my friend here.
> > What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
> So you’re going to pass a raw key/value record set around your code and program like it was 2006? So now you’re going back to mapping your sql resultset to an object that makes sense anyway. Going right back to my point that you’re going to have a mapping between your raw results -> domain model -> view model (where the view model is serialized for external use) and back again anyway.
What SQL? What resultset? What object? I have an integer. Why do you assume it doesn't make sense?
You seem to think they hat's absurd? Maybe I want to store a set of accountids. So I sort them (bit-extraction or comparison!),
You’re going to do “bit extraction” to sort? If you treat accountId as an opaque type, you are going to have a comparison operator as part of the type and the rest of your code is going to just use a < or > symbol.
take deltas (subtraction!), and encode the deltas sensibly. Maybe I have two lists of accountids and I want to check that none of the things in the first list show up in the second list. Some sort of hashing scheme (bit-fiddling!) is going to be my friend here.
Or in a modern language since you have already defined equality between the two types and overridden the GetHashCode() function....
var deltas = accountList1.Except(accountlist2)
Why are you doing that all through your code instead of encapsulating the concept of equality, less than and greater than in one place?
What SQL? What resultset? What object? I have an integer. Why do you assume it doesn't make sense?
You were referring to CRUD apps and having to serialize the Account object. Either you are using an ORM, you are getting results from a database as a record set - which is usually represented by a dictionary of key value pairs and mapping it to your Account class or you are sending back a raw record set.
More than likely, you are mapping from your domain model to your externally exposed view model anyway.
The idea of a type / class / object here is equivalent in that they provide locality of interpretation. There is one place that may contain the code that manipulates these values; in other words, there is one place that contains your understanding - your interpretation - of the integer.
You (i.e., all commenters in this thread somehow disputing the AccountId object version of the story) are completely right, it is difficult to decide up front what you need. Of course, depending on your domain, it may be perfeclty valid to have -5 years or 1800, but I am not asking you to never implement years like this, I am merely suggesting that it is useful to have one place where such decisions, invariants, roles - your understanding of a year, perhaps specific to your domain - are located.
How your understanding is structuring is a different concern, because of course, a class with 10000 LOC is horrible to work with. But you can always use means of abstraction, and compose independent modules. An int does everything, maybe in one context a year shoudln't be 1800, maybe formatted as 4 digits. A year class / type / object is the place to store these decisions. As I outlined, such decisions are both technical (how many bits?) but may also be domain specific (year must be > 1800). Maybe you have an int internally, but abstract it with a non-zero int.
It boils down to interpretation. An int allows for many operations, but they may be invalid (in the sense of "implausible", or "undefined"). For example, an account ID of 1 can be multiplied by 2 and then by 2 again, and so on. Integers form a relation this way, but not all extensions of this relation might be "meaningful.
Integers as ids are a great example for this. Technically, they are great, because they are simple numbers, can be typed on a keyboard, and be readily interpreted, but not like integers.
6 people two times as many than 3 people. In Germany, grades are marked 1 - 6, (1 = very good, 6 = very poor). But the relationship is merely ordinal. A grade of 4 is not "twice as bad" as a 2, although the numbers are relatable that way. An Account ID of 6 is not twice as "good" as an ID of 3. For ids, you want them to identify, in that they are exclusive and exhaustive - but integer IDs are not supposed to be ordinal (in that their sizes are comparable in relation to each other, 6 > 5 is an invalid statement). Of course, you can retain this interpretation, e.g. an auto increment id would indicate that id 6451 was created WAY later than ID 6, but this is difficult to interpret; because it doesn't really tell you how much later, and also deletion of integers in between a range may be reissued (id 1, 2, 3, 4, delete 3, 3 is a missing rank, 3 could be reissued). So ids aren't ints, because they are exhaustive, but they aren't ordinal, or at least interpreting them ordinally is dangerous, and some operations aren't allowed even if you interpret them to be ordnial; for example, it would not make sense to calculate an arithmetic mean from integer ids, although mathematically this operation is allowed. In statistics, this concern is discussed as the Skalenniveau (German, meaning Level Of Scale), in English it's called level of measurement [1].
In sum, it does not matter what your usecase is, an int is an int, but depending on your use case, it might be worthwhile not to pass an uninterpreted integer around, but actually wrap it in some kind of object, where you localize all your decisions how to interpret the integer.
None of should be derogated as some form modern hipster javascript, where none of us highlevel kids don't know how to bit-bang a set difference from some account ids; but rather, these ideas are really old. Even in C, data abstraction is useful, in that you don't fiddle with integers but define a set of methods, possibly in a module that interact with a hidden internal representation. Deciding on where to cut these modules appears to be a difficult...
The programmer must look up that datatype and see oh, it's just an int. And now you need conversion routines or worse yet casting to convert that type to a simple int for interop reasons (database, UI).
Why should the developer care what the underlying type is?
It should be an opaque type. When you serialize it, your serializer should call your ToString() method. When you deserialize it, your deserializer would call the constructor that takes an int.
But why are you saving an id to a database - which would never be used as int - as an int?
But even in most CRUD apps, your domain model with rich types would be different than your view model which would probably also be different than your DB model.
You are going to be mapping back and forth regardless - hopefully using a tool like Automapper.
A much novel way - use functional programming and create higher order functions and purely mathematical combinatoric functions and compose them in a complex way. A normal day developer must go to college again to even getting closer to understand it. This way the code is highly functional and there won't be a compromise in the quality of the output!
Thats what i actually use with C macros, except its intentional and saves lots of typing: however you must know some macro trickery to understand how it even works.
As a creative process, and in the interest of compassion, I tend to name everything as humanly readable as possible, and provide voluminous documentation in production systems. Even quick-and-dirty things tend to get moved to production if they work, so I increasingly apply the same approach to them, as the "this will be tossed after x" sentiment has bitten me on the ass more than once.
Could you elaborate what criteria you apply to decide whether something is "as humanly readable as possible"? I am asking out of genuine interest, I am researching readability and I am interested in how you make decisions and what criteria you apply. What means "readable" to you?
169 comments
[ 2.2 ms ] story [ 219 ms ] thread> It took a team of maintenance engineers nearly 3 days to figure out that this whopper variable name described a const, reference, function argument that was holding information from a database column of type Varchar[30] named "LastName" which was part of the table's primary key.
- const, reference and function argument should be pretty obvious from the code context
- LastNameCol seems like self-describing... of course you would have to know that you're debugging an application that talks to databases, but eh
I'd say it's somewhat akin to proving a negative. Sure you get that it's a const, reference and function argument which likely refers to the Last Name Column. However is that all it is? Or is there more meaning in the name? Meaning that could be crucial to understanding how the application functions? Does one of the characters mean function reference or does it refer to something else? Apparently not in this case but how can you know without investigating?
It’s like when you find a nil-guard in a function. You can’t truly refactor it until you know whether it was added defensively just-in-case or whether nils can still exist in the system at the function’s callsite, calling for further upstream examination.
There’s a certain hell in trying to follow a code base where every function is infected with ‘if (!arg) return’.
At the very least it should be presented in a more unmaintainable way.
Edit, apparently repo owner already checked with author: https://github.com/Droogans/unmaintainable-code/issues/3 :+1:
https://quoteinvestigator.com/2016/12/30/not-malice/
I can offer as corollary to the article a couple of unmaintainable simple batches:
http://reboot.pro/topic/2986-yacbfc-dec2hexcmd-and-hex2deccm...
This one is good and there's no attribution problem.
I've noticed this especially true for JavaScript and Go.
In JavaScript , because the language is still evolving and there is no real standard about how to architect code with this language unless you are using a entire "platform" like Angular / Vue / React witch will highly influence your coding style.
In Go because Go developers are performances obsessed , coming from C/C++ background and do believe that naming their variable with one letter will save 1GB of memory allocation , 1000 CPU Cycles and make their code "Clear and Concise"
Ultimately , because there is very little standard in this industry in terms of code governance and a large percentage of projects are outsourced to other companies , we end up with what is described in this repository.
The problem is that you get too close to the code you write, and make assumptions about its obviousness that simply aren't true. Far better to be explicit at the cost of a few extra keystrokes.
Go is also not performance obsessed - else it would not have opted for garbage collection by default.
As far as variable naming goes, Go convention says - the further from its declaration a variable is used, the more explicit and qualified the name should be to disambiguate it. This to me strikes as being eminently sensible.
What is meant by that statement? Is "idempotent" a mistype/mistakenly selected word? If the author meant "pure" instead, should I file a PR, or consider this some sort of meta-meta-obfuscation? If the author meant "pure", is that actually true of C specifically, or just generally good programming advice (localize side effects to known locations, the fewer the better, don't sprinkle them all over and document where they occur)?
f(f(x))=x
assuming f is a function (binary operators are just functions (maps) in mathematics, too, e.g. +:NxN->N (read: "plus" is a map from the Cartesian product of N by N to N))
Fortunately, he stopped programming and became very successful in agriculture. Good outcome for both fields.
Well that is what agriculture is all about.
I realize that with time, functions like `malloc` become second nature, but why not call it `allocate_memory`? (bash suffers from this too) It's even worse when reading the source code of old video games due to the abstract nature of the operations being performed on virtual representations of visual elements.
The argument of time constraints when authoring doesn't hold much value. Java is verbose AF but it's also one of the most used languages. Second, most code is read, not written and should be written to be read by others, not just the author.
There may be a reason to be terse due to time constraints and lack of IDEs/autocomplete or memory constraints or w/e, but the need to shorten variables names and functions today only serves the whims of the author, not the 10+ future readers. tinyrant :D
I know when I was in my CS program in the early 90s it seemed like that was transitioning. The older profs would teach us every trick in the book to reduce the actual file size & visual noise in our code. The younger profs would beat in our head to not do that.
Map<K,V> drives me nuts. Especially since K has to be comparable and/or hashable.
I'd be going with `userAge` or `user_age` either way. :-P
(in fact, some of the best keyboards I've ever used were of the old mechanical-switch variety found on IBMs, teletypes, and various CRT terminals. Even the worst of the bunch were a lot better than the almost-legendarily-bad macbook keyboard I'm using at this moment.)
> I was watching videos about my favorite topic, UNIX history, and found a talk by Hendrik Jan Thomassen. He's the UNIX pioneer in the Netherlands and has worked with Thompson and Ritchie in the past. He says the real reason why UNIX commands like ls, cp, mv, etc., are short is because PDP11 console keys were super hard to press. You'd get knuckle pain if you typed too much. So to save your fingers, all the commands were designed to be as short as possible. True or not, that's a fun story.
The page includes a video.
As an undergraduate, I was relegated to the teletypes or a modem on one of the 10 dialup lines -- upperclassmen and grad students got first pick of the CRTs and there was rarely a free one available. I suppose I get the last laugh, as I didn't developer carpal tunnel syndrome.
Coding assembly on a teleterminal using greenbar paper now seems equal parts primitive, badass, and insane.
It still is; there is a reason people tended to shorten terms in the field they are deeply engaged in. Verbosity gets in the way, and there is always a tension with it (and where the optimum point for clarity is depends on the focus and expertise of the audience.)
As to why it happened in the first place: it is from an era when some compilers only looked at the first 8 characters for uniqueness of a name. Toss on top of that that a screen was only 80 characters wide and short names made a lot sense.
Now it is just tradition, I feel, more than anything else.
There are still developers that insist on code only ever being 80 characters wide, even in today's world of 24+ inch, 1920x1080+ monitors.
I understand wanting code to be narrow enough to be able to have diffs side-by-side, but you can get 120-character wide lines and still achieve that.
I've seen others argue that if a line is too long, it's harder to understand what it's doing, and I would agree with that with one caveat: In this context, "line length" should be understood as the number of operations being done, not necessarily absolute number of characters. A line that is 110 characters long but has meaningful variable and function names is going to be easier to grok than a line with only 60 characters but has terse names.
Resolution is largely irrelevant. The size of the font and screen size/distance is what matters for readability.
Think in terms of books that are pleasent to read. If the lines are too long it’s easy for some to lose track of where they are. If the font is too small people with poor eyesight will have trouble reading anything. Character limit is an accessibility issue. I am personally limited to ~80 characters for my use case.
Though the issue is largely irrelevant for people who are know of therecent AST aware formatting tools that can limit lines to whatever length you’d like.
Regarding variable names, it's a matter of taste, but for example:
I find the second line much more pleasant to read, especially if these variable names repeat several times in a function, which they usually do. The problem with long lines is that the style inevitably drifts toward the first example, because nobody can say no to "Can we make the names more descriptive?" With 80-character limit there's a certain friction that acts as a counterbalance.Only have one struct with a member of every type. That one struct will be the type of every variable you have.
If the language you use support implicit operator overload abuse them to make sure all types can be implicitly converted to each other.
Ex.
struct Variable { byte a; short c; int d; long e;
}This will make it impossible to know what type a variable is until you find its assignment, which of course isn't on the same line as the declaration and even better reuse the same variable for multiple values.
typedef union { int i; float f; double d; long l; char c; void *p; ... } var;
Now you might also get some nice float-interpreted-as-int bugs if you forget the type!
You can also kind of pretend that you have C#-style type inference:
var x = { .i = 10 }; var y = { .f = 2.0f };
> then... (and this is the kicker) when you write the code, for each of these paragraphs you write a corresponding global function named:
> Act1_2_4_6_3_13()
> Do not document these functions. After all, that's what the design document is for!
Ideally, your company will also have several file shares into which old and unidentifiable documents are filed, in nested layers of folders named things like "Old documents", "John's Code", "Please Clean Up", and "Maybe Delete". However, for maximum effectiveness do not file your design documents here! Instead, put various old drafts (not identified as such, of course) in a folder on the server where your code used to run, somewhere like C:\backups\aurdbcl1\C\Documents and Settings\users\jdoe\Desktop\ (where aurdbcl1 is the name of the server where your program ran 20 years ago). This will ensure that they will spend as long of a time as possible hunting for something that could be useful. If possible, also ensure that there are some multi-Gigabyte zip files scattered about which have to be unpacked just in case there's something useful inside.
A related technique for compiled languages is to arrange for various copies of the source code to be copied into similarly diverse locations. Do not use version control under any circumstances; if you must, check in at most once a month with the comment "changed code". This works best for a program which is maintained very rarely. Anyone attempting to fix it will first have to identify which copy of the source code is the right one. Make sure you also have some copies that never made it to production, where you started changing things and never finished. If possible, the production version should also be subtly different from all copies of the source code that can be found.
(The above is, unfortunately, from experience. I have many other rants which similarly belong in this document, particularly around exception handling and copy-pating, but I feel that this comment is becoming too long already.)
1. Write a CAD system. User-facing code in BASIC. Rendering engine written in assembly.
2. Run the interpreted BASIC code through an optimizer, which condensed all variable and function names to be as short as possible (one or two characters) and moved frequently-used routines to the top of the module before compiling it all into one executable chunk (no more interpreter). This optimized code is almost unreadable but executes a lot faster (25%-50%) than the human-readable interpreted code. Hurray!
3. Lose the original source code.
Build machine's hard drive died. The backups weren't. No version control -- only occasional snapshots. Source code that was on various floppy disks lying around the office was incomplete and badly outdated (like the "Old Documents/John's Code example, above).
4. Team panics and spends a month trying to piece "Humpty Dumpty" (the software) back together again. No luck.
5. Spend the next year trying to reconstruct function and variable names from "AA423()" and "aa_237" to such gems as "customerListIThink", "doesSomethingToInventory()", "NoIdea()", "BreaksWhenIChangeIt()" while struggling to add new features at 1/10th the rate before the loss.
6. Try to rewrite the whole thing in C.
7. Go out of business.
True story.
I carry over the same principles to functions/methods. Functions are usually less than a screen which makes it rather easy to pick up the context and understand the program flow and the use of variables. I use "list" if there is there is a single list. The context usually defines what is in there. Maybe the function name, the type declarations or the fact that we are in a part of code handling lists of customers. How hard is it to guess what countCustomers(list) or incrementCustomersScore(list, n) is doing? Writing incrementCustomersScore(goodCustomerList, customerScoreDelta) is just redundant. Variable names just need to be descriptive enough to define their role within it's context. If the function is very small, the variable names can be very short.
Local variables are the smallest named things in our systems, whith a very small scope, there shouldn't be an excessive naming cult around it. Give advice how to properly name projects, packages, namespaces and classes and I start listening. We are really bad at this.
Also, by not including semantic information in the variable, it also requires this to be conveyed everywhere else e.g. your function names. You rightly point out that hopefully there's tight context around this thing (e.g. it's only about customers) and maybe you can leave that out, but over time as code changes and grows in weird ways, this may not always hold.
What do you think?
Won't other people still need to read private code?
It does help one turn a bad API into a bad API that will lead to slightly less bugs.
Using `list` is just bad. Your list (probably) holds a set of things that has a name. So name your list that. At the very least, name it after the type of things it's holding. Types are only declared once, but variable names are forever. Having to constantly look up the type of a variable because the programmer was too lazy to name the list of things as `things` instead of `list` is rather inconsiderate.
(Of course, there are times when you would use `list`. For instance, if you're writing a list sorting function, and you actually have zero context more than "yep, this thing is a list".)
As you say, list might be the best name for a variable if the use context is generic. The same with iterators and such. But you really have to think ahead to keep the names consistent, complicated names work against consistency.
That, of course, is assuming you even need the indexes. Otherwise you get this:
i and j really make more sense (as i and j) when used in their more mathematical connotation: indexes into a multidimensional array/object.
Makes perfect sense because i and j are being used to index into the same thing.i and j would be poor fits for your first example because they don't index into the same thing anymore. You've introduced additional levels of indirection, to rewrite your first example using i and j (and illustrate why this is an awkward construct):
It's not wrong, but it is awkward and unclear as the code grows larger it becomes easier to misuse i, j, and subsequent index variables.=====
And of course, you're absolutely correct: foreach or equivalent is a better construct. It states the intent of the code, rather than the how of the code. This keeps your code closer to the semantics of your problem specification.
lst is actually a very good example. If you are writing small functions of less than a screen (which you arguably should be), and you can see that lst is a list of FileInfo, and the code to populate it in a function called GetAllWebConfigsUnderPath, and you have twenty of thirty other types of list generating functions, then seeing lst as the primary thing that gets filled everywhere is comforting and recognisable.
Having said that, having lstWebConfigs and lstAppleFiles and lstUnixFiles might be useful, for example.
Also, I assume you mean "type" as in "the kind of things that it is", rather than its actual type, such as integer, string, etc. lstInteger is largely pointless in this age of IDEs, but lstUserIdentifiers is demonstrably better (You shouldn't compare UserIdentifiers with StockCodeIdentifiers unless you work in H.R.)
"Your list (probably) holds a set of things that has a name. So name your list that."
"Oh, great... Here's another `list` variable, let me go see what it is this time."
Thre's strong grounds for saying that `i` is better.
At least in a lot of the code I work with, `index` is most likely to be assumed to mean something akin to a SQL index similar - a data structure or module used for fast lookup.
Whereas `i` is universally understood to be an iteration counter or a position in a sequence. That's a well-known convention that goes back for hundreds of years, and that many (if not most) of us learned before we really learned to program, by virtue of its use in math notation.
And yes, words can have many definitions. This is where you actually do rely on context for the reader to separate. And I have zero issues with local conventions overriding global conventions. So if you really do have a lot of confusion between whether an integer means an array index or an SQL index, by all means create convention to disambiguate.
(Also, while "hundreds of years" is technically accurate, it's probably not as old as you think. Euler was mid 1700's with his sigma summation notation.)
like iFoo, or listFoo, etc... When using these, I often dont know whats an Object that cannot be changed, and whats a variable.
Sure you can play around with it, but I feel like I've lost plenty of time trying to understand other's code.
When you're as jaded as I am, very.
What is list? What are its elements? Is it a list of customers? Is it a list of something that has a customer as one of its properties? I've seen far too much bullshit in my time to make any contextual assumptions about something called "list".
Same goes for "n". Is that the number of customers whose scores to increment, or is it the amount to increment for each customer in the list? Or something else? What happens if it's negative?
Any time I have to look inside a function (no matter how small) to see what it actually does, I find myself wondering why I must endure this extra cognitive load (and then endure again later when I read the same code but have forgotten the implementation details). And any time I find even mildly ambiguous names, I MUST assume the worst, because people just do stupid things, and therefore I must assume for my own survival that YOU have done stupid things, even if you haven't.
It's very little extra effort to call it modifyCustomersScores(customers, scoreDelta) and eliminate as much guesswork as possible.
Remember: Code is read FAR more often than it is written. Why wouldn't you take steps to optimize throughput on the common path?
Explicit naming is a courtesy. It's developer U/X. It's telling your reader "Yes, I've been there in the trenches. I've seen the horrors. Here's a flashlight to help you on your way."
Local type inference ("var") can reduce the friction here, but, you've probably still got to explicitly declare types for parameters and return values, so there's always an excuse to be lazy.
I'd say that full-on Hindley-Milner type inference might help, except that I suspect that the folks who do this kind of thing would find a way to create even more chaos in the kinds of languages that have one.
But even code rot can be mitigated somewhat with this discipline.
Which of these changes is easier to justify:
VS Code is a map to what the computer is supposed to do. Making things familiar and easily navigated just makes sense. Good code requires good U/X, just like good websites require good U/X. It's just something you do to make your users have a better time using your stuff.I would invest effort in making the definitions of local variables easy to read, while keeping the names short. (And keeping the functions themselves short so there aren't many locals in scope.)
The idea is to reduce cognitive load as much as possible for someone navigating your code. Every line the reader has to read increases cognitive load, and slows him down.
There are situations where text needs to stand alone (such as tweets) but I think a function call is too narrow a focus. The body of a reasonably-sized function seems like a more natural chunk for understanding a program.
The correct comparison is: 1. what is the cumulative time wasted when people guess wrong? 2. what is the cumulative time spent writing out a meaningful name?
When I'm conservative in my estimate of how good people are at guessing my intuitions, I always find that #1 seems to dwarf #2.
Are you getting paid by least amount of characters typed?
“Fewer characters” seems to be the domain of people who place little to no value on good tools. In the worst cases they make choices that are actively hostile to anyone else using better tools.
See also the Toxic 10x-er, who is really a 3x who makes everyone else 2/3rds less effective.
This doesn't make sense on so many levels. Only a recluse sitting in some dungeon who never dealt with people can come up with something so inane.
Edit:
I understand that that every developer should aspire to become someone who writes code that reads like a children's book, and they should. However, actively discouraging writing comments is so wrong on so many levels.
At the last place I worked, we used to have a small at-home coding test before interview and none of the young developers who successfully completed the test did any sort of code documentation or commenting. When I asked why, they would tell me that their code is self-documenting, which was utter nonsense.
At the last place I worked, we used to have a small at-home coding test before interview and none of the young developers who successfully completed the test did any sort of code documentation or commenting. When I asked why, they would tell me that their code is self-documenting, which was utter nonsense.
Would you really argue in favor of something like this?
``` class Account { ... /// returns the accountId public int d_nr() { ... } } ```
``` class Account { ... /// returns the accountId public int getAccountId () { ... } } ```
The first comment is obsolete when using a proper method name, the second does that and so the comment is redundant.
May I ask: What did you experience in the wild?
All you have accomplished really is to move the descriptive name from the variable's name to the variable's type, and at what cost? You now have to create a whole new datatype. The programmer must look up that datatype and see oh, it's just an int. And now you need conversion routines or worse yet casting to convert that type to a simple int for interop reasons (database, UI).
Experience has shown that it is very productive to have a small number of generally useful datatypes which are augmented by custom datatypes.
Creating a new datatype for something as simple as an int or String defeats that and makes it more cumbersome to work with. (It's especially unwarranted for a variable that is an immutable variable serving as a simple id.)
If you are using a strongly typed language, you can just right click on the accountId type and find everywhere in your codebase where the “accountId” is being used.
The “accountId” is not “just an int”. An accountId has semantics that would be different than an int. An accountId is not the same as a “customerId” that may also be represented as an int. The accountId also has different uses than an int. You’re not going to take the sum, average, etc of an accountId.
An accountId that happens to have a value of 1 is semantically not equivalent of a customerId of 1. A method that expects a list of accountIds that are just ints will just as happily take a list of customerIds.
But a method that takes as a parameter List<AccountId> will cause a compile time error if you pass in a List<CustomerId>.
Why use a strongly typed language and then ruin one of the benefits of it if you don’t use domain specific types?
And as for the "type shyness":
> Creating a new datatype for something as simple as an int or String defeats that and makes it more cumbersome to work with.
It is never "just an int". Soon you will ask questions about the int, and in a sufficiently large application you will forget the answers to the quesions easily, and scatter the same question (i.e., the same boolean logic) all over the place. Consider an application where you need to filter something by year. A year, you know? As in 1974, 2018, etc. An int is bad, because an int could be -5, which does not make sense. Ok, a uint then? Still bad, because then there is a year 0 and - if your application deals with birthdays of real people, 1800 would be an invalid birthday. The semantics (or pragmatics, linguistically speaking) emerge from how you interpret the integer, and this interpretation can happily live in that one file with that one class that encapsulates "just" that integer.
The AccountId probably shouldn't be < 0, and a specific type could be the one place where you make sure that this is always the case.
What if you start with a 32bit integer and suddenly you realize that your weird Internet-Of-Things-Sensor reading database has grown and the 32bit integer is getting too small? You could just use 64bits. With "just an int", you now get to change every single function that expects an int for the purpose of a SensorReadingId (or an account id or whatever) and change it to size_t, uint, int64 or whatever. OR, you just tell the object that its internal representation uses 64bits now, because the AccountId class is the one single place that deals with account ids. The type provides a consistent interface that allows you to infer the semantics of its use. An int doesn't do that.
This idea predates java by some ~20 years [1]. An int is not an account id. An int is not a year. An email is not a string. The result of working like that is considered a code smell [2].
Still, coming back to the example I made: It was primarily about commenting. I was taught to always comment, and StyleCop enforced comments on each and every field of a class, leading to noisy, superfluous comments.
does not add anything.As for returning an AccountID: I'd like to point out that the Account is a strong and independent object, who can make his own decisions and does not need to return anything. Returning its id like that is actually breaking encapsulation. Still if you do it, it probably shouldn't be an integer.
Why are people scared of creating types and objects?
[1] https://link.springer.com/chapter/10.1007%2F978-1-4612-6315-...
[2] https://sourcemaking.com/refactoring/smells/primitive-obsess...
You deal with a lot of data that is of only a few simple types, and as I said in my post, interoperability means you can't get too crazy with your class definitions. They need to stay simple or the SYSTEM will become more complicated.
As for a variable never staying a simple type, again, see my example in my previous post. A string id is going to stay a string id and its data and behavior is dictated by being an id to remain supersimple.
I am not against defining new types; I am against defining a new type for every single variable "just because."
Most languages don't have the plethora of integer types that C has. Java, for example, has just has 2 in general use: int (signed 32 bit) and long (signed 64 bit). (Nobody considers byte a general integer type.)
In C/C++, you have to define so many things about the types you use because so much is "implementation dependent." So for C/C++, you may be correct. Most other languages define their types more stringently and include a smaller number of general purpose types.
https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...
But even then, a year is an int, but it would have certain semantics you would want to enforce outside of integers.
Are you honestly suggesting making a Year class to represent years in a C++-like language? And you want to forbid Years from being -5 or 1800, because they "do not make sense"?
What if I want to interpret the integer in a way you didn't think of when you wrote your Year class? Maybe I want to know whether my friend John was older when his sister Sally was born than Jesus was when St. Paul was born?
> What if you start with a 32bit integer and suddenly you realize that your weird Internet-Of-Things-Sensor reading database has grown and the 32bit integer is getting too small? You could just use 64bits. With "just an int", you now get to change every single function that expects an int for the purpose of a SensorReadingId (or an account id or whatever) and change it to size_t, uint, int64 or whatever.
The work needed to push the buttons on your keyboard to make the changes is the least of your worries when you want to change the underlying data types used in a complex program. If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle.
It may not be practical to enforce "ints that represent sensor reading ids are SensorReadingId type" across all of your code. You'll need to unpack your type to a primitive type whenever you talk to an external library or serialise SensorReadingId's to storage or the network. Even if you did enforce SensorReadingId everywhere:
- Arrays of SensorReadingId now double in size. Perhaps your code crashes at startup because some stack frame grew too large.
- If you serialise SensorReadingId's anywhere, changing the type broke binary compatibility with your old code. Or the network protocol you're supposed to be speaking. Or the library you're using.
- You probably had to make a toInt() or unpack() or similar function anyway so that people who want to get at information inside the value of the SensorReadingId can do so. You still have to look at all the call sites of these functions.
In reality, you're going to do this sort of change incrementally. You push the 64-bit integers through more and more code, taking care that the high bits of the integers make it through everywhere you've made the change.
> As for returning an AccountID: I'd like to point out that the Account is a strong and independent object, who can make his own decisions and does not need to return anything. Returning its id like that is actually breaking encapsulation. Still if you do it, it probably shouldn't be an integer.
Why is an Account making its own decisions? This sounds like the "circle that knows how to draw itself" antipattern. My friend and I want to talk about a certain Account; how do I arrange things so that the Account makes the decision to put an identifier for itself into a certain field in a certain protobuf message before I send the message to my friend?
That’s why languages don’t represent dates as just as struct with ints - there is a Date class that knows how to semantically add days to dates taking leap years into account, time zones, etc. That supports our point....
Then will your class take into account that the calendar changed in the mid 1600s(?). If you need to calculate dates and compare dates.
If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle.
How so? All of your other code should be treating it like an opaque type. If I was comparing two sensor ids for equality outside of the class and I changed the underlying type, I should modify the equality comparison operator to work with the new semantics.
It may not be practical to enforce "ints that represent sensor reading ids are SensorReadingId type" across all of your code. You'll need to unpack your type to a primitive type whenever you talk to an external library or serialise SensorReadingId's to storage or the network. Even if you did enforce SensorReadingId everywhere:
You shouldn’t have to do it in but one place. The serializer would call your .ToString() method anyway. You just override it.
If you serialise SensorReadingId's anywhere, changing the type broke binary compatibility with your old code. Or the network protocol you're supposed to be speaking. Or the library you're using.
Versioning and deprecating APIs and interfaces is a solved problem. But you shouldn’t be using the same model for internal use as your external model anyway.
Arrays of SensorReadingId now double in size. Perhaps your code crashes at startup because some stack frame grew too large.
I thought we were specifically not talking about low level languages and you mentioned Java. When would that be an issue in Java?
Why is an Account making its own decisions? This sounds like the "circle that knows how to draw itself" antipattern. My friend and I want to talk about a certain Account; how do I arrange things so that the Account makes the decision to put an identifier for itself into a certain field in a certain protobuf message before I send the message to my friend?
I wouldn’t be sending my Account class that probably has business logic in it across the wire. I would be mapping it to a dumb AccountModel POCO/POJO with something like Automapper.
In C, `struct tm` is literally a struct whose fields are all `int`s. But people typically represent time in C programs as an integer of some width measuring time in some fixed unit since some fixed point in time.
> - there is a Date class that knows how to semantically add days to dates taking leap years into account, time zones, etc. That supports our point.... > > Then will your class take into account that the calendar changed in the mid 1600s(?). If you need to calculate dates and compare dates.
I can't anticipate those requirements---maybe our date class has to bomb out if you pass in, say, 1800 or -5 as a year because someone on my team insists they're invalid.
People work around these classes when they have to do date arithmetic because they're too slow, or they depend on the machine's locale and the locale isn't under control, or they take timezones into account in fundamentally broken ways, or any number of other reasons.
>> If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle. > > How so? All of your other code should be treating it like an opaque type.
That's a fantasy. When you wrote your reply, you deleted all the parts of my post explaining "how so"---why not all of your other code is going to treat it like an opaque type.
> You shouldn’t have to do it in but one place. The serializer would call your .ToString() method anyway. You just override it.
I don't want to convert your boxed int to a string and then parse it as an integer just to do math on it. That creates a ton of syntactic noise, not to mention being slow.
> Versioning and deprecating APIs and interfaces is a solved problem.
Yes, and the solution is a pain in the ass! So don't create new interfaces to version and deprecate for no good reason.
> I thought we were specifically not talking about low level languages and you mentioned Java. When would that be an issue in Java?
I didn't mention Java.
> I wouldn’t be sending my Account class that probably has business logic in it across the wire. I would be mapping it to a dumb AccountModel POCO/POJO with something like Automapper.
And I'd just send an integral id over the wire, probably documenting that it's the id of an account using something like the field's name in the message. No extra tooling needed, and I don't have to make a fuss about how the id() function (that I had to add to the Account object anyway) is for the big boys and should be avoided whenever possible.
People rapidly get themselves into deep shit when they find out their overcooked codebase is too slow to be useful. The class names remain but their structure and function and any semblance of the clean abstraction they used to provide are the first things to go.
Even C has a bunch of functions to work with the date struct as an opaque type....
I can't anticipate those requirements---maybe our date class has to bomb out if you pass in, say, 1800 or -5 as a year because someone on my team insists they're invalid. People work around these classes when they have to do date arithmetic because they're too slow, or they depend on the machine's locale and the locale isn't under control, or they take timezones into account in fundamentally broken ways, or any number of other reasons.
And then you use a better class. In Java you use something like JodaTime in .Net you use NodaTime - a .Net library written and maintained by Jon Skeet (the person with the highest ranking on Stack OverFlow). If that’s not good enough you write a custom extension or mixin to handle your special snowflake handling and still let all of the consumers treat it as an opaque type.
I don't want to convert your boxed int to a string and then parse it as an integer just to do math on it. That creates a ton of syntactic noise, not to mention being slow.
We were talking about an accountId that just happens to be represented as an int. You’re not going to be doing math on an account id. Also neither .Net with generics or even C++ templates have the boxing and unboxing issue.
Yes, and the solution is a pain in the ass! So don't create new interfaces to version and deprecate for no good reason.
If there was a business case to change the underlying structure - by definition there was a “good reason”.
And I'd just send an integral id over the wire, probably documenting that it's the id of an account using something like the field's name in the message. No extra tooling needed, and I don't have to make a fuss about how the id() function (that I had to add to the Account object anyway) is for the big boys and should be avoided whenever possible.
Even so, you should still not send your internal representation that may change across the wire and do some type of mapping. Are you going to send the same model that came from your ORM that’s tied directly to your database across as the representation to the outside world?
Yes, for conversion to/from time_t, formatting, and parsing. That's my point; you use the integral type whenever you don't care whether it was a Thursday because the integral type isn't going to waste your time.
> And then you use a better class. In Java you use something like JodaTime in .Net you use NodaTime - a .Net library written and maintained by Jon Skeet (the person with the highest ranking on Stack OverFlow). If that’s not good enough you write a custom extension or mixin to handle your special snowflake handling and still let all of the consumers treat it as an opaque type.
Stop with the hero worship. You should do things that make sense in context. Recommending that everyone use a library in all contexts because it's written by Jon Skeet is a ridiculous thing to do. (BTW, it seems like the Joda-Time people recommend you use the standard java.time package instead.)
> We were talking about an accountId that just happens to be represented as an int. You’re not going to be doing math on an account id. Also neither .Net with generics or even C++ templates have the boxing and unboxing issue.
Who are you to say I won't do math on account ids?
> If there was a business case to change the underlying structure - by definition there was a “good reason”.
In the situations the two of us have discussed, there is no business case to create these interfaces in the first place. Where is the good reason?
> Even so, you should still not send your internal representation that may change across the wire and do some type of mapping. Are you going to send the same model that came from your ORM that’s tied directly to your database across as the representation to the outside world?
What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
What else are you going to do in C? But even most real world implementations of C have all sorts of structs and typedefs that you shouldn’t make any assumptions about and you should treat as an opaque type.
Who are you to say I won't do math on account ids?
Is that really what you are going to argue? That in a real world use case you’re going to be doing math on an accountId?
What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
So you’re going to pass a raw key/value record set around your code and program like it was 2006? So now you’re going back to mapping your sql resultset to an object that makes sense anyway. Going right back to my point that you’re going to have a mapping between your raw results -> domain model -> view model (where the view model is serialized for external use) and back again anyway.
Subtract two times? (BTW, struct tm isn't an opaque type.)
> > Who are you to say I won't do math on account ids?
> Is that really what you are going to argue? That in a real world use case you’re going to be doing math on an accountId?
You seem to think that's absurd? Maybe I want to store a set of accountids. So I sort them (bit-extraction or comparison!), take deltas (subtraction!), and encode the deltas sensibly. Maybe I have two lists of accountids and I want to check that none of the things in the first list show up in the second list. Some sort of hashing scheme (bit-fiddling!) is going to be my friend here.
> > What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
> So you’re going to pass a raw key/value record set around your code and program like it was 2006? So now you’re going back to mapping your sql resultset to an object that makes sense anyway. Going right back to my point that you’re going to have a mapping between your raw results -> domain model -> view model (where the view model is serialized for external use) and back again anyway.
What SQL? What resultset? What object? I have an integer. Why do you assume it doesn't make sense?
You’re going to do “bit extraction” to sort? If you treat accountId as an opaque type, you are going to have a comparison operator as part of the type and the rest of your code is going to just use a < or > symbol.
take deltas (subtraction!), and encode the deltas sensibly. Maybe I have two lists of accountids and I want to check that none of the things in the first list show up in the second list. Some sort of hashing scheme (bit-fiddling!) is going to be my friend here.
Or in a modern language since you have already defined equality between the two types and overridden the GetHashCode() function....
var deltas = accountList1.Except(accountlist2)
Why are you doing that all through your code instead of encapsulating the concept of equality, less than and greater than in one place?
What SQL? What resultset? What object? I have an integer. Why do you assume it doesn't make sense?
You were referring to CRUD apps and having to serialize the Account object. Either you are using an ORM, you are getting results from a database as a record set - which is usually represented by a dictionary of key value pairs and mapping it to your Account class or you are sending back a raw record set.
More than likely, you are mapping from your domain model to your externally exposed view model anyway.
You (i.e., all commenters in this thread somehow disputing the AccountId object version of the story) are completely right, it is difficult to decide up front what you need. Of course, depending on your domain, it may be perfeclty valid to have -5 years or 1800, but I am not asking you to never implement years like this, I am merely suggesting that it is useful to have one place where such decisions, invariants, roles - your understanding of a year, perhaps specific to your domain - are located.
How your understanding is structuring is a different concern, because of course, a class with 10000 LOC is horrible to work with. But you can always use means of abstraction, and compose independent modules. An int does everything, maybe in one context a year shoudln't be 1800, maybe formatted as 4 digits. A year class / type / object is the place to store these decisions. As I outlined, such decisions are both technical (how many bits?) but may also be domain specific (year must be > 1800). Maybe you have an int internally, but abstract it with a non-zero int.
It boils down to interpretation. An int allows for many operations, but they may be invalid (in the sense of "implausible", or "undefined"). For example, an account ID of 1 can be multiplied by 2 and then by 2 again, and so on. Integers form a relation this way, but not all extensions of this relation might be "meaningful.
Integers as ids are a great example for this. Technically, they are great, because they are simple numbers, can be typed on a keyboard, and be readily interpreted, but not like integers.
6 people two times as many than 3 people. In Germany, grades are marked 1 - 6, (1 = very good, 6 = very poor). But the relationship is merely ordinal. A grade of 4 is not "twice as bad" as a 2, although the numbers are relatable that way. An Account ID of 6 is not twice as "good" as an ID of 3. For ids, you want them to identify, in that they are exclusive and exhaustive - but integer IDs are not supposed to be ordinal (in that their sizes are comparable in relation to each other, 6 > 5 is an invalid statement). Of course, you can retain this interpretation, e.g. an auto increment id would indicate that id 6451 was created WAY later than ID 6, but this is difficult to interpret; because it doesn't really tell you how much later, and also deletion of integers in between a range may be reissued (id 1, 2, 3, 4, delete 3, 3 is a missing rank, 3 could be reissued). So ids aren't ints, because they are exhaustive, but they aren't ordinal, or at least interpreting them ordinally is dangerous, and some operations aren't allowed even if you interpret them to be ordnial; for example, it would not make sense to calculate an arithmetic mean from integer ids, although mathematically this operation is allowed. In statistics, this concern is discussed as the Skalenniveau (German, meaning Level Of Scale), in English it's called level of measurement [1].
In sum, it does not matter what your usecase is, an int is an int, but depending on your use case, it might be worthwhile not to pass an uninterpreted integer around, but actually wrap it in some kind of object, where you localize all your decisions how to interpret the integer.
None of should be derogated as some form modern hipster javascript, where none of us highlevel kids don't know how to bit-bang a set difference from some account ids; but rather, these ideas are really old. Even in C, data abstraction is useful, in that you don't fiddle with integers but define a set of methods, possibly in a module that interact with a hidden internal representation. Deciding on where to cut these modules appears to be a difficult...
If you can explicitly refute those, then your position would be stronger.
Why should the developer care what the underlying type is?
It should be an opaque type. When you serialize it, your serializer should call your ToString() method. When you deserialize it, your deserializer would call the constructor that takes an int.
But why are you saving an id to a database - which would never be used as int - as an int?
But even in most CRUD apps, your domain model with rich types would be different than your view model which would probably also be different than your DB model.
You are going to be mapping back and forth regardless - hopefully using a tool like Automapper.
That's because some document generators will exclude functions that doesn't have document attached.