
I am still perplexed by how people judge a language purely by its declaration syntax, and will decide whether to use the language purely based on whether they like that aspect or not I am specifically…
Edit: Concrete syntax does matter a lot which is why I’ve had to write a follow-up article to explain why: Does Syntax Matter?
A programming language is not merely its syntax. Semantics actually exist, be that denotation semantics I’ve always found that focusing on the denotational semantics of a language is more important than focusing on the operational semantics because (for me at least) the operational semantics are “obvious” once the denotational semantics are decided upon., operational semantics, or algebraic semantics. The issue is that many inexperienced programmers don’t have this mental distinction and think all languages are mostly the same but with just differing “syntax”. Just wait until they are exposed to a functional programming language or a database language, or even find out that a spreadsheet is a language.
I wrote an article in 2018 On the Aesthetics of the Syntax of Declarations about the different families of declaration syntax, but it is still weird to me how people view things when deciding whether to use a language or not. To use my language Odin as an example, do people think this substantially changes the semantics if its declaration syntax had these different looks?
// Actual Odin
x: i32 = 123
y := 123 // inferred type
FOO :: "some constant"
bar :: proc() -> i32 {
return 123
}
// Qualifier focused
var x i32 = 123
var y = 123 // inferred type
const FOO = "some constant"
proc bar() -> i32 {
return 123
}
At best the difference here is going to be slightly more typing needed for var and const, and thus just becomes a question of ergonomics or “optimizing for typing” (which is never the bottleneck). I’d argue most of the compiler would effectively be the same for the latter approach, since it already has to disambiguate between the different kinds of declarations
Internally within the compiler, they are called entities. Some other compilers will use the term symbol, and other terms.. However each syntax family does have its own trade-offs which allow or prevent certain possibilities.
Syntax restricts the possibilities of what semantics are possible.
Semantics are where the rubber meets the road, certainly; but syntax determines how readable the code is for someone meeting it the first time.
Contrast an Algol-descendant like C, Pascal, Java, or even Python with a pure functional language like Haskell. In the former, control structure names are reserved words and control structures have a distinct syntax. In the latter, if you see `foo` in the body of a function definition you have no idea if it's a simple computation or some sophisticated and complex control structure just from what it looks like. The former provides more clues, which makes it easier to decipher at a glance. (Not knocking Haskell, here; it's an interesting language. But it's absolutely more challenging to read.)
To put it another way, syntax is the notation you use to think. Consider standard math notation. I could define my own idiosyncratic notation for standard algebra and calculus, and there might even be a worthwhile reason for me to do that. But newcomers are going to find it much harder to engage with my work.
> Contrast an Algol-descendant like C, Pascal, Java, or even Python with a pure functional language like Haskell. In the former, control structure names are reserved words and control structures have a distinct syntax. In the latter, if you see `foo` in the body of a function definition you have no idea if it's a simple computation or some sophisticated and complex control structure just from what it looks like. The former provides more clues, which makes it easier to decipher at a glance. (Not knocking Haskell, here; it's an interesting language. But it's absolutely more challenging to read.)
For what it's worth, Python has been moving away from this, taking advantage of a new parser that can implement "soft keywords" like 3.10's "match" statement (which I'm pretty sure was the first application).
Believe it or not, the motivation for this is to avoid reverse compatibility breaks. Infamously, making `async` a keyword broke TensorFlow, which was using it as an identifier name in some places (https://stackoverflow.com/questions/51337939).
In my own language design, there's a metaprogramming facility that lets you define new keywords and associated control structures, but all keywords are chosen from a specific reserved "namespace" to avoid conflicts with identifiers.
I don't have any real problem with words that are reserved absolutely and words that are reserved just in particular places. My point was more that in Algol-derived languages control structures look like control structures. And even in languages that implement `map()` and other higher-order functions, you can tell that it's a method/function call and that these things are being passed to it and those other things are not without going and looking up what `map()` does.
I absolutely agree about Haskell (and also OCaml). They both suffer from "word soup" due to their designers incorrectly thinking that removing "unnecessary" punctuation is a good idea, and Haskell especially suffers from "ooo this function could be an operator too!".
> In the latter, if you see `foo` in the body of a function definition you have no idea if it's a simple computation or some sophisticated and complex control structure just from what it looks like.
All control structures are reserved as keywords in Haskell and they're not extensible from within the language. In C I can't tell that an if(condition) isn't a function call or a macro without searching for additional syntactic cues, or readily knowing that an if is never a function. I generally operate on syntax highlighting, followed by knowing that an if is always a control structure, and never scan around for the following statement terminator or block to disambiguate the two.
I've found in general programmers greatly overestimate the unreadability they experience with the ISWIM family to be an objective property of the grammar. It's really just a matter of unfamiliarity. Firstly, I say this as a programmer who did not get started in the ML family and initially struggled with the languages. The truth of the matter is that they simply engage a different kind of mental posture and have different structural lines you're perceiving, this is generally true of all language families.
Pertinant to that last point and secondly, the sense of "well this is clearly less readable" isn't unique when going from the Algol family to the ISWIM family. The same thing happens in reverse, or across pretty much any language family boundary. For example: Prolog/Horn clauses are one of the least ambiguous syntax families (less so than even S-expressions IMO), and yet we find Elixir is greatly more popular than Erlang, and the most commonly cited preference reason has to deal with the syntax. Many will say that Erlang is unintuitive, confusing, strange, opaque, etc. and that it's hard to read and comprehend. It's just the same unfamiliarity at play. I've never programmed Ruby, I find Elixir to be borderline incomprehensible while Erlang is in the top 3 most readable and writable languages for me because I've spent a lot of time with horn clauses.
I think there's a general belief programmers have where once you learn how to program, you are doing so in a universal sense. Once you've mastered one language, the mental structures you've built up are the platonic forms of programming and computer science. But this is not actually the case. More problematically, it's propped up and reinforced when a programmer jumps between two very similar languages (semantically and/or syntactically) and while they do encounter some friction (learning to deal without garbage collection, list comprehensions, etc), it's actually nothing that fundamentally requires building up an entirely different intuitive model. This exists on a continuum in both semantics and syntax. My Erlang example indicates this, because semantically the language is nothing like Prolog, its differentiation from Elixir is purely syntactic.
There is no real universal intuition you can build up for programming. There is no point at which you've mastered some degree of fundamentals that you would ever be able to cross language family boundaries trivially. I've built up intuition for more formal language families than is possibly reasonable, and yet every time I encounter a new one I still have to pour a new foundation for myself. The only "skill" I've gotten from doing this ad nauseum is knowing at the outset that mastery of J does not mean I'd be able to get comfortable reading complex Forth code.
> There is no real universal intuition you can build up for programming. There is no point at which you've mastered some degree of fundamentals that you would ever be able to cross language family boundaries trivially.
I don't really agree with you on this, even though I agree with everything else here. Then again, I am an outlier where I've used ~40 programming languages in my career. There are a couple of language families (array languages like APL, exotics like BF) where I cannot read it because I've had no real opportunity to learn them, and there's a significant difference in being able to read a language and use a language (I can read, but not really use Haskell -- although I have shipped a couple of patches to small libraries).
I despair at the number of developers in the profession who understand only one or two programming languages…and badly at that.
(It's worth noting that I wholly disagree with the original post. 24 years ago I chose Ruby over Python because of syntax. Ruby appealed to me, Python didn't — purely on syntax. I never pretended that Python was less capable, only that its syntactic choices drove me away from choosing it as a primary language. I'm comfortable programming in Python now, but still prefer using most other languages to Python … although these days that has more to do with package management.)
> its differentiation from Elixir is purely syntactic.
Well, there's also standard library, Erlang one is very messy while Elixir one is very consistent (and pipe operator - `|>` - enforces order of arguments even in low-quality 3rd party code as well, making whole language more pleasant to work with. Same goes for utf8-binary string everywhere and other idiomatic conventions
> All control structures are reserved as keywords in Haskell and they're not extensible from within the language. In C I can't tell that an if(condition) isn't a function call or a macro without searching for additional syntactic cues, or readily knowing that an if is never a function. I generally operate on syntax highlighting, followed by knowing that an if is always a control structure, and never scan around for the following statement terminator or block to disambiguate the two.
Any Haskell function can serve as a control structure in the Algol sense, not so? As for `if(test)` that could indeed be a macro if the programmer’s a durned fool; but absent macros I don’t believe it can be a function call.
Mind you, I take your point about language familiarity; obviously people manage it.
Syntax is what keeps me away from Rust. I have tried many times to get into it over the years but I just don't want to look at the syntax. Even after learning all about it, I just can't get over it. I'm glad other people do fine with it but it's just not for me.
For this reason (coming from C++) I wished Swift were more popular because that syntax is much more familiar/friendly to me, while also having better memory safety and quality of life improvements that I like.
Wow, this is one of the most surprising comments I've ever read on HN!
Personally, I bucket C++ and Rust and Swift under "basically the same syntax." When I think about major syntax differences, I'm thinking about things like Python's significant indentation, Ruby's `do` and `end` instead of curly braces, Haskell's whitespace-based function calls, Lisp's paren placement, APL's symbols, etc.
Before today I would have assumed that anyone who was fine with C++ or Rust or Swift syntax would be fine with the other two, but TIL this point exists in the preference space!
There are a lot of important points of difference. Whether functions are introduced with an explicit keyword; how return types are marked; whether semicolons can be omitted at end of line; how types are named (and the overall system for describing algebraic types). Not to mention semantics around type inference (and whether it must be explicitly invoked with a `var` or `auto` etc.) and, again, algebraic types (what means of combination are possible?). And specifically with Rust you have the syntax required to make the borrow checker work. Oh, and then there are the implicit returns. Rust certainly didn't invent that (I seem to recall BASIC variants where you could assign to the current function name and then that value would be returned implicitly if control flow reached the end), but it reflects a major difference in philosophy.
... Which is really all to say: different people are focused on different details, at different levels.
> Whether functions are introduced with an explicit keyword; how return types are marked; whether semicolons can be omitted at end of line; how types are named (and the overall system for describing algebraic types).
Yes, these are all examples of things I always thought were generally considered small enough differences that nobody who was okay with how C++ or Rust or Swift did them would find the way one of the others did it a deal-breaker.
> ...Which is really all to say: different people are focused on different details, at different levels.
For sure!
> examples of things I always thought were generally considered small enough differences...
The thing is that they add up. Writing C or C++ is unpleasant enough for me that I've seriously thought about learning Rust just to have an alternative in that niche that actually has traction this time. (I wasn't under the impression that Swift — or Go, similarly — is intended to be quite as low-level. But maybe they are?)
Swift's syntax may look nice, but as soon as you run into "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions" you'll forget all of that. Hint: they are related.
It didn't have to be like that though... It hasn't always been this bad. Personally I think they've been ruining Swift since version 4.
It's particularly terrible in SwiftUI context nowadays but you can also make it chuck on something as simple as a .map(...)
Definitely second this sentiment. Rust just... Looks wrong. And for that reason alone I've never tried to get into it.
I understand exactly how shallow that makes me sound, and I'm not about to try and defend myself.
No need to defend yourself, I share this sentiment as well. If I'm going to spend time writing and reading a lot of code in a new learning language, I want my previous knowledge to be somewhat reusable.
For this reason I was able to get into Odin as opposed to Zig because of some similarities with Swift Syntax as well how easy it is to parse.
The less I need to rewire my brain to use xyz language, the greater the chance of me getting into it.
If my life depended on it, I could get over such a shallow reason to dismiss a language but fortunately it doesn't and that's why I write Swift rather than Rust.
When I was a kid learning BASIC, a lot of beginner examples in books used the (purely decorative) keyword LET for every assignment. Consequently I associate it with "coding like a baby who understands nothing" and still hate to write let to this day.
Do you have some examples of what you couldn't get along with? I know this is a lot to ask, but to me while I do write Rust and I don't write C++ or Swift in volume (only small examples) the syntax just doesn't feel that different really.
If you do like Swift you might want to just bite the bullet and embrace the Apple ecosystem. That would be my recommendation I think.
This resonate so much to my relationship with Rust. Also with Go. I'm having hard time learning Rust's advacend concepts because of its syntax.
Strangely enough I find Lisp's parentheses much more attractive.
The syntax of a language is the poetry form, it defines things like meter, scansion, rhyming scheme. Of course people are going to have strong aesthetic opinions on it, just as there are centuries of arguments in poetry over what form is best. You can make great programs in any language, just like you make beautiful poetry in almost every form. (Leaving an almost there for people that dislike Limericks, I suppose.) Language choice is one of the (sometimes too few) creative choices we can make in any project.
> Another option is to do something like automatic semicolon insertion (ASI) based on a set of rules. Unfortunately, a lot of people’s first experience with this kind of approach is JavaScript and its really poor implementation of it, which means people usually just write semicolons regardless to remove the possible mistakes.
Though the joke is that the largest ASI-related mistakes in JavaScript aren't solved by adding more semicolons, it's the places that the language adds semicolons you didn't expect that trip you up the worst. The single biggest mistake is adding a newline after the `return` keyword and before the return value accidentally making a `return undefined` rather than the return value.
In general JS is actually a lot closer to the Lua example than a lot of people want to believe. There's really only one ASI-related rule that needs to be remembered when dropping semicolons in JS (and it is a lot like that Lua rule of thumb), the Winky Frown rule: if a line starts with a frown it must wink. ;( ;[ ;`
(It has a silly name because it keeps it easy to remember.)