1. 68
  1.  

  2. 25

    A small handful I disagree with:

    • Integer division: I take the position that any (T, T) arithmetic operator should also return T, which mandates integer division (or no division at all, which I think we can agree is undesirable) on integers. (T, U) arithmetic operators aren’t a good idea and should require explicit conversion of one of the arguments. The mistake in the anecdote is yours, not the computer’s, because very few general-purpose programming languages are suitable as calculators.
    • Increment and decrement: I like these operators. They only really cause problems when combined with assignment-as-expression, something I agree with wholeheartedly and have never seen used for good.
    • ! as not operator: never caused me issues. Syntax highlighting mitigates the issue, and programming is all about detailed reading.
    • braces and semicolons: Significant whitespace is a bad idea. A significant number of tools (like the entire Web) ignore and munge or arbitrarily modify it. Thus, you’re left needing textual delimiters; braces are compact, and braces and semicolons nicely conventional. (Incidentally, this subsumes allowing hyphen in identifiers; if whitespace is insignificant, you have to choose between allowing hyphen in identifiers and having it as an operator, and I’ll pick subtraction every time. I don’t otherwise disagree with that point, though.)

    And a few that bear reinforcement:

    • Textual inclusion: Bad idea, full stop.
    • Out parameters: It could be argued this is “just” an ergonomic issue, but it’s a serious one.
    • Silent errors: Yep. On the other hand, while Eevee calls out Java’s checked exceptions as a good example, they’re so catastrophically unergonomic that programmers consciously avoid or subvert them and they can loop back around to being ignored again.
    • Mandatory nullability: Oh god yes. This is my least favorite aspect of Java, a language I dislike in a wide variety of ways.
    • Assignment as expression: I’ve never seen code using this that shouldn’t be refactored (this is typically trivial and could probably be done mechanically) not to.
    • Blaming the programmer: This is not just a problem with C, either. It seems a lot of programming languages respond to common error cases by telling the programmer to be more disciplined, even though the fact those cases are common indicate this doesn’t work. One of the things I like about Rust is that the language developers specifically design in favor of automatic safety over requiring “discipline”.
    1. 4

      So, one part with your integer division bit: this really depends if 2 as a literal has to be an integer or could be a float. With Rust, we force you to write 2.0 to get a floating point literal, and every once in a while, someone gets Real Mad that they have to type the extra. (Technically you can even write 2., but I don’t like that at all.)

      1. 1

        Dealing with floating point numbers in general can lead to people getting Real Mad.

        Is this historical OCaml influence? I’ve seen people upset there about having to opt-in to a lossy numeric representation, but I appreciate needing to be explicit.

        1. 1

          Before my time, I’m not sure.

        2. 1

          Could be worse. Could be they have to write 2 for an int, 2.0 for a double, and 2.0f for a float. Inconveniences abound.

        3. 3

          Integer division is clearly useful, and I don’t think that it’s unreasonable to have / represent integer division on int. In fact, it’s a valuable first lesson: the meaning of / is different on int versus float.

          My complaint would be how negatives are handled. I would prefer that % (mod) return a positive representative at all times and / (div) round down, e.g.

          -1 / 7 = -1
          -1 % 7 =  6
          

          You still have the mod/div law, d * (n / d) + (n % d) == n, but you also have n / d == 1 + (n - d) % d, which is broken with the round-toward-zero behavior. I don’t see what the value in the round-toward-zero behavior is.

          Assignment-as-expression is awful for modern code. It makes code slightly more concise but a lot harder to read, and far more error prone. It also lets the if (x = 2) mistake, which ought to be a compiler error, get through. It’s probably a remnant of a time when storage was so expensive that even source code storage needed to be considered, but these days its value is negative.

          I say this because, in 2016, you pretty only use C for a certain type of highly careful system programming where you want to know exactly what is going on (and can’t afford, say, the nondeterminism that comes with GC and a powerful runtime). In this case, you want code to be verbose and boring, not clever and succinct but harder to follow.

          1. 4

            It also lets the if (x = 2) mistake, which ought to be a compiler error, get through.

            FWIW, Clang and GCC both emit a warning for this, with -Wparentheses, which is also enabled by -Wall. To silence the warning, you can write it more explicitly as if ((x = 2)) instead.

          2. [Comment removed by author]

            1. 2

              This seems like an odd rationale. Even the smallest C compiler should be smart enough to use such opcodes when faced with a += 1 or -= 1.

            2. 1

              Blaming the programmer: This is not just a problem with C, either. It seems a lot of programming languages respond to common error cases by telling the programmer to be more disciplined, even though the fact those cases are common indicate this doesn’t work. One of the things I like about Rust is that the language developers specifically design in favor of automatic safety over requiring “discipline”.

              And still, Rust has quite some “you need to know that”-corners.

              It can’t be avoided, but a “blaming the programmer” stance doesn’t help - we need to promote those flaws together with the strengths.

            3. 6

              The do-while loop is another construct that most languages that copy C have, while other languages chose to avoid it. It has its uses, but sometimes it annoys me is that the condition in the while is outside the scope of the loop, so you cannot test variables local to the scope.

              do {
                  /* ... */
                  int res = some_function();
                  /* Will not work, res is not in scope below */
              } while (res != -1);
              
              1. 10

                Recall that C originally required variables to be declared at the beginning of function, so that was not an issue until they relaxed.

                1. 5

                  Nitpick: At least by C90 it was not beginning of a function but they had to be the first thing in a block. So you could add set of { ... } in the middle of a function and define variables in it.

                  1. 2

                    Wouldn’t their scope be limited to that block?

                    1. 4

                      Yes they are, but it’s still not the beginning of a function which was the nitpick.

                  2. 1

                    A valid point, perhaps the scope could have been relaxed a little in this case as well. There might be valid use-cases for shadowing a variable but still checking it in the condition, to me it seems rather misleading though:

                    char *p = NULL;
                    do {
                        char *p = some_function();
                    } while (p != NULL);
                    
                    1. 1

                      Actually, now that I think about it, the continue statement might explain why the do loop still needs two scopes.

                2. 6

                  Nice informative piece.

                  I don’t agree with “Braces and semicolons” section. The main problem is that we are not living in perfect world. Code may be presented with indentation messed up or it coping it may mess it up. It’s similar problem to file sharing. Except for data of the file itself you can’t really count on anything else. Name may be messed up and almost surely you will not get any other metadata at all (permissions, xattrs). Sadly indentation is such a delicate metadata. Braces then allow to bring the peace back. Only solution left is to have 1st party authoritative formatter like “go fmt”.

                  I know that string concatenation is outside of C-likeness (maybe except reimplementing C string’s library), but it would be nice thing to cover. I would prefer for languages to have separate string concatenation operator.

                  There is maybe small case of vector and matrix types. C has (for some definition of “has”) intrinsics. I would like to have a language with separate vector types that would work with proper operators. They could then map to SIMD instructions native for target CPU. That would also lessen (or eradicate?) the need for overloaded operators. The closest thing that I am aware of is the Google’s PNaCl C - https://developer.chrome.com/native-client/reference/pnacl-c-cpp-language-support#portable-simd-vectors

                  I was many times confused with summary at the end of each section. I understand that it was for humor, but nevertheless it could be more clear.

                  Example: In section “Switch with default fallthrough” - “Follows through:” means that it “falls through” by default like in C or not?

                  1. 5

                    I would like to have a language with separate vector types that would work with proper operators.

                    Note that this only papers over a very small subset of available SIMD operations.

                    The closest thing that I am aware of is the Google’s PNaCl C

                    Other places have them too. Rust’s SIMD crate, SIMD.js, and D.

                    1. 4

                      My main qualms with haskell come from the reliance on indentation. My favorite style is lisp, which is really magical to use with the proper tools

                      1. 2

                        Haskell lets you drop out of indentation and use braces and semicolons, if you prefer them.

                        1. 3

                          Yes, but most haskell code out there uses indentation, and I’m not sure I want to have my code formatted differently than the rest.

                    2. 3

                      not one of their better efforts. the discussion of out params and single return is (along with being one of the more error-strewn sections) focuses entirely on surface syntax and elides call-by convention in a way that doesn’t actually touch on what the important issues here are, design-wise.

                      1. 3

                        Yeah, most discussions of multiple returns miss the point: that they are much more elegantly expressed using pattern matching, and that really the only excuse for having multiple returns in your language is that it was invented before pattern matching was widespread.

                        That said, most of the other sections were pretty on-point. Her position on “no hyphens in identifiers” is something I’ve long considered obvious but never seen stated anywhere like this.

                      2. 2

                        Python 2 is mostly strong, but that whole interchangeable bytes/text thing sure caused some ruckus.

                        Bonus points for disagreeing with Zed.

                        1. 2

                          Obligatory don’t copy that floppy

                          But on a serious note, I am not sure what to think of this on the whole. It’s true that many of the features outlined have considerable advantages, over C, but how far can you stray from familiar and still be relevant? And, at what cost?

                          Rust was deemed too complex for a long time, and I still hear that argument. Haskell as well. These languages have stronger communities now (and growing!), but they require significant time investments to gain any value out of them.

                          Contrast this with something like PHP (or JavaScript), which is super familiar, and you can see why soooo much of the web was/is powered by it in the early 2000s/now. But, I guess things, like always, are on a spectrum of tolerance and value.

                          1. 2

                            It would be interesting to hear about these things from someone who started without C or C-like languages. I’m sure the bias would be different.

                            I had the opportunity once to conduct an experiment when Git was fairly new. It was still trendy (and sometimes seems to be even today) to complain about how complicated it is, especially compared to SVN. So it told this new intern, who knew nothing about version control, that this is a branch, this is a commit and this is how you push it. This is how you merge, this is a rebase and this is how conflicts are resolved.

                            She got it immediately.

                          2. 2

                            I think this is a really good list of things that C does because it’s close to the metal, and doesn’t want to hold your hand or get in your way. It’s less about bashing C and more about bashing languages that had some short-sighted design and used C as a template. But, can you blame them for designing around C? It’s basically the Lingua Franca of programming, and taking the “when in doubt, emulate C” route when designing a language was probably the best option at the time of a lot of these languages' inception (C# and Java especially).

                            1. 10

                              Oh, blame is beside the point. Blame is very rarely a useful concept in general. Certainly in this case it’s easy to understand all sides, and concrete advice on how to do better is a lot more useful.

                              Which is what the article is, and I like it a lot for that. :)

                              1. 2

                                Yes. Blame aside, you can certainly fault language designers for needlessly* emulating C, and I think the article does that well, in a level-headed way, without throwing unnecessary shade C’s way.

                                * Whether Java and C# needed to emulate C is open for discussion. Their designers felt so. Maybe they were right.

                              2. 2

                                Things like not having multiple returns arguably takes you further from the machine. From a calling convention perspective, you can allocate multiple return registers just as simply as arguments, and x64’s ccall does exactly this. Exceptions too: stack unwinding can be cheap, but implementing it in the context of C’s manual memory management means you’ll either need to touch all the intermediate stack frames to run destructors, or you do what C currently does and thread “exceptional” flow manually.

                              3. 1

                                No mention of Haskell precedence or type declarations? ?

                                1. 1

                                  In the “Nulls” section:

                                  Nothing doing: C#, D, Go, Java, Nim (non-nullable types are opt in), R.

                                  As far as I know, nullable types in C# are always opt-in. I can’t do int number = null, however, I can do int? number = null. I’m not sure if that’s what the author’s saying or not, some of their descriptions for how languages behave are hard to understand, and I’m not sure I understand what is meant by “Nothing doing” in this context.

                                  Also, under “No hyphens in identifiers”:

                                  Nicely-designed: COBOL

                                  Never thought I’d see the day.

                                  1. 4

                                    Nullability in c# is opt-in for builtin non-reference types and structs. You cannot create a non-nullable reference type but you can create a optionally-nullable struct type.