This is the true sign of a programmer's transcendence. Specifically the irrational joy of seeing net negative LOC diffs.
It's not about how much you can add. It's about how much you can remove without sacrificing correctness, functionality, and readability.
Deleted code on the other hand, is never a liability. The act of deleting it may be, but due diligence should ensure it is not.
Once I was using an industrial camera that had a mode that allowed it to mirror or “flip” the image output. Unfortunately, after running for a few hours, sometimes it would decide to flip the image on its own. Had that “flip” feature not existed, the bug could never have occurred.
The point isn’t that you shouldn’t add useful features, but that even easy to add features aren’t as “free” as they might seem and the risk of adding them should be recognized and weighed against their utility.
He would write helper functions that he thought would be useful before writing actual code and often ended up wasting time. Half the functions he wrote, he never actually used, but he'd still spend time writing them and unit tests for them.
If it was this simple we wouldn't be still arguing about coding styles decades after they were invented. I know few old programmers that I respect very much, and they don't agree about the perfect coding style, not even simple stuff, like braces in separate lines or not.
Money quote.
I disagree with the logic in this sentence. New coding styles are invented all the time. The guard statements in the article were only formalized in the late 90s, for example. These arguments about coding styles "decades after they were invented" are the only way we know which work and which don't.
It's not a matter of taste. It's how you separate the wheat from the chaff. Except for the brackets one. ;)
You could argue the placement of braces with lots of valid arguments both way, but this... to reduce LOC ? Doesn't feel like a valid reason in any language using braces...
uh ? for me the less code on each line and the easier it is to read
Sometimes more verbose code is more readable, and sometimes it is the presentation, and sometimes this is "superfluous lines", that increase LOC.
The argument against placing braces on their own line is that this:
if (a)
{
print(a)
}
conveys exactly as much information as this: if (a) {
print(a)
}
while taking up more space. The thing the brace tells you is already told by the indentation, so the brace is on a superfluous line. if (user.exists()) {
destroyHuman(user);
hideRemains(user);
}Which is true, and of course begs the question: why do you even need the braces?
2) I semi-frequently make indentation mistakes when resolving merge conflicts. In braced languages this is fixed by an autoreformat. In unbraced languages, I have to take a lot more care with merging, lest I end up in yet another lengthy debugging session ending in facepalms - or worse, check it in.
3) Redundancy in the face of typos and whitespace destroying mediums such as many pieces of web technology - comments sections, forums, etc.
if (a) {
print(a)
}
and
if (a)
{
print(a)
}Could just be if (a) print (a)
Except in perl, where it's
print (a) if (a)
And if you want to add a second line you have to change it, thus I got into the habit of if (a) { print(a) }
But I don't code for a living, any more than I run network cables for a living, or screw things into bays for a living. I code as a tool to get the job done, most recently that was writing some perl to parse the output of a tcpdump which was outputting rtp sequence number discontinuities to see where packet loss was occurring, what jitter was going on, etc. Fast, to the point, isolate the problem (crappy juniper srx), fix the problem (replace with mikrotik ccr), job done, drink beer.Maybe I am mentally disabled, but I can only focus on one logical task at a time. Can other programmers multi-task their coding?
I more meant it in the sense that it's nice to be able to soak in more of the code at once and avoid unnecessary scrolling.
The less scrolling, tab management, and other context switching I have to do to look at those "2" functions, the easier it is on my memory.
I don't disagree, but the sentiment seems to be "fit everything on the screen at all times", which seems like one of those rules that may take more time to implement than it saves in actual time.
I work on projects with upwards of 100k lines of code consistently, the very idea that anything relevant will fit on one screen is absurd, so I am always curious what kinds of projects people work on where the relevant code is literally within a half screen of each other.
Given that above description, you can probably guess I don't have everything on screen at all times, not even close. But I'll refactor off small files of related functionality where I can, subdivide things that are getting unwieldy, etc. - put private/static functions near the public/exported functions that call them, break things up into (sub)modules, etc. and get good grouping.
> I don't disagree, but the sentiment seems to be "fit everything on the screen at all times", which seems like one of those rules that may take more time to implement than it saves in actual time.
Taken to the extreme, any such rule absolutely will. I certainly don't have that as a hard and fast rule. Or even as a guideline per se - it's more a faint but distant dream, or perhaps a feverish hallucination, a reminder of codebases past. But there's often plenty of cruft and low hanging fruit that can be fixed up in these codebases. Everyone has their "just one small change" to check in, so things build up and they don't jump to "okay, this is one line too many - time to refactor."
I practice pain driven development - when it's getting to be a pain to keep enough of something my head to do whatever sweeping changes or overhauls I might need to do, see if some pure or near-pure refactoring changes it. No expected behavior changes, often not even class or structural changes - often literally just regrouping code by topic and category via cut and paste. Horrific diffs, but low-risk checkins, and often a lot easier to reason about afterwards, and suddenly related code is often on the same screen.
(Although far from always.)
Yes, it's nice when things are close together, but in my case it's so rare as to not be point to consider trying to achieve, but more like a mental joke like "Hey, Bob. Come here and check this out! Two functions and template I am working on all fit on the same screen!" And Bob's like "wooooah, dude". And then we take a group selfie with my monitor and have a laugh, print out a poster of the selfie for the bulletin board for a few months, then throw it away during a cleanup and move on...
But to have people say "It's important to save whitespace so stuff can all be on the screen to avoid context switching" seems completely and utterly pointless.
void foo(int x) {
if(x == 0) {
bar() }} public class Main {
public static void main(String[] args) {
System.out.println("Hello world!") ;}}
We had a good laugh. (that was fine with him, of course as long as it's a joke)PS: getting back to the original topic, imo, formatting style should be project-wide rules set at the beginning or during refactoring. Don't get too hung up on style as long as it stays consistent.
void foo(int x)
{ if(x == 0)
{ bar(); } }
That is workable. I have adhered to it (mostly) in this Yacc grammar file: http://www.kylheku.com/cgit/txr/tree/parser.yJust in the grammar; not in the supporting functions.
It's an attractive style for C statements embedded in another language, because they look more like capsules somehow.
It's not really a valid reason. Unfortunately, there are still a lot of people who think LOC is a valid metric.
/**
* Get the foo
*
* @return the foo
*/
int getFoo();
/**
* Get the bar
*
* @return the bar
*/
float getBar();
/**
* Set the foo
*
* @param foo the foo
*/
void setFoo(int foo);
/**
* Set the bar
*
* @param bar the bar
*/
void setBar(float bar);
Compare with what any sane project would do: int getFoo();
float getBar();
void setFoo(int foo);
void setBar(float bar);
Multiply that by a dozen such attributes, and what should fit on a single screen spans now hundreds of lines. What you could see at a glance, you have to search for. There's simply no way those utterly redundant comments increase readability. (Yes, the real comments in the real code were just as redundant.)LOC is more valid as a metric than most care to admit.
case class Baz(foo: Int, bar: Float)
Gives you a free hashCode, equals and copy as well.
int foo; float bar;
That can later be changed, when the need arises.
>LOC is more valid as a metric than most care to admit.
It's not.
Not all of it, actually.
> nobody had to physically type that
We totally had to read that.
> And usually no developer will be dwelling on this object or this area of the object.
Well… yes, usually. Sometimes however we did want to pay more attention than usual. At that point the original dev would have made a real comment. And that comment was lost in the noise, so you couldn't even distinguish between real comments and useless boilerplate.
I'll repeat my current gut feeling. Intrinsic measures of software quality are terrible as guides for how to write code. They are decent at predicting quality, but are not usable in a prescriptive way to write quality code.
I had the same thing happen about 10 years in. I just got sick of the extra coding and long indented blocks. And just switched over one day.
Why is this not taught from day one?
There are times where early return makes a ton of sense. Usually that's the case when you can derive the return value from a shallow interpretation of some input values (i.e. check preconditions) but still require more complex processing for other input values. In that case, return early, but constrain the rest of the method to one return point.
On the flip-side, there are cases where an early return makes it incredibly hard to reason about the function. This usually occurs in dense, nested conditional logic. In those cases, I found sticking to rule that a return value should be initialized once and only once, and return from a single point, beneficial in even reasoning about the problem.
But as a novice youd should adhere to rules. As you gain experience you learn when to break them.
I think the "rule" should be "exit early", and the exception "multi-indent exit later".
Having said that, early return should be for error/ignore situations - there should only be one return for a result and that should be at the very end of the function.
I now think of the early part of a function as a filter for bad params, invalid state & whatnot, and return as early as possible.
Code just feels less complex that way.
OO is better if the objects tell a story in a way that ProviderStrategyDataFetchers fail to do so and are effectively just wrappers for data structures. If you go pure data structure without story you just end having to add comments to explain purpose. The comments are the classes.
" Best thing I learned about commenting is: Comment WHY you are doing it... not WHAT. Code is the WHAT... Comment is the WHY
# open file
with open(my_file, "rb") as binary_file:
binary_file.seek(8)
No mention of what was at byte 8, or what the hell it was doing, but he commented the only obvious part of the code.This is one of those things that whether you drop it or not, you recognize the times it's caused you severe pain because you didn't notice it was single line when adding a statement, such as a debugging one, and all of a sudden the conditional part isn't what you were expecting.
This is actually one of the things I love about Perl. Single line conditionals were changed so they both looked different, and they only allow a single statement. e.g.
if ( condition ) statement;
becomes statement if condition;
Regular if blocks still work as expected, but there is no braceless version of regular if conditions. I understand many people find it jarring at first, but it does allow for clear and concise precondition and return early statements, especially when grouped together. e.g. return undef if $param1 < 0;
return undef if $param1 > 100;
die "Not a number" if not looks_like_number($param1); if (condition){
doSomething()
doSomethingElse()
}
else
doSomethingDifferent()
doSomethingConfusing() // is this part of the else or not??
Perl got many things right. Its a shame it seems to have fallen away as a language.Is that a thing? No question that many concepts of OOP are in heavy need of reform, but I didn't know the basic idea of a struct was one of them.
I can sort of imagine this for quick-and-dirty things in untyped languages - but if you have types, passing dicts around everywhere seems like needlessly throwing away type safety - while it's also cumbersome to code and less efficient at runtime.
Even if I'm getting from a DB which won't be changing out from under me, many times I'd prefer just getting a simple Map rather than spending time creating a "safe" object. Do I create a User and UserWithDetails? Or always return UserWithDetails but sometimes fields are empty because I don't care about them? Dicts are light, objects are cumbersome.
I also see getting parse errors when APIs change as a positive thing. When you change an API, things do break. Learning what breaks as early as possible is good. It's not even about objects vs. dictionaries, you could use any structured composite datatype and the overhead you associate with it depends very much on the language.
Of course, for a quick hack I'd happily dump data into a dictionary and call it a day. But if I want to learn why it breaks 5 years later, having a clear specification of the expected data is much better even if it cost me an extra few hours.
My understanding is, by “heavy OO” they mean custom container-like classes implemented by encapsulating a list or dictionary, but exposing non-standard APIs for get/set/erase/replace.
By the OO books that’s the way to go, because object state encapsulation. Practically, exposing raw lists/dictionaries/vectors/iterators at the class API boundary is more flexible because it makes the collection compatible with lots of other code, both in standard runtime and third-party libraries.
Another thing, if the only state that container holds is the collection of items, maybe you don’t need any container class at all. Just write global functions / static class / static methods that directly process lists or dictionaries of these items.
So you end up with the best of both worlds (in my opinion), where your state is made up of simple plain objects and you behaviors are just functions that accept simple plain objects, but you get all of the benefits of compile time static type checking because they are checked against the interface.
interface SomethingWithAge{ age(); }
class Person{ age(); // age in years }
function classify( SomethingWithAge item ){ if( item.age() < 20 ){ print( 'young') } else{ print( 'old' ); } }
p = new Person( 33 ); classify( p ) // prints 'old'
class Message{ age(); // age in milliseconds }
c = new Message( 231443 );
classify( c ); // prints 'old'
It's in the roadmap, but discussions about it are still going on four years after the ticket was opened...
OO can run into versioning issues with fields/structures changing and needing to have many versions of those over app iterations just dicts/lists. Database fields being added and removed and supporting old versions is more easily solved in basic dicts/lists that map to json/xml and the base of any language. Dicts/lists are very interchangeable across systems and are the base of API outputs and the ease of use in javascript, python and other dynamic uses.
There should be a reason you are using structured OO but many times it becomes as weight, though there are good times to use it and most projects have some of it. Good reasons are hardened/non-changing codebases, possibly native apis/libs to help understanding, but when it comes to rest/http apis usually you are building OO that consumes data simply to return to dicts/lists when output/input into the apis/client-side/etc.
Classes/OO/types can be built as well with objects that are based on dicts/lists that do have some internals and helper methods that get/set keys and values and perform actions if you don't want to do that with another context class/api, or wrap dict/lists so that the serialization/deserialization and versioning issues are not a problem as the flexibility of dicts/lists and the benefits that brings in simplicity are still there but there is more structure where needed.
I agree with all your other points and I‘d even agree if you wrote return early makes code more readable but not that it‘s something experienced programmers do.
Programming is still - to some degree - resource management. Inexperienced devs often miss that fact, because they are focused on memory management and believe they can rely on the garbage collector for clean-up. In real projects you quickly end up managing file descriptors, sockets, handles, GUI resources, etc. Early return is terrible for that, because your coding style starts to depend on the kind of resource to manage.
That‘s why the more experienced folks end up writing single exit point code sooner or later.
EDIT: I‘m not arguing that early return and proper resource management are incompatible, just that most experienced programmers have often been burnt enought to avoid it.
The resource almost all programmers are managing is the resource of human time. Human time to code the project, human time to review the code, human time to alter the code after the fact, human time to port the code to different environments. If you're writing code to run AI at Google then fine, you're managing resources at a level where performance starts to hit limits that warrant a style change, but generally speaking you know that ahead of time and you choose tools to handle a project of the right scope, but generally speaking exiting early isn't impacting your code performance. Big O mistakes are.
Also, some of your examples (like altering or reviewing code) can become easier and faster if resource management is a first-class citizen in your code base or language (i.e. is either more explicit in the code, and you know to look for it and manage it, or it is somehow taken care of automatically by your language, so you would have a hard time forgetting it, even if it is handled implicitly).
Yes, RAII works for the casual std::vector, but it's not a maintainable solution for general resources.
EDIT: and I was the lead for a high availability C++ RTOS. I know that not all patterns fit for writing quality, available code. This fits remarkably well though. Even though we were totally async and didn't rely on the stack for context lifetimes.
The guard object is there (e.g. "on the stack") but the destructor is not defined in-line but in a separate class method definition. Classes have always that implicit ideal of being "isolated". This is wishful thinking, of course. In the end all the parts of the code need to contribute to the program. That's why OOP codebases often end up as a terrible mess. Classes can't decide if they want to be isolated or involved. The compromise is all this terrible implicit state. (do you know the quote by Joe Armstrong about the banana, monkey and jungle?)
Exactly, simple but not too simple. If the condition does not need to return early or can't, you don't.
> quickly end up managing file descriptors, sockets, handles, GUI resources
In some of the cases you describe you wouldn't return early or depending on the need, but if you can you should to minimize work.
Some examples: file descriptors you'd return early if the file does not exist, sockets you'd return if dropped or compromised and you have performed cleanup, handles if null, gui resources can be lots of things but if you were doing something where you needed to cleanup you can cleanup and return. In C# for instance you might wrap some of that in a using such as files, sockets, streams etc. Most of your examples you might already be in the meat of the return early flow and usually there is still a return at the very end but in/after the meat.
Returning early isn't saying do a dirty break or asserting out, you don't return early before cleaning up if you are within some resource.
Even C has auto cleanup with __cleanup__ attribute in both gcc and clang.
Other languages have constructs that obviates this pattern, for example Python has `with` blocks that acquire resources and free them upon leaving the block, and Go has a `defer` statement which adds code to some LIFO structure that always gets executed upon returning.
And I'm not saying that finally depends on checked exceptions, I'm saying that pattern is very brittle. You change lower code to throw a new exception, and you change the above code to catch it like you're supposed to, but now the middle code has no idea that there's this new exception and leaks resources. So you end up either having brittle code who's correctness depends on implicit choices of the code around it, or you're wrapping pretty much all function bodies with try-catch-finally-rethrow.
If you’re able to follow that pattern then safe handling of unusual control flows tends to follow naturally. You just adopt whatever mechanism your language provides for scheduling the code to clean up and release resources at the same time as you allocate those resources. Almost all mainstream languages at a higher level than C provide a suitable mechanism today: RAII, some variation of `with` or `using` block, try-finally, Lispy macros, Haskelly monad infrastructure, etc.
That way, the only places you need to write any extra logic are the layers where you allocate/deallocate and possibly lower layers that manage side effects on those resources in the unusual case that they require a specific form of recovery beyond the default behaviour in the event of aborting early.
Hopefully if you do have to work at low level like C or assembly language and you’re using forms of control flow that could bypass clean-up logic then you already have at least some coding standards established for how to manage resources safely as well.
You misunderstand how `finally` works. The whole point is that it runs regardless of exception type. So the resulting code in the middle layer doesn’t need to know anything about the code it calls, and doesn’t leak resources, even when calling or called code changes: It’s not brittle.
> or you're wrapping pretty much all function bodies with try-catch-finally-rethrow.
No need for `catch` and rethrowing. And in the case of C# and modern Java, no need for the rest either: You only need to wrap resources that you allocate into `using` (C#) or `try` (Java; but not `try…finally`! [1]). Sure, it’s more verbose than RAII in other languages with automatic storage duration (like C++). But it effectively performs the same.
[1] https://docs.oracle.com/javase/tutorial/essential/exceptions...
I generaly don't like when one person says (declares) how experienced persons act, but in this case I have to confirm with myself, as I also "evolved" to it and I tend to program in isolation, so I am no influenced by others so much.
(This example comes from Elixir.)
value = if boolean, do: this_value, else: other_value
is undefined in some circumstances unless an "else" is included.
This pattern might seem weird to OOP folks (the assignment, not the ternary-operator-style logic), but in Elixir-land, it is not only considered superior to assigning inside if/then logic, you actually get a compiler warning if you assign/bind to a variable inside a logical expression because (again) that value becomes undefined as soon as you leave local scope (or re-acquires the value it had in the outer scope, thanks to immutability).
And if you think about it, values can only travel in one direction using this scheme, which further simplifies reasoning about information flows.
I would argue that this is also superior to early-exit as having one entry point and one exit point is much easier to reason about, and because the extra logical complexity you're ostensibly trying to avoid writing by early-exiting is actually still there, it's just obfuscated... and that complexity should be made obvious (in which case, if it's extensive, it would be a code smell... consider the example of a function with 25 early exits that "looks flat" visually, but which actually has 25 different branches)
value = boolean ? this_value : other_value;
If the branching logic gets too complicated, I usually move it into a function (private method) with a return in each branch. let value = do {if (boolean) { this_value } else { other_value }}
...which is even closer to the Elixir style than JS's ternary operator (which is identical to Java's).What about error handling? No mention here. What I do not is let the library/framework to handle most of it. With a recent server library I wrote, I can throw anywhere and it will be caught and displayed as expected without extra try/catch.
Same way you learn to let some code fail instead of handling every exception.
This! Comments should convey programmer's intention and explain the thought process, not the code itself. Also, too many comments usually indicate that code should be refactored.
> Same way braces go on the end of the method/class name to reduce LOC.
For me, these are exact opposites; the reason to not use one-liners is the same as the reason to put braces in separate line: both improve readability.
Screw that. I've been writing code for 15 years, and Allman style braces make it so much easier to mentally parse code into blocks that they're worth every single LOC. I can't speak for anyone else but I'm not working on an 80x24 terminal anymore.
I just defer to the code formatter. That way everything is consistent looking internally, which is all I care about. I couldn't care less whether the braces are on the same or a different line or other things like that outside of the fact that I do like internal consistency within projects.
All that aside, as soon as I saw the OP title I knew (via years of exposure to bikeshedding programmers) that this thread would end up having a ton of comments on it and wasn't surprised to see it was up to 500-something and still rapidly climbing.
So totally this. I despise indenting with tabs with a passion, but when I heard gofmt did that I was actually pleased. There's no arguing with the official formatting utility: https://golang.org/doc/effective_go.html#formatting
At the end of the day you can do whatever you want with the code you're writing, and some pre-commit hook can run go fmt/prettier/whatever and then it'll all get standardized.
I've become a very heavy proponent of code formatters (I added prettier to the entire team) and I truly believe that talk about code formatting will never really die off, but at the end of the day you can do whatever you want on your side but still have a standardized looking codebase, and that feels extremely liberating to me.
As a PS: If you are a diehard tab proponent/different bracket placement/whatever but your team formatter configuration uses spaces instead then you can also use code formatters to do spaces -> tabs locally, say, on file open. Once you commit your file your precommit falls back to the team configuration, which removes your tabs. Everyone is happy.
I should see if he remembers where he got it.
Same goes to spaces vs braces, editor wars, etc.
Downside: my projects are usually a bit of a mess with mixed indentation styles :D
How big once you remove all the extra newlines?
Thank god OC didn't mention "Just like you move from tabs to spaces."
To me, it looks weird when functions don't have that extra line of space to set off their definition, but if/while/whatever aren't special enough to need that call out.
That said, the most important factor is simple consistency. In my person projects, I have the hybrid style. At work, I use same-line-only. If I'm on a project that has next-line, we all use next-line.
int max(a, b)
double a, b;
{
return a > b ? a : b;
}When I write Python I end every block with a "pass" statement so that emacs can auto-indent my code properly. The "pass" statement thus effectively becomes a close-brace. It drives Pythonistas into conniptions, but I never have to worry about reverse-engineering a block of code to figure out how to restore the proper block boundaries after a cut-and-paste has screwed up the indentation.
If boundaries exist they need to be clear. One shouldn't have to count the tabs that make up the level of indentation.
Its already a challenge reading code. Counting invisible tabs makes it even worse.
def foo(bar)\
:
if (bar > 5)\
:
return bar + 1;
#
else\
:
return bar - 1;
#
#Similarly, copying code from StackOverflow, Github and random blogs have all worked without any issues.
So it can't be as easy as you imply.
Mixed spaces and tabs as a next go, and it worked too---converting the pasted tabs into spaces of my style.
Smart-tabbing is pretty smart.
Granted, I knew that so long as the indents were proper (i.e it compiles), that pasting at that point would give valid code but... I can't see how using braces would have been different. Just because it compiles, doesn't mean it works. I've pasted javascript incorrectly multiple time to produce valid, but incorrect for my needs code.
The indentation strategy also looks to produce syntactically valid code, so long as what you are pasting lacked indentation errors.
Unsurprisingly, PEP 8 does have a rule for tabs vs. spaces: https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces (spaces, of course)
Tabs are semantic and only take one key press for movement back and forth and to delete.
If you see 1 tab you know it meant one indentation level. With spaces you have to think.
Plus with spaces you are stuck with 2/4/8 spacing(unless you reformat), with tabs you can configure your editor to your preferences.
Configure 1 tab to be 4 spaces.
Sometimes it's easier to go with the flow. I like tabs for the reasons you mentioned, but fixed-width spaces are a bit better for some reasons too. IDE's can do the heavy lifting of re-formatting indentation levels and converting tabs to spaces for me, and it means if I cat a file on a remote server regardless of the bash tab width settings or if I'm in your code or the stdlib it will all make sense.
Except that you can't because people using tabs will invariably start mixing tabs and spaces because they can't separate indentation from layout. So the code will be messed up unless you configure your editor to someone else's preferences. Also, I'm sure there is some obscure git setting to de-uglify tab users' diffs, but I'd rather not find out.
Before you say that I'm crazy... please, go and read pep8.
OK. What am I looking for?
But tabs vs. spaces is indeed a bad analogy, because of course it's 4 spaces. ;-P
pycodestyle, the Python style checker, has a few checks disabled by default (because there's no consensus they're good ideas), and even some mutually exclusive ones:
https://pycodestyle.readthedocs.io/en/latest/intro.html#erro...
The correct answer is whatever the project is already using, unless it is new and then you luckily get to choose.
Tabs vs. Spaces has a fairly good answer that flows out of this way of looking at things. There is a counter-argument to that answer, but it is invalidated if you move toward conventions for argument lists and chained method calls that are also designed to limit noise in the source control system.
IMO, the argument about bracing also has a practical, non-religious answer once you start looking at your coding conventions this way.
>>> from __future__ import braces
SyntaxError: not a chancePS: he coded at Hercules the graphic cards back in the day before he got into teaching.
It's only people who cut their teeth on adjacent bracing that find it more readable.
I've seen both, and the reduction in vertical whitespace matters more to me than the readability to an untrained individual.
You seem to be erroneously equating a coincidence with a preference.
You should have a pretty cool family!
The advent of automatic code formatting (and Python pioneered this in many ways with pep8, but even huge C++ projects are realizing the value in clang-format, clazy, etc) put the nail in the coffin of arguments against whitespace blocking. You can have an enforced uniform style throughout a project now, with no ambiguity, and in such contexts using whitespace as a block delimiter also has no ambiguity. It just takes advantage of formatting already being done to avoid redundant glyph use.
Yup, Python clearly pioneered automatic code formatting. Nobody had done it before them...
That document states Bill Gosper wrote one of the first pretty printers, but doesn’t give a date for it.
I would think it is much older, as writing a simple lisp pretty-printer is easy and reading lisp without autoformatting is “less than ideal”.
In what way are context-free languages "a grammatically bloated mess?" Whitespace delimited languages like Python have context-sensitive grammars. Now that is a mess.
Dismissing the whole field of formal language theory with that statement is so bogus, it "is not even wrong." There are tools and techniques for parsing context-free languages that are impossible with context-sensitive languages. Parser generators, structured editing, metacompilers, composable grammars; those are all impossible with context-sensitive languages. Context-free languages are faster to parse, easier to write compilers/interpreters for, and much easier to write tooling for (editors, linters, etc).
Ambiguous context-free grammars are a very different thing from context-sensitive grammars. You can't point to the former and say "so whitespace sensitivity is ok."
I can point to ambiguous context-free grammars and say whitespace sensitivity is ok, because the tools to fix the one are often the same to fix the other. (Ambiguous context-free grammars do turn out to make context sensitive languages, because it's a blurry line in the grass between them.)
Whitespace sensitivity is one of the easier context sensitivity challenges to embed as a sub-language in an otherwise (mostly) context-free language, because you can represent it entirely as pseudo tokens from a simply modified lexing phase in a traditional CFG parser. Python is an easy and clear proof, as that is exactly what it did (Python is also not purely a CFG after whitespace tokenization because of also how it handles the dangling else, but we've already mentioned those weeds).
Even if you don't find the boundaries between the classes of languages fuzzy (and Parser Combinators and PEG grammars have a lot to say here), context-sensitive languages are a tool in a growing toolbelt, not a "complex" evil to be demonized.
Algorithmic complexity is not "moral superiority." The whole point of formal language theory is to show how the different languages differ in terms of how difficult they are to work with.
> just as they use regular expressions for tokenizing and allow regular language sub-languages
That is because regular languages are a subset of context-free languages. There is nothing special to "allow" there.
> I can point to ambiguous context-free grammars and say whitespace sensitivity is ok, because the tools to fix the one are often the same to fix the other.
No they are not. Precedence rules and extra lookahead will help with ambiguous context-free grammars but not with context-sensitive ones. There is also a whole class of algorithms, such as GLR, specialized to handle ambiguous CFGs efficiently.
> Whitespace sensitivity is one of the easier context sensitivity challenges to embed as a sub-language in an otherwise (mostly) context-free language, because you can represent it entirely as pseudo tokens from a simply modified lexing phase in a traditional CFG parser.
That statement is nonsense. There is no way to "embed" a context-sensitive language in a CFG. What you are saying is that it is "easy" to make a whole context-sensitive parser just to produce a context-free language so you can pass that to another CFG parser. That is not "easy," that is bolting crap onto other crap.
Chomsky's hierarchy was designed for production from categories of languages. That it doubles as a useful rule of thumb for algorithmic complexity in parsing is a fascinating dualism in mathematics. That those same rules of thumb reflect basic automaton abstractions is even more fascinating. This is also where it seems the clearest analogy lies to why I find your arguments to "complexity" so useless. It sounds to me like a strange steampunk form of the "640k is all anyone will ever need" fallacy: "why use Turing machines when Pushdown automatons will do just fine?"
Yes, there are a lot of great tools for working with CFGs, just as we've pushed Regular Expressions far past the boundaries of formal Regular Languages, we push these same tools past the boundaries of formal CFGs. We aren't actually constrained by the limits of only using Deterministic Finite Automata or Pushdown Automata in our programming, we have vast Church-Turing–level state machines at our power completely and easily capable of tracking contexts/states.
The "context" in a whitespace-sensitive language is "how many spaces have you seen recently?". This is not a hard question, this is not some mystical "context sensitivity" that needs an entire "context-sensitive language parser". It's a handful of counters. That is the very definition of easy, that's the built-in basics of your average Church-Turing machine, keep a number on the tape and update it as necessary.
Python's specification for the language defines the language in a CFG BNF just as the majority of brackets languages. The one concession to its whitespace sensitivity is that it expects from its lexer INDENT and DEDENT tokens. Those tokens are added simply by counting whitespace between lines, seeing if there is a < or > relationship. After those tokens are in the stream (along with the rest of the tokens defined in their associated (Mostly) Regular Languages) it is parsed by whatever CFG automaton/algorithm the Python team wants (LR, GLR, LALR, PEG, etc). That's not "bolting crap onto other crap" by most stretches of the imagination, that's using a pretty simple stack of tools that any programmer should be able to (re-)build, and not one at all more complicated than (and in fact built entirely inside) the classic tokenizer/lexer feeding a parser stack.
Honestly, from everything you have posted so far, I really get the impression that you have never worked on programming language parsing, or with large code bases. The difference in algorithmic complexity is not "useless," it is the difference between being able to compile millions of lines of code in a few seconds on modest hardware, and needing a cluster just to get your builds done:
https://community.embarcadero.com/blogs/entry/compiling-a-mi...
If we want to fight anecdote for anecdote, I've certainly seen millions of lines of code of Python analyzed ("compiled") in seconds on modest hardware, rather than a cluster. Differences there are more in the static typing versus dynamic typing, and strongly compiled versus weakly compiled/primarily interpreted there. Maybe a better anecdote is millions of lines of Haskell? Seen that too. Whitespace-sensitive languages scale just fine.
I can understand it if you want to admit that you just don't like whitespace-sensitive languages for personal reasons, but there aren't very good technical reasons and you seem unhappy with my explanations of why that is the case. I'm not sure how much more I can try to explain it, and given you seem close to resorting to personal attacks I'm afraid this is likely where the conversation ends.
https://softwareengineering.stackexchange.com/questions/9954...
The name tells you that it won.
To say it is religion isn't to say "your preference is dumb". In my own code preferences I try to maintain a split between "have a reason I have confidence in" and "personal preference". And "easier to read" is almost always in the latter. (If you can say WHY, that is the actual reason, but your actual reason still has to be provable.)
Given the difficulty of finding good research and the inherent difficulty of the research (if familiarity is a big component to preference, and a proven ability to mentally parse code required to judge, then good luck running a control group) I have a lot of items I have confidence in being provable without having actual proof, but the lengthy preference list also means I'm willing to accept that each of those can switch columns.
Way too many of our "easier to read" defenses are really "it is easier to read because I'm familiar with it. I personally think camelCase is terrible - language has spaces for reasons! - but there is no denying that it is very common and that the vast majority of coders learned it first, an unfortunate self perpetuating cycle.
Nonetheless, shown research that said I was wrong (assuming said research had taken familiarity into account) I would change my stance rather than dig in my heels...even if that change were to only add "but that's just me" to the end of it.
A later comment on the SO post described how K&R can have maintenance costs because when moving things around it's more difficult to tell where a statement ends, and can accidentally cause side effects.
The only thing being considered was "bugs in the final code", not "time and effort in maintenance."
As everyone else seems to use K&R I've just had a adapt over time :-(
There are lots of sane ways to read code, but consistency is key. It's hard to change muscle memory in how I type, though, so I think auto format is the way to go.
I like One True Brace Style (1TBS) with an uncuddled 'else', which has else/else if on new-line with the break before it (similar to Stroustrup K&R without any same line properties, one thing per line, no bracket-less statements) and setup the standard that way if I am designing it, but do Allman or whatever variant the codebase uses if it already exists.
void foo() {
code...;
}
void foo()
{
code...;
} stuffstuffstuff....
stuffstuffstuff...
So I read C and Python (and Lisp) code the same way. A naked open brace looks jarring and ugly to me. It also increases the separation of other related parts of the code, i.e. if (...) {
while(...) {
do(...) {
vs if (...)
{
while(...) {
{
do(...) {
The latter seems unnecessarily wasteful to me. void MyLongMethodName(SomeLongParamType param1, SomeOtherLongParamType param2,
YetAnotherLongParamType param3) {
if (longContrivedVariableName1 == longContrivedVariableName2 &&
longContrivedVariableName1 != longContrivedVariableName3) {
// do stuff
}
}
void MyLongMethodName(SomeLongParamType param1, SomeOtherLongParamType param2,
YetAnotherLongParamType param3)
{
if (longContrivedVariableName1 == longContrivedVariableName2 &&
longContrivedVariableName1 != longContrivedVariableName3)
{
// do stuff
}
} void MyLongMethodName(SomeLongParamType param1, SomeOtherLongParamType param2,
YetAnotherLongParamType param3) {
if (longContrivedVariableName1 == longContrivedVariableName2 &&
longContrivedVariableName1 != longContrivedVariableName3) {
// do stuff
}
} void MyLongMethodName(
SomeLongParamType param1,
SomeOtherLongParamType param2,
YetAnotherLongParamType param3
) {
if (
longContrivedVariableName1 == longContrivedVariableName2 &&
longContrivedVariableName1 != longContrivedVariableName3
) {
// do stuff
}
}
I also make liberal use of temporary variables: void MyLongMethodName(
SomeLongParamType param1,
SomeOtherLongParamType param2,
YetAnotherLongParamType param3
) {
auto condition1 = longContrivedVariableName1 == longContrivedVariableName2;
auto condition2 = longContrivedVariableName1 != longContrivedVariableName3;
if (condition1 && condition2) {
// do stuff
}
}
Vertical space is allocated to the actual parameters and conditions, and the extra syntax-only lines exist only to separate chunks. This works well with "paragraphs": void MyLongMethodName(
SomeLongParamType param1,
SomeOtherLongParamType param2,
YetAnotherLongParamType param3
) {
auto condition1 = longContrivedVariableName1 == longContrivedVariableName2;
auto condition2 = longContrivedVariableName1 != longContrivedVariableName3;
if (condition1 && condition2) {
// do stuff
}
auto someData = buildSomeData();
doSomethingWith(someData);
while (someCondition) {
// loop
}
finishUp();
return someData;
}Allman is great for this. Though I'm a Whitesmiths guy, myself. Still, same idea.
/36 years of commercial coding here.
Even with 80x24 (don't ask), I fully agree with Allman style braces.
indeed I've evolved it myself but always wondering if it was the right thing to do. Now I even have a name for it.
That's not true.
Sometimes, the code begs for an early return so you can focus on the meat of the method. Most times, that's not the case. Single return conditions should be the default, and you should have a good reason not to do the default. In Java, for example, if you also stick a 'final' on the uninitialized variable, you have very nice compiler check to enforce that every code-path initializes the variable once and only once. Sometimes that is also too strict for what you want to do, so you can break the rule, but you have to have a good reason.
>Same way braces go on the end of the method/class name to reduce LOC.
No. Just no. Not for that reason.
>Same way you move on from single condition bracket-less ifs. (debatable but more merge friendly and OP hasn't yet)
It's only debatable by people who are just used to it, or who want to minimize LOC for insane reasons. It's non-debatable in that it is a very common vector for bugs to sneak in. Over the last 5 years, there were probably as many bugs stemming from bracket-less ifs in our codebase.
>Same way you move on from heavy OO to dicts/lists.
You start introducing another common vector of bugs for a little flexibility. Sometimes that may be worth it. As a general rule, it's not worth it.
Except that I see soooooo many people try to write state machine code with early return/function encapsulation just to avoid indentation.
And then they get pounded when the state machine needs to evolve--and it always needs to evolve.
I really wish programmers had to design a VLSI chip, get it manufactured and debug it just once before they get out of school. The "So THAT'S why they have a global clock that synchronizes all the parallel operations in lock step" moment tends to be blinding.
Unless you are writing completely brilliant code your future self will hate you if you skimp on comments. Also self-documenting code is great, but intentions are not always clear to another person trying to figure out your code.
Plain code organized into understandable methods (usually no more than half a page of code), with good naming for variables & method names reduces the need for comments.
It's also easier to scan/read code if there's a minimum of comments in the way.
Clear, self-documenting code is great but it can't capture that holistic insight into what the whole program is doing. It can't capture context, because the whole point of clean refactored code is to be as context-free as possible.
As an example, our code base has a comment which refers to our internal issue tracker which itself refers to this HN comment: https://news.ycombinator.com/item?id=9048947
I was secretly hoping you actually meant your own comment, creating a recursive loop. But a surprise John Nagle is even better
Or sometimes, not even that: issues elsewhere in the system, possibly completely outside of your control (e.g. the language/framework/library you're using, the OS, business requirements, etc).
Code shows what is being done. Comments should explain why it's being done.
Often you have horrible complexity imposed on you from outside -- business rules you're implementing, etc, which are _not simple_. You can write code that encapsulates a lot of that, and even refactor it so you can see _what_ the code is doing pretty easily ... but it's often _very_ valuable to document in a code comment (docstring, JSdoc, etc) WHY it's like that.
Arguably, the comment is not to explain the code, in this case, but rather to explain the twisted bureaucratic logic you're having to implement, so maybe that still counts as "brilliant" code. ;)
I look back at a lot of my old code and even without comments, it's easy to follow the logic because I used sensible names and constructs.
Often it means you can refactor it so that there is only one of any "thing" so there is no need to clarify which version or role this "fooBarThing" is serving to disambiguate it from the other "bazBarThing".
Functional composition and well structured data is the key to this. It's basically halfway to point-free style.
Readable variable names doesn't mean wordy names. I would avoid using more than 2 words in a variable name.
You're lucky if you're dealing with code that's either small enough or simple enough not to need the added context...or if you only need to read your old code.
Documentation explains HOW to use a thing. Good comments explain WHY a thing is strange. Bad comments explain WHAT a thing does and must be made redundant by extracting and naming the thing.
Probably overly generalized to be pithy, but no.
Comments are by very definition documentation, which can and should cover of all of the what/why/who/how/where.
Documentation that occasionally explains how to use code can actually be useful!
Unless you actually get around to writing a user manual (which gets out of date), please do consider documenting how something should be used, particularly in libraries.
Also, i never said documentation isn't useful. HOW and WHY are useful. WHAT in documentation and comments isn't because it should be in the variable/function/method/class/instance name.
(Who/Where/When are covered by the source repository and the blame function.)
Moreover, when the weight of those comments trends towards 50% being justifications for why you did it that way, it's an excellent sign that your code has gone from "brilliant" to "super-genius", as in "Wile E. Coyote, Super-Genius".
"Completely dumb" code can be read by your future self without comments. Even, sometimes, by other people!
But for the general case, I do wholeheartedly agree with you.
That's like saying "the problem with healthcare is that it costs money". Of course comments can get out of date. The solution isn't to throw them out!
There's alternative to in-code comments answering the question "why" - it's commit messages. They can't be out of date.
git log -L0,10:file.txt
Write good commit messages and comments that answer "why" are redundant too. Commit messages by their nature refer to the exact code that they refered to when they were written. Comments answering "why" after a few years are misleading anyway, because code changed around them.
Commenting public api etc is obvious, and most people do it.
If there's not 1 line of code from that commit remaining why is it relevant?
If something looks weird still - I go back to the commit that created this part of code and git blame that. I don't remember a case where I had to do 2 steps like that.
BTW we have a rule of putting JIRA ticket numbers in commits, that makes it even easier to find out. You can see the whole discussion that resulted in the code you try to understand, test cases that you don't want to break with your new changes, etc.
sounds great until there's a refactor (including moving code around), then all the "comments" get buried.
So something entirely outside the codebase, which may or may not be available to the person who needs the information, and which may or may not need to be extensively searched through years of history to find the relevant commit message, which may or may not be sufficient to explain the code... is better than having a comment in the code.
The vast majority of comments I usually see can be rolled into variable names or function names. If you're writing lots of comments, that's a good indication your code is hard to understand, your variables are badly named and your functions are too long in my opinion. I think people that say "your code is bad if you don't have any comments" have things backwards personally.
Pretty much the only time I use comments is when I'm forced to write weird code to workaround an API bug, to explain an unintuitive optimisation or to give high-level architecture documentation.
I've honestly returned to code I've written myself maybe 5 years later and rarely had an issue that would have been helped with more comments.
That should leave you with a solid documentation of your application's components and APIs and a small amount of inline comments.
If you find you have more comments, either your code should be simplified or you're commenting trivialities.
As everything, it's a guideline with exceptions, not an absolute rule.
Likewise, your future self will hate you when comments fall out of sync with the code.
If the code is readable you don't need much in the way of comments.
Exit or get out quick... make it readable... then the meat is left for those that want it below...
one thing i do disagree with is commenting, there is a time in a place and its hard to derive on a rule on exactly where, when and how, but well placed comments are VERY important and can save developers a ton of time.
What does that mean?
I couldn't interpret this sentence.
Among all types a 'while' loop gives the strongest mathematical guarantees. In particular it ensures a condition is true at the start of each iteration, and is false when exiting the while loop. Combining this with loop invariants leads to precise and straightforward to verify code.
Avoiding while loops to ensure an exit just seems to be sacrificing a lot of power in exchange for some pretty shaky guarantees that your loop will finish.
Although in the majority of cases where a while loop could be used you're simply iterating over some (implicit) data structure. In which case it's obviously better to make this explicit using a "for ... in" loop.
For one thing you can't misplace ";" in a while loop.
I agree that "do while" loops are unintuitive and rarely used, and thus the place to first check for bugs. Also they save very little so I just implement them as do(); while usually.
But I know people who disagree about that, too, and shout at me for avoiding them:) It seems what's unintuitive for me isn't so for others.
But yes if you end up in a loop you have some sort of counter/null checks or way out if you end up in a constant/recursive loop.
Never leave open the possibility of a lock due to being stuck in a loop.
I think for loops should be limited to iteration over collections, or repeating sth known number of times.
There are cases when you need to decide if you want to continue iteration on the fly, and while loop is the perfect tool for that. Also while loop is arguably less error-prone than for loop: can't swap clauses because there's just one, can't write "," instead of ";" by mistake.
Less merge friendly as well. Also you should only do one thing per line when it comes down to it to increase simplicity. I like compact code but not adding complexity for no reason so others on the team can be confused.
> Comments are rarely extra weight if there's anything worth explaining
Good cases are non obvious flows and especially algorithms/notes or any section that will be public or generate docs from it. But mostly comments are weight not because they aren't good, but because they aren't updated and become out of date weight.
> Collection types are no replacement for OO patterns that have a reason to exist in the first place.
They definitely have a place but prototyping even they are better. They are the base of json/xml data formats and most languages. Everything on top of that should have a reason for complexity including OO as long as it improves understanding and simplicity.
The "simple but not too simple" rule is what a programmer/engineer should live by.
> good argument to be made that early exits, other than perhaps constraint checking, make control flow harder to reason about
I'll fight to the death for better flow with early exits it is much easier flow with less indentation. There are times where you do need to go about it differently but most do not.
Same way you evolve out of one liners.
Same way comments are extra weight that should only be in public or algorithm/need to know areas.
Same way braces go on the end of the method/class name to reduce LOC.
Same way you move on from heavy OO to dicts/lists.
Same way you go more composition instead of inheritance.
Same way while/do/while usually fades away, and if needed exit conditions.
Same way you move on from single condition bracket-less ifs. (debatable but more merge friendly and OP hasn't yet)
Same way you get joy deleting large swaths of code.
and many others on and on.
Usually these come from hours of writing/maintaining code and styles that lead to bugs.