Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
False Cognates and Syntax: The Importance of Syntax in Programming Languages (txt.io)
96 points by Swizec on May 29, 2012 | hide | past | favorite | 47 comments


I disagree pretty strongly about the specifics of syntax being unimportant. If I had no experience and were speculating based on common sense, I would say "of course the details of the syntax don't matter". Hell, I might have even said that a year ago.

Then I started using CoffeeScript. Now, don't get me wrong; CoffeeScript has lots of little features that are very nice, like concise comprehensions and bound functions, which are not mere details. But if you took away all of that and left only the JavaScript with different syntax details, you would still have something much more pleasant to read and write than JS. -> function syntax, postfix conditionals, unless, on/off/yes/no, @, and "string #{interpolation}" are all tiny "syntax details", and I simply cannot overstress how much nicer they are than the JavaScript they trivially transform into.


For anyone who didn't know what if expressions look like in Erlang and had to look it up, apparently they use a series of "guard sequences" and the first one to be true determines the return value. If none of the sequences is true, it creates an error. There's no "else" keyword, so you have to use true as a substitute for it like this:

  is_greater_than(X, Y) ->
    if
        X>Y ->
            true;
        true -> % works as an 'else' branch
            false
    end
(Code from http://www.erlang.org/doc/reference_manual/expressions.html section 7.7)

See also: http://erlang.org/pipermail/erlang-questions/2009-January/04...


This seems like the same thing that lisp has had since forever in cond. In CL:

(defun is-greater-than (x y) (cond ((< x y) T) (T nil)))

As with the erlang example, I need a T test to get an else clause, although in this particular example, the result of the cond is nil if nothing matches anyway.


You can do something similar in any language that support booleans as keys for dictionaries/hashtables/maps/associative arrays/...

For instance in Lua:

    function abs(x)
        test = {
           [true]=-x,
           [x>0]=x,
           [x==0]=0}
        return test[true]
    end

    > print (abs(-3))
    3
To understand this code, think that `test[true]` is overwritten at initialization time by the last predicate to evaluate to True.

It's not the best way to do this, but maybe it'will help understand the above construct.


There's a difference, though. This example evaluates every "branch".


Make it even more ridiculous and wrap every branch in a lambda function, then do

    test[true]()


That would only evaluate the original [true] key, though, right?


No, `test[true]` will be reassigned to the given function each time the condition evaluates to true. Calling `test[true]()` will execute that given function.


Haskell has the exact same thing. It defines an alias for true called otherwise, so it would be

if x > y -> true; otherwise -> false; end

Using otherwise makes it a loss less ugly, and I guess fixes the false cognate.


I don't think that's valid Haskell. Examples of Haskell:

    case (x>y) of {True -> "Yup"; False -> "Nope");}
or

    if (x>y) then "Yup" else "Nope"
An example that uses otherwise would be

    fizzbuzz x | x `mod` 15 == 0 = "Fizzbuzz"
               | x `mod` 5  == 0 = "Buzz"
               | x `mod` 3  == 0 = "Fizz"
               | otherwise       = show x  -- This is x.to_string


A programming language designer shouldn’t have to make accommodations for users of whatever other languages are popular at the moment. If “return” is the natural name for a keyword, then regardless of its semantics in other languages, that is the best choice in the long term.

Technical note: even in languages where every expression has a value, it is possible to have an `if` form that doesn’t require an `else` value: just return nil. See, for example, Clojure: http://clojure.org/special_forms#if


return was pretty clearly chosen in haskell to make its monadic do syntax look even more like a regular procedural language. Names that better express what return really does would be something like "pack" or "lift" or "pure".

A typical example of a confusing return in haskell is this. Note that this always prints "hi".

  foo bar = do
    if bar > 5
       then plugh
       else do
         xyzzy
         return ()
    print "hi"
Here xyzzy and plugh return different types; since the if statement needs the same type on both if branches, return has to be used to return a dummy value of the same type as plugh (here assumed to be ()). But it only returns it to the outside of the if; haskell's return does not influence control flow.

You do get used to this pretty quickly, but there's also a tendency to move away from that style of haskell. I'd write the above more like this, using guards rather than the if, and using void to force both plugh and xyzzy to return the same type.

   foo bar = go >> print "hi"
     where
       go 
         | bar > 5 = void plugh
         | otherwise = void xyzzy


I wouldn't say that that's that surprising, when you remember that 'do' in Haskell is sort of analogous to defining and calling a function inline. If you keep that in mind, 'return ()' makes perfect sense.

Return does influence the control flow in the sense that it signals the end of the monad - it's just that Haskell allows you to define and call a monad ('function') inline, which is not idiomatic in most procedural languages.

It's not too far removed from the following Python code:

> (lambda x: 5)(5)

Which, naturally, returns '5'. Lisp, of course, treats lambdas similarly; however, writing a series of statements in Lisp (like progn) is not considered idiomatic/'good' Lisp, whereas writing monads in Haskell is absolutely necessary.


Statements only make sense when you have side-effects. Having an "if statement" in Haskell wouldn't make much sense.


What I find to be the most annoying false cognate is attribute accesses which are secretly method calls.

In [ x.y() for x in X], it's immediately obvious that x.y() is doing some work. Not so obvious in [ x.y for x in X ].

This is a very practical concern - in Django code (both mine and other people's), I see lots of unnecessary SQL queries all over the place because of this.

Of course, Ruby, Scala, etc, are not immune to this criticism.


The criticism does not apply to Scala because paren-less method calls are the norm, and because () was never intended to distinguish between field access and a method call. To assume x.y is a cheap operation in Scala would be to apply an assumption from other languages that is not valid in Scala. (Using parens for a zero-arg method in Scala communicates something else: by convention, x.y() is used if y has side effects, and x.y is used otherwise. I don't know how widely that convention is observed, though.)

Python is different because the style guide explicitly discourages [1] computationally expensive accessor methods (as well as accessor methods with side effects) specifically so that programmers can treat accessor methods the same way they would treat a data field. Assuming that x.y is a field access in Python is not supposed to lead to problems, and if it does, it's the fault of the class implementer and not the user.

[1] http://www.python.org/dev/peps/pep-0008/#designing-for-inher...


Scala (and likely Ruby) have a very specific reason for doing this.

http://en.wikipedia.org/wiki/Uniform_access_principle

It does put more onus on the API implementer to be careful about hiding non-trivial work, like you say.


> What I find to be the most annoying false cognate is attribute accesses which are secretly method calls.

The issue being that attributes can be accessed at all of course.

> Of course, Ruby, Scala, etc, are not immune to this criticism.

Well they are — or at least Ruby is — in that they don't allow attribute accesses from third-parties at all. Just consider that Python is the same (it is).

Hell, in Python `.` is already a method call. In fact it's a whole sequence of method calls.


> Not so obvious in [ x.y for x in X ]

Yes it is - that's always a method call: http://docs.python.org/reference/datamodel.html#invoking-des...

The problem is that you're unclear whether it's a mutating method or not, as well as whether it's an expensive operation. But that can be solved a number of ways - immutability and lazy evaluation would be one approach, though unfortunately neither Python nor Ruby enforces immutability, and both use absurdly eager evaluation.

(Regarding that last point, try doing [x.y() for x in foo][0] and you'll see that y is called for every x!)


Also terrible is expecting a property but accidentally evaluating a method as a boolean (which always evaluates true). Hard one to catch in debugging.


> In functional languages, however, where everything is an expression that has a value, the else block (or its equivalent) is mandatory. You simply cannot have an empty or missing else block. (Further, in languages with strict type systems, the else block has to have the same type as the if block, typically.)

The else clause is also mandatory in C, java, ... in if-expressions:

    int foo = <test> ? <then-clause> : <else-clause>;
They even have to have the same type!


AFAIK (as someone who studies programming languages, but not as a "real" Haskell developer) the metaphor of "return" with respect to monads in Haskell is that when you "bind" a function to a monad the result (the "return value" as you might say in a many other languages) must itself be wrapped in the monad (so returning really is putting something "into" the monad).

I personally do not feel like this is squinting and rotating your head 47 degrees: if you look at simple Haskell code examples the "return" function is used in the same way as it would at the end of any normal C function. The author of this article sees it that way, but that is just an opinion in not backed up in the argument by the "false cognates" premise.


It's not hard to learn the difference, but the article's point in calling it a "false cognate" is just to note that there _is_ an unexpected difference. I don't believe the author's use of the phrase is correct, though. Cognates don't have to mean the exact same thing in the different languages - they just have to be derived from the same source. So a "false cognate" actually would mean words that appear to have come from the same historical roots but don't really. What makes "return" in Haskell a (true) cognate is something you mentioned - its name was chosen intentionally as a _metaphor_ for return in other languages, though it's not the same thing.

On the other hand, I think the article's point that it's tricky is valid. The way it is usually used makes it appear that it is causing the procedure being defined to exit. If a user thinks that's what it's actually doing (which they tend to do when coming from other languages), they will try to write things like "when (i == 0) (return x)", which does not mean what they think it means.


And here we have an example of a similar linguistic problem- polysemy (one expression meaning multiple things) within a single language.

In philology / historical linguistics, "cognate" is a term relating strictly to etymological origins (in which context English and German "gift" really are true cognates). In typical middle/high-school foreign language classes, though, "cognate" is usually used in the related-but-rather-different sense of a word that ought to be easy to remember for the vocab test because it's sound and meaning are both similar to a word in your own language, and a false cognate is a word that looks or sounds familiar but means the wrong thing (in which context English and German "gift" would be considered "false cognates" for pedagogical purposes). Most true cognates in the technical sense would never be presented as cognates to a beginning foreign language class.


I have heard the term "false friend" suggested as an alternative for "false cognate" but I've never seen it used except when someone is trying to talk about both.


  do if condition
       then do putStrLn "bailing out early"
               return ()
       else putStrLn "carrying on"
     putStrLn "Launching missiles..."
This does not do what a C/Java/C# programmer would expect.

`return` in Haskell is a false cognate and it causes beginners difficulties. Some of them post to StackOverflow asking confused questions about it.

if-then-else in Haskell is also a false cognate (and there are formatting issues to trip up the unwary as well). It's actually the same as C's/Java's/etc ?: operator, so it's unfortunate the standard library doesn't contain something similar to that instead of adding if-then-else to the language.

Having said all that, I don't think being a false cognate should automatically be a disqualifying attribute. Haskell's new <> operator (a synonym for mappend) looks like Basic's/Pascal's not-equal-to operator. But I think few Haskellers come from that immediate background.


It really should be called something better, like pure. And Monads should be a sub typeclass of applicative.


Hmm. Why is he calling them false cognates? False cognates don't share a common origin whereas these words clearly do.

Anyway, for me, a good example of this phenomenon is when people come to C++ from a language like Java. In Java, you have to use "new" every time you want to create an object, so these people tend to go around putting "new" everywhere.


I get your point, however Java came after C++, its usage of "new" is however more consistant IMO. In that regard I don't see what either language designers could have done. This post, if I understand it correctly, was a plea to language designers.


The usage of "new" in Java is consistent according to the rules of Java and the usage of "new" in C++ is consistent according to the rules in C++. I don't think you can say that one is more consistent than the other. To a C++ programmer, the usage of "new" in Java, on first blush, seems highly inconsistent. Why use it for a String but not an int for example? The answer, of course, is that Strings in Java are objects and ints aren't. In C++, everything is an object.

"new" simply has a different meaning in Java even though the syntax is similar. If I were designing Java, I'd leave out the "new" keyword altogether. I see it as unnecessary to the semantics and confusing to C++ programmers, but that's just me. =)

I agree that the post was a plea to language designers, but such a plea is probably pretty useless and I didn't find it worth discussing. However, it can be fun to relate to his frustrations by sharing a story from your own experience.

Are you a Java developer? What's the most annoying behavior you see coming from converted C++ developers?


In C++ everything is an object? Since when? Or are you only calling the added "++" components "C++" but not the included C syntax?

As for me, I don't call myself a [insert language]-developer. I use whatever language suits best to design software. So far I have experience in many common procedural, OO and markup languages (C,C++,Java,Obj C,CUDA C,bash,python,VB,Matlab,Lotusscript,html+css) and I try to get experience in functional languages as soon as I can get some time for that. What's characteristic for me in Java is mainly its VM architecture. This makes it useful when you need the flexibility of a 3rd generation language for easily portable code (e.g. many business applications), however it has some disadvantages that have hindered its success for consumer applications. The main disadvantages IMO are the non-native feel of the GUI, maintenance and compatibility problems of the Java VM and Oracle's business strategy as of late. Java's syntax is just fine, I don't have any grudges about it. Knowing Obj C well, I know what ugly syntax looks like, however I still like that language as well (performance and frameworks are quite good, especially compared to other mobile frameworks).


Mathematics also has many false cognates. I agree that they make the subject harder than they should be for beginners by bringing in extra baggage. It takes more effort to dampen an old association and compartmentalize a new one than to form new ones.

A good example I can think of right now is Imaginary numbers and complex numbers. The legendary Gauss said they would have been better named Lateral Numbers.

But for programming languages avoiding false cognates is even harder as they not just naming things, but also deciding structure and must draw from the same pool of words as hundreds of other languages.


Not sure I understand your point. Gauss is credited for the term “complex number” as well.


A really good example the text doesn't mention is the awful C-like syntax in Javascript. You have curly braces which denote block scope in C-like languages, and yet in Javascript you have functional scope. Also, other things, like the "new" keyword. The syntax hides the true nature of the language.


A false cognate in Python is super(). It does something much different than super in other languages -- calls to super may call some class that is not a parent of the class where the call originates.


One problem is that there are only so many punctuation characters in ASCII, and in addition to that, there are only so many English words that are general enough to be used as keywords. Take Clojure as an example: assoc, do, and even let are basically false cognates according to the OP, since they differ from the same keywords in Common Lisp and Scheme, both a lot (assoc and do) and a little (let). But if Rich Hickey had picked other words, just for the sake of making it easier on beginners, I think the language would have suffered. This is just another example of optimizing for the experience of beginners, rather than for the experience of programmers who have taken the time to become proficient with the language.


> One problem is that there are only so many punctuation characters in ASCII

You can use sequences of punctuation characters, e.g. <:= and =:> for brackets, or =>> for an operator, or :*; for a separator. Or use punctuation characters not in ASCII: there's hundreds of them in the symbols blocks of Unicode.


This is certainly true, but I don't think it offers much practically. I would prefer to use a language that re-used or recycled keywords made up of letter than use a soup of punctuation just for the sake of being different.


"Stop making things more difficult in the long term just because some whiners are unwilling to learn the trivialities of a new syntax in the short."

The unwillingness of whiners is the number one reason for software to suck. Why does a C11 compiler still compile Ansi C? It is utter crap, and whiners are to blame.

edit: Conservativeness is the reason young developers don't learn C, why C# beat Java and why everyone will have to learn a new language every few years to keep on top of the curve.

If you see a flaw in a language you use, do you report it, contribute to a thread, or do you just learn to live with it?

(sorry I forgot to mention I had added 2 paragraphs)


The problem with using this kind of rhetoric is that it is also "utter crap" to require everyone to stop making progress on applications and high-level systems because some "whiners" are "unwilling" to put up with a couple warts in the languages they are using so that we can all instead go through tons of working code to update it to some new syntax or language standard; one might even, then, claim that it is this other set of whiners that "are to blame" for why a lot of libraries in some languages (I'm looking at you, Python) have never been able to get past puberty where they can start to crystalize into high-level perfection, and are instead trapped in a perpetual quagmire of "suck" while they scramble and fight to catch up to a shifting platform (and often just get thrown out and rewritten over and over and over again by new generations of whiners that insist that the old syntax is so unusable that they refuse to touch it anymore and would rather just start fresh).

(edit: The author of the comment I responded to added another two paragraphs.) C# beating Java is not something I think is as clear-cut as you seem to believe (from my vantage, C# is used by Windows developers, and Java is used by everyone else: the dividing line has little to do with syntax preferences and much more to do with the quality of the IDEs and the integration of the runtimes; before MS was legally required to stop distributing it, J++ was gaining ground), but to the extent to which it is true you have to remember that C# also is not making breaking language changes, and in fact didn't even make drastic changes to Java (which one might argue is the legacy that C# continued). In fact, C# was so compelling to a lot of us who adopted it early because it had such amazing backwards-compatibility in the form of interop with native C and C++ code with the ability to nearly natively interact with our existing COM objects.


If I have a wart, I go to the GP to have it iced, perhaps it hurts for a little while, but my skin is better off.

If a compiler removes a wart, why wouldn't you s/wart/scar/? Perhaps it's a bit of a pain every compiler release, but in the end isn't your software a bit more future-proof?

There is no need to stop making progress, this is why good software is open source and on github.

New generations of whiners are better judges of what is and what is not useable than old farts. The new generation has a glimpse of the future.

edit: I don't think native code interop is backwards compatibility, that would imply C/C++ is somehow a thing of the past. My point was that as long as C/C++ are being adapted to the future, they will never be legacy and retain relevance where their use is warranted. C# perhaps is not a winner in adoption, but it is gaining ground on linux and osx. My observation that C# beats Java is more in the syntax area.


This is a circular definition of "future-proof": if the compiler never changed, then the software would have been future-proof the day it was written, and the only reason it is not future-proof with the shifting compiler is due to the threat that the compiler will eventually stop supporting the feature. However, the assumption is generally and honestly should be (having been proved in the field after decades of engineering) that there will be many orders of magnitude more lines of code that exist outside of the compiler (which is but one program often maintained by a single team of people) than there ever will be in the compiler to maintain the old feature.

As for your comment about open-source... that doesn't help the problem of "all the developers are spending their time updating and re-testing working code rather than writing new code that relies on it", and specifically looking at GitHub makes no sense. Finally, the "old farts" you are now denigrating have more knowledge and experience of the kinds of failures you will run into, and so far in my experience the people rebuilding systems end up learning, the hard way, the same lessons that they could have just inherited (a rather visceral lesson as I've watched my friend Yehuda work on Bundler over the years, as he got to rediscover all of the things that APT solved over a decade ago).


I don't think it is circular, if the compiler never changes, a new compiler (for a new language) will come out. New generations might find the unchanged compiler to be obsolete (as many think of C). Thus its future might be mitigated. I don't believe introducing backwards incompatible changes is such a big thing that developers have significantly less time to develop new features. Look at Ruby 1.8->1.9 and Rails 2->3, yes it was a pain to upgrade, for a little while. But the advantages are obvious, and it was not _that_ much work. I was looking at github specifically because of their 'fork me' attitude, which I admire. I don't advocate at all we should develop software without looking at the past, if Yehuda didn't look at APT closely when he was working on bundler, then that was a missed opportunity I agree. Do you think Bundler should have somehow been an (compatible) improvement on APT instead of a separate software? I myself have never used APT the way I use bundler, and I have no idea if that would be possible.


I note that the writer has fallen into the trap of a false cognate - or, at least, a near-homonym - by writing "demure" when he/she meant "demur".


Any monad is an applicative functor, and any applicative functor provides 'pure', which does the same as return (lifting a value). The argument of the name 'return' in a discussion on syntax doesn't quite seem appropriate to me - it's an API/library problem, not one of syntax in the language. However, maybe I'm in the camp who doesn't believe too strongly in the importance of syntax though, as was mentioned in the opening paragraph...


Evaluate_some_stuff() in Prolog? Quaggy ground.


I had an idea for how to reduce errors when programming among many languages. You choose a language to 'get in the zone with' (in your profile you have already put all the languages you work with.) So, say you choose "C++". It would flash some code and what it's supposed to do, and you put in whether it's right (compiles and does what the comment says) or wrong (doesn't compile or doesn't do what the comment says).

The key is that the examples it flashes exactly try to cover the code mistakes/syntax errors (even things that don't compile!) that you would normally create in the first few minutes or hours - before you're 'in the zone' when coming from one of the other languages in the list. Things like leaving off a semicolon when coming from Python to C++. You can be an experienced C++ programmer, but after long hours of python, you need a period of adjustment. Isn't this the most dangerous time to code?

So if you put that you want to get in the zone with C++, but in your profile Python is listed, then some of the examples will be missing a semicolon while being indented properly. If you put a language that uses eq instead of ==, . instead of +, this is brought up a couple of times. All the things that separate languages - so that in the first few error-prone hours of transition you leave them out or forget them at times - are brought right to the front so that you can produce much higher-quality code in your 'target language' after 'zoning up.' What do you guys think?

Pedagogically, it may be better not to flash incorrect code, but instead make you write the code. But ask you to write code such that it explicitly tests something that may trip you up coming from one of your other languages.




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

Search: