FWIW, I was always extremely skeptical of this argument. Case in point, the most popular language, Python, is profoundly weird and looks nothing like any other popular language.
I wish Rust didn’t use C++’s <> for generics and :: for namespaces (which are needed in C++ to fit C syntax), and instead did a from-first-principles syntax there.
FWIW, I was always extremely skeptical of this argument. Case in point, the most popular language, Python, is profoundly weird and looks nothing like any other popular language.
I disagree. Python definitely does not look like C, sure, but it is similar to Pascal or Basic. Python spent its strangeness budget in meaningful indentation and a few other constructs, but besides that almost every single feature came from another popular language. The keywords are all familiar: for, if, else, while, class, break, continue. Function calling convention is also very common.
I think we should be discriminating between mandatory weirdness and optional weirdness.
For example, python’s for has an else branch. This is really weird. But you can still write a for loop in python without it, and you won’t actually see it very often in the wild. It’s completely optional weirdness (that’s occasionally quite useful), and so it doesn’t stretch the weirdness budget much.
It looks similar to pseudocode you find in text books. Even if that’s not a “real” language it’s familiar to a lot of professors. While bootcamps mostly teach nodejs many university teachers reach for Python.
I also don’t mean to argue that any specific syntax is unambiguously the right choice, just that, you have to understand your audience, and work with their expectations. And if you want to spend some strangeness points on syntax, that’s totally fine.
I really like <> for generics and :: for namespaces, but it’s also a good thing that Rust diverged from C style syntax for declarations. There’s stuff I would tweak about Rust too.
the most popular language, Python
I also don’t think you can compare the way languages gained marketshare in the late 90s and early 2000s to today. The landscape is totally different.
A more modern example would be Go, it has a whole bunch of weird syntactic choices: alien import statements, trailing dot for chained method calls, , special syntax for channels, multiple returns, itoa, no while, := (which, by the time of Go release, was forgotten enough to be considered weird), capitalization to denote privacy, and, well, now parens for generics.
Given Python and Go, the model which looks more reasonable to me is that you need syntax which is:
actually good
vaguely familiar
Aping specific syntactic constructs just for the sake of familiarity seems overvalued.
Now, given convergent evolution, the two strategies (picking existing syntax vs picking good syntax) will end up giving similar results, but I’d rather we have more syntactic innovation on the margin.
For ::, just get rid of separate namespaces for types, values, macros (a very dark corner of the language semantics most users don’t realize even exists) and use . for everything. Works fine in Zig.
Generics are harder to tackle. My choice would be to spell generics as Vec[i32] and indexing as xs.[92], but, luckily for everyone, I am not a language designer :D
The use of dot is interesting because in x.y, y is an offset from the start of the x struct, so it makes sense that x.[y] would also be an offset from the start of the array. It also gives you an obvious syntax to do dynamic look ups, like x.[attr], instead of Python’s getattr(x, attr). I don’t like how a lot of languages use brackets for map look ups, because a map look up isn’t a primitive operation like an offset dereference. A map lookup should be x.get(key).
In languages that does operator overloading, [ ] may not do a “primitive operation” either.
I’m not decided on whether to like or not map(key) syntax, but I believe Rich Hickey had a presentation where he claimed that maps are the most fundamental functions, and I really liked the elegance of that idea.
fwiw I also think this is the most elegant syntax, and not so exotic that it should really spend the “strangeness budget.” Not that I think Rust should change basic syntax at this phase in its development.
My choice would be to spell generics as Vec[i32] and indexing as xs.[92], but, luckily for everyone, I am not a language designer :D
I believe F# just got rid of the dot in their accessor syntax a few years ago. (Link)
Personally, I like the way that Scala does it:
generics use square brackets (Vec[i32])
objects can have an apply method
calling an object (e.g., obj(x)) is transformed into an apply call
collections use the apply method instead of square brackets (e.g., array(0) gets the first element of array)
I think closures are implemented as objects that have an apply
apply can have different function arguments in different classes. I think the typing works out because it uses path dependent types. Seems pretty elegant, but might require an unusual type system. (EDIT: it probably works out because it’s purely a syntactical transformation and not an interface/trait/whatever.)
I saw and chewed on this a fair bit and the more I think about it the more I appreciate it, with one caveat. In Python, for example, you could do the same thing with __call__ to make an arbitrary object callable and that works quite excellent for flexible API design: you make a function that requires a callable as a parameter and you don’t have to care if it’s a function or an object that implements __call__ or a collection or whatever. The downside, though, is that you lose get/set symmetry: with separate glyphs used for collections you can have foo[2] (get) and foo[2] = 3 (set).
Whether that’s a good thing or not… that’s up to taste. And some languages do have facilities to still make that work (eg lvalue magic in C++ or setf magic in Lisp).
The downside, though, is that you lose get/set symmetry: with separate glyphs used for collections you can have foo[2] (get) and foo[2] = 3 (set).
Scala also has some additional syntax sugar for updating a value using an update method as well as unapply and unapplySeq for destructuring/pattern matching.
The latter especially interesting because the one issue I have with pattern matching is that it doesn’t work well with abstraction. It requires you to have direct access to the internals of a data structure. Abstracting it allows you to hide your implementation and potentially provide use a less convenient, but more performant data structure. I was surprised to find out that scala does this out of the box. (Though, TBH, it might be a bit too flexible.)
EDIT:
Interesting. So it’s basically a function from type to type that can be inserted in patterns? TBH, the example was a bit hard to follow:
type Typ
data TypView = Unit
| Arrow Typ Typ
view :: Typ -> TypView
size (view -> Unit) = 1
size (view -> Arrow t1 t2) = size t1 + size t2
I can kind of see how size takes a Typ and returns one variant of TypViee, but I don’t really get how view does it given that it only looks like a type signature. Is there more mechanism in practice?
I’m not sure you even need the Vec[i32]/xs.[92] split- it should be fine just to use foo[bar] for both generics and indexing. The .[ has about as much practical use as :: vs ..
The hard part of using <> as delimiters is that they’re also used as infix operators, so it’s not immediately clear whether they need to be balanced. But both generics and indexing use [] as balanced delimiters!
The remaining trickiness is if you have ident[ and you want to know whether you should parse a type or an expression next. But this distinction is really only necessary in a Rust-like syntax because type-vs-expression contexts treat <> differently!
(And at that point you could almost get away with just using () for all 3 kinds of delimiters. The hard part of that would be distinguishing generics from parameter lists, because in function declarations the generics are typically optional.)
x[y](z) does it call a generic function x, or does it call the yth function pointer in an array of function pointers x?
That being said, I do agree that distinguishing syntactic categories of expressions, types and patterns is not useful (and, since const generics, a lie in Rust), and that it’s better to parse them all into identical trees, and disambiguate them latter, because that’s what you’ll end up doing anyway later in the language evolution. But that’s a bigger diff to the overall structure of Rust’s syntax.
The point is that the disambiguation here doesn’t change the syntax tree at all- it’s purely a type system distinction. This is much smaller of a diff than the kind of thing you’re talking about in the rest of your comment.
That is, we already know whether y is a type or expression independent of its context in the tree. The type checker isn’t working with an “ambiguous nested soup” structure, it’s simply []-applying x to y, and then ()-applying the result to z. In a vaguely typeclass/trait style, the [] operator is “overloaded” for both generic functions and arrays, while the () operator is “overloaded” for both generic and concrete functions.
I think we are on the same team age here: this is syntactically ambiguous, but semantically unambiguous. But so far rust tries really hard to drive these sorts of decisions through syntax. But that’s not necessary the best way to do this, you could just punt the decision to classify something as type or a value later, for when you are doing type inference.
Though, this case can be semantically ambiguous as well! You could have x type and y type, as well as x value and y in scope at the same time, because Rust has separate namespaces for them! Which is another argument for why not having namespaces might be better.
I’m really just trying to make the distinction that this is no more syntactically ambiguous than a call to a trait method, which Rust doesn’t try to drive through syntax.
But yes, I am also assuming a world without that kind of namespacing.
For namespacing, most languages seem to have landed on using dots as separators, e.g. Python, Java, Zig, Haskell…
About the <> syntax there’s some more variance, though note that rust’s use does fix the biggest issue C++’s template syntax has, which is ambiguity (as in, you might not be able to tell if the symbols are for templates or for comparison operators) by requiring them to be prefixed with ::, leading to the “turbofish operator”: a::<T>
In an Interview with Louis Pilfold on the Developer Voices podcast, he mentions how the earlier versions of Gleam had an unconventional Elm-inspired syntax. It was only until he switched to a more C-like syntax that the language started to get some adoption.
It’s amazing how much impact strange syntax has on the adoption of a language. It shouldn’t be the case, because syntax is much easier to grasp than semantics. But the reality is that a strange syntax will put off a lot of people from ever learning the semantics.
I think Rust did a good choice going for a familiar syntax and innovating with its semantics.
I think it has more to do how people are learning, a bit like people studying foreign languages with books and others just by hearing stuff.
As someone who has experienced this wtf barrier at the very start of a new programming language quite a few times… I don’t think it has anything to do with curly braces or C-like syntax, just that most people prefer familiarity and small steps, same as learning a language with a latin alphabet will be much easier for most people whose native language uses it.
I think everyone’s free to be put off by what they want.
You can’t make people just like something. You can’t even make them look at the thing you find great. This is a bit of a marketing thing, if you have a new language.
Maybe we can agree that people should be open to this. But people only have finite time and you have to pick your battles. I’m a huge fan of learning new languages and yet every new one feels like a chore these days. We’re apparently not getting the magical solution (or do we, with Rust? :P)
I wrote some SML in university. I could get by. But if I won’t be as comfortable as I am in C-like languages. That comfort is built up across decades of programming in C-like languages and doesn’t come from having taken one “programming languages” course whereby the focus was shared across SML, Java and Prolog.
Given the popularity of python, I don’t think it’s curly braces persay. It’s true that there are people who hate python’s whitespace based syntax, but a lot of people (most?) are pretty comfortable using python. Furthermore, the existence of python as one of the most popular languages would make it easier for people to learn other languages without curly braces. Plus, there are languages like Ruby and SQL which are also pretty popular.
I think what’s actually challenging about ML based syntax is the lack of parens around function arguments. While it’s aesthetically pleasing for some people (including myself), it can also make the syntax a bit soupy. This is similar to lisps where function names get packed in with the arguments. I think this is visually difficult for novices to parse, which makes adoption difficult.
I think an ML style syntax that used parens around function arguments would do fine.
Interestingly, Scala 3 is adding an optional python inspired syntax. I haven’t used it, but from what I’ve seen some uses seem nicer and others seem a lot worse. It seems to suffer from the “soupiness” problem and might be a good counter argument to my claim.
Familiar things are easier and take less time to learn. For something which doesn’t matter a ton, like syntax, doesn’t it therefore make pragmatic and rational sense to converge on some common conventions in our programming languages?
Well that’s not what we have done. I don’t think re-educating tens of millions of programmers and getting them familiar with a new set of extensions is the most rational and pragmatic use of time, but that’s admittedly a partially subjective value judgement.
APL has that, so do most of the languages in the Iverson family (J, k, BQN), but it seems that the people most likely to shudder from that syntax are people with a degree in computer science.
After working on enterprise projects long enough, I’ve learned that I don’t want that much expressiveness in the language I’m working in. Mostly because I don’t want to read the idioms my co-workers try to come up with.
I’m willing to give up expressiveness for myself as well in exchange, that only seems like a fair trade for not having to dig through someone’s macro heavy scala DSL again (or worse, a spot where they interleaved two DSLs).
Python isn’t necessarily the most expressive language around, but I’ve seen plenty of abominations written by colleagues. I think it’s more a matter of cultivating a mindset in the team that “we don’t do that kind of thing around here”. The CTO or team leads should ideally have a strong opinion about what’s acceptable and what isn’t, and enforce it. New hires should be suitably indoctrinated through code review.
Basically, the same way you’d cultivate a healthy testing or security mindset.
One might also say that they would like to converge on more simple conventions, so that a piece of code can be understood quite well in isolation, without having global understanding of everything.
These are fundamentally opposing goals, so it’s not as easy, I think.
It’s interesting… I’ve used a lot of languages over the years all over the spectrum from assembler to Perl and Python and PHP and Ruby to heavy duty C and C++ to Lisp and Haskell and other weird functional friends.
One of the things that has often tripped me up while evaluating whether Rust might be a good fit for a given project is that syntactically and ergonomically it feels a lot like C++ to me but conceptually is a lot different. I’m sure if I spent a lot of time with it that works flip around and C++ would start to feel like the odd duck but for me personally I wonder if it might be easier to learn if it looked less like C++ so that my brain didn’t automatically make incorrect assumptions about what a given piece of syntax would do.
One flip side of this is that if your new language has a a syntax that’s significantly different from mainstream, you’re going to have a smaller user base, but it’s going to be comprised of people who are open to trying new things and are more likely to have more unconventional ideas. Essentially you end up with a higher new-ideas density.
This is honestly a really weird take. “We” have collectively decided that caring about syntax is irrational and people ought not to. How did we decide that? Is there any empirical or logical evidence that syntax doesn’t matter? What would that even look like?
We like to think of ourselves as rational and pragmatic, but we freak out when we don’t have curly braces.
I can scan the structure of code in many, many languages, without expending any conscious mental effort at all.
It’s taken decades to reach the level of training where I can do that ‘on autopilot’, and it makes me feel dramatically more productive - so I feel its sudden absence very keenly.
I don’t know, we are just humans and no human is rational. I think it can be very harmful to think that we somehow are an exception.
Like, marketing works on everyone, it wouldn’t be a multi-billion dollar industry otherwise. (Also, that’s why the homo economicus is an inadequate model)
Also, I find this actually a very interesting part of PL design! You can create the best thing from a theoretical standpoint, but if it can’t be used in real life by real people, is it really good? Finding that delicate balance requires careful considerations.
I think that if you’re going to break from a standard approach you need a good reason. Even if the thing is just some silly syntactic measure like using <> instead of [] for generics, you need to explain why that’s worth me having to spend the extra 3 seconds feeling unfamiliar.
The other thing is that for junior developers these are not small things. The first time I looked at Rust was 2014 and I was confused enough without having additional new syntactic concepts to deal with. That’s not nothing.
Syntax is the user interface of a language. User interface design also matters for programming languages.
Controversial opinion: blaming lack of adoption on the prospective users is like blaming victims of Norman doors for not liking the door. The problem isn’t that the syntax is strange: it’s that it is bad. Blaming ‘strangeness’ serves only to protect the ego of the language developer.
But the reality is that a strange syntax will put off a lot of people from ever learning the semantics.
It is - let me use the bad word, just in this case - literally about learning a new language, and it can get frustrating. But, maybe, in certain instances, a good amount of innovation can make the frustration bearable.
Ideally, we should have many more programming languages trying out various different takes on what a programming language should be (or do?) and the aspect of getting wide adoption should be marginal. So, in conclusion, my take here is: let people be put off. If the language is worth it, they’ll eventually want to actually learn it.
As another controversial opinion: the correct takeaway from the famous ‘The Next 700 languages’ paper is that the semantics of all languages is ultimately all the same and trivial. It’s the syntax that matters!
A funny thought! An offshoot thought from yours: what if one viewed PLs through the lens of McLuhan’s “the medium is the message,” but considered the different syntax families (curly-brace/Algol, ML, homoiconic/lisp) as the distinct mediums? And then a for-coversation’s-sake future hypothetical McLuhan-esque PL archaeologist found that programmers used each syntax family (“medium”) in such predictably discrete ways from one to the next, that the capabilities and feature sets didn’t even matter: the way a PL looks is how it is used and therefore also its essence, even if the specialist is able to identify a rogue octopus in a tree from time to time.
Meanwhile, its likely moot, and we’ll simply converge on the LCD network-effect-syntax incumbent for the almighty sake of Adoption. The other mediums will have to wait their more civilized age, which will presumably arrive either on the other side of the great bigtech centralization or never.
It’s really sad, as the elm syntax is to me really pretty, easier to type and a bit closer to math. Maybe more exposure to different syntaxes in uni would help?
Well I’m mostly happy that the ideas behind functional languages are picked up slowly but surely. Sum types especially should really be commonplace.
This principle is understandable if you are making a language that you aim to reach massive adoption, like Rust.
But I worry it’s not a good principle for the average language designer. If anything, I am more likely to actually try a language if it doesn’t look and feel like ones I already know.
I recall the Adam Perlis quote:
A language that doesn’t affect the way you think about programming, is not worth knowing
If a language doesn’t invite me to solve problems differently, why wouldn’t I just stick with what I know?
From working on Objective-C, I was surprised at how much smaller the syntax strangeness budget is than the semantics strangeness budget. People preferred to move from Java to C++ than to Objective-C, even though it involved a much larger change to the object model and underlying semantics. The Smalltalk-like message syntax was too strange.
To me, the Perlis quote is about semantics, not syntax.
If a language doesn’t invite me to solve problems differently, why wouldn’t I just stick with what I know?
This is exactly the tension I’m trying to draw out with the post. You do need to do this, but if you do it too much, you won’t get adoption, if that matters to you. If it doesn’t, go wild!
I’d agree if it weren’t for what seems like evolutionary small steps and some examples where people switch from, say Ruby to Crystal for the speed, or from one lisp to another, or to another functional programming language (if we ignore what are dialects and if maybe they diverge enough to be counted).
Also I’d say there are good reasons to use modern C++ over C, and it does not really change the way I think.
I worry this kind of thinking will make C syntax and other baggage a tar pit, hard to escape from irrespective of the merits of any alternatives.
For me (C background, educated in C-brace languages, but competent at Haskell; 90% of my coding ends up being bash scripts anyway) I find Go’s reordering of declaration tokens far more confusing than leaping to a brace-less syntax like ruby or python or Haskell (all of which of course actually support braces, but nobody uses them)
I wonder if aiming for being intuitive is perhaps better than aiming for not being too strange?
Consider SQL: strange (unusual) compared to the languages around at the time it was introduced, but intuitive (easy to learn) given some prior exposure to discrete math.
Another example: dependently typed languages (e.g. Agda and Idris) are more strange than Haskell, but more intuitive (because type-level computations unify many of Haskell’s extensions in a coherent way).
I also think that being intutive is closely tied to having a nice and simple denotational semantics.
SQL has relational algebra (not precise, but good enough for intuition). Agda and Idris have Martin-Löf type theory (predicate logic). Rust, on the other hand, is an example of a language where the syntax was designed before the nice and simple semantics was found. I know people have tried to give a semantics for Rust, but doing it the other way around makes it difficult because: 1) your semantics is now constrained by your syntactic choices, and 2) the semantics can/should inform the design of the syntax but since you’ve already committed to a syntax it gets expensive to change.
maybe that confuses what I would disambiguate as grammar strangeness vs. syntax strangeness. SQL is, in most cases, just words and quotes and a semicolon. The allowed syntax is very concise and simple, and not strange at all.
That’s just how I’ve made it for now. I’m not a fantastic designer, so maybe it’s a bad design. I have at least one bug I still need to fix where the margins are real bad on mobile.
FWIW, I was always extremely skeptical of this argument. Case in point, the most popular language, Python, is profoundly weird and looks nothing like any other popular language.
I wish Rust didn’t use C++’s
<>
for generics and::
for namespaces (which are needed in C++ to fit C syntax), and instead did a from-first-principles syntax there.I disagree. Python definitely does not look like C, sure, but it is similar to Pascal or Basic. Python spent its strangeness budget in meaningful indentation and a few other constructs, but besides that almost every single feature came from another popular language. The keywords are all familiar: for, if, else, while, class, break, continue. Function calling convention is also very common.
I think we should be discriminating between mandatory weirdness and optional weirdness.
For example, python’s
for
has anelse
branch. This is really weird. But you can still write afor
loop in python without it, and you won’t actually see it very often in the wild. It’s completely optional weirdness (that’s occasionally quite useful), and so it doesn’t stretch the weirdness budget much.It looks similar to pseudocode you find in text books. Even if that’s not a “real” language it’s familiar to a lot of professors. While bootcamps mostly teach nodejs many university teachers reach for Python.
I also don’t mean to argue that any specific syntax is unambiguously the right choice, just that, you have to understand your audience, and work with their expectations. And if you want to spend some strangeness points on syntax, that’s totally fine.
I really like
<>
for generics and::
for namespaces, but it’s also a good thing that Rust diverged from C style syntax for declarations. There’s stuff I would tweak about Rust too.I also don’t think you can compare the way languages gained marketshare in the late 90s and early 2000s to today. The landscape is totally different.
A more modern example would be Go, it has a whole bunch of weird syntactic choices: alien import statements, trailing dot for chained method calls, , special syntax for channels, multiple returns, itoa, no while,
:=
(which, by the time of Go release, was forgotten enough to be considered weird), capitalization to denote privacy, and, well, now parens for generics.Given Python and Go, the model which looks more reasonable to me is that you need syntax which is:
Aping specific syntactic constructs just for the sake of familiarity seems overvalued.
Now, given convergent evolution, the two strategies (picking existing syntax vs picking good syntax) will end up giving similar results, but I’d rather we have more syntactic innovation on the margin.
That’s fair!
Ah, I’m curious: what’d be a better concrete syntax for generics and namespaces in your opinion?
For
::
, just get rid of separate namespaces for types, values, macros (a very dark corner of the language semantics most users don’t realize even exists) and use.
for everything. Works fine in Zig.Generics are harder to tackle. My choice would be to spell generics as
Vec[i32]
and indexing asxs.[92]
, but, luckily for everyone, I am not a language designer :DThe use of dot is interesting because in x.y, y is an offset from the start of the x struct, so it makes sense that
x.[y]
would also be an offset from the start of the array. It also gives you an obvious syntax to do dynamic look ups, likex.[attr]
, instead of Python’sgetattr(x, attr)
. I don’t like how a lot of languages use brackets for map look ups, because a map look up isn’t a primitive operation like an offset dereference. A map lookup should bex.get(key)
.In languages that does operator overloading, [ ] may not do a “primitive operation” either.
I’m not decided on whether to like or not
map(key)
syntax, but I believe Rich Hickey had a presentation where he claimed that maps are the most fundamental functions, and I really liked the elegance of that idea.Very nice observation. You should work with programming languages
fwiw I also think this is the most elegant syntax, and not so exotic that it should really spend the “strangeness budget.” Not that I think Rust should change basic syntax at this phase in its development.
I believe F# just got rid of the dot in their accessor syntax a few years ago. (Link)
Personally, I like the way that Scala does it:
Vec[i32]
)apply
methodobj(x)
) is transformed into anapply
callapply
method instead of square brackets (e.g.,array(0)
gets the first element ofarray
)apply
apply
can have different function arguments in different classes. I think the typing works out because it uses path dependent types. Seems pretty elegant, but might require an unusual type system. (EDIT: it probably works out because it’s purely a syntactical transformation and not an interface/trait/whatever.)I saw and chewed on this a fair bit and the more I think about it the more I appreciate it, with one caveat. In Python, for example, you could do the same thing with
__call__
to make an arbitrary object callable and that works quite excellent for flexible API design: you make a function that requires a callable as a parameter and you don’t have to care if it’s a function or an object that implements__call__
or a collection or whatever. The downside, though, is that you lose get/set symmetry: with separate glyphs used for collections you can havefoo[2]
(get) andfoo[2] = 3
(set).Whether that’s a good thing or not… that’s up to taste. And some languages do have facilities to still make that work (eg lvalue magic in C++ or setf magic in Lisp).
Scala also has some additional syntax sugar for updating a value using an
update
method as well asunapply
andunapplySeq
for destructuring/pattern matching.The latter especially interesting because the one issue I have with pattern matching is that it doesn’t work well with abstraction. It requires you to have direct access to the internals of a data structure. Abstracting it allows you to hide your implementation and potentially provide use a less convenient, but more performant data structure. I was surprised to find out that scala does this out of the box. (Though, TBH, it might be a bit too flexible.)
https://otfried.org/scala/apply.html
https://docs.scala-lang.org/tour/extractor-objects.html
Reminds me of view patterns in Haskell
I think that link is broken. This one seems to work: https://gitlab.haskell.org/ghc/ghc/-/wikis/view-patterns
EDIT: Interesting. So it’s basically a function from type to type that can be inserted in patterns? TBH, the example was a bit hard to follow:
I can kind of see how
size
takes aTyp
and returns one variant ofTypViee
, but I don’t really get howview
does it given that it only looks like a type signature. Is there more mechanism in practice?Oh that’s super cool! Thanks!
I’m not sure you even need the
Vec[i32]
/xs.[92]
split- it should be fine just to usefoo[bar]
for both generics and indexing. The.[
has about as much practical use as::
vs.
.The hard part of using
<>
as delimiters is that they’re also used as infix operators, so it’s not immediately clear whether they need to be balanced. But both generics and indexing use[]
as balanced delimiters!The remaining trickiness is if you have
ident[
and you want to know whether you should parse a type or an expression next. But this distinction is really only necessary in a Rust-like syntax because type-vs-expression contexts treat<>
differently!(And at that point you could almost get away with just using
()
for all 3 kinds of delimiters. The hard part of that would be distinguishing generics from parameter lists, because in function declarations the generics are typically optional.)x[y](z)
does it call a generic functionx
, or does it call they
th function pointer in an array of function pointersx
?That being said, I do agree that distinguishing syntactic categories of expressions, types and patterns is not useful (and, since const generics, a lie in Rust), and that it’s better to parse them all into identical trees, and disambiguate them latter, because that’s what you’ll end up doing anyway later in the language evolution. But that’s a bigger diff to the overall structure of Rust’s syntax.
The point is that the disambiguation here doesn’t change the syntax tree at all- it’s purely a type system distinction. This is much smaller of a diff than the kind of thing you’re talking about in the rest of your comment.
That is, we already know whether
y
is a type or expression independent of its context in the tree. The type checker isn’t working with an “ambiguous nested soup” structure, it’s simply[]
-applyingx
toy
, and then()
-applying the result toz
. In a vaguely typeclass/trait style, the[]
operator is “overloaded” for both generic functions and arrays, while the()
operator is “overloaded” for both generic and concrete functions.I think we are on the same team age here: this is syntactically ambiguous, but semantically unambiguous. But so far rust tries really hard to drive these sorts of decisions through syntax. But that’s not necessary the best way to do this, you could just punt the decision to classify something as type or a value later, for when you are doing type inference.
Though, this case can be semantically ambiguous as well! You could have
x
type andy
type, as well asx
value andy
in scope at the same time, because Rust has separate namespaces for them! Which is another argument for why not having namespaces might be better.I’m really just trying to make the distinction that this is no more syntactically ambiguous than a call to a trait method, which Rust doesn’t try to drive through syntax.
But yes, I am also assuming a world without that kind of namespacing.
For namespacing, most languages seem to have landed on using dots as separators, e.g. Python, Java, Zig, Haskell…
About the <> syntax there’s some more variance, though note that rust’s use does fix the biggest issue C++’s template syntax has, which is ambiguity (as in, you might not be able to tell if the symbols are for templates or for comparison operators) by requiring them to be prefixed with ::, leading to the “turbofish operator”: a::<T>
In an Interview with Louis Pilfold on the Developer Voices podcast, he mentions how the earlier versions of Gleam had an unconventional Elm-inspired syntax. It was only until he switched to a more C-like syntax that the language started to get some adoption.
It’s amazing how much impact strange syntax has on the adoption of a language. It shouldn’t be the case, because syntax is much easier to grasp than semantics. But the reality is that a strange syntax will put off a lot of people from ever learning the semantics.
I think Rust did a good choice going for a familiar syntax and innovating with its semantics.
I find this to be a somewhat sad reflection of our field.
We like to think of ourselves as rational and pragmatic, but we freak out when we don’t have curly braces.
(see also: Facebook creating reason)
I think it has more to do how people are learning, a bit like people studying foreign languages with books and others just by hearing stuff.
As someone who has experienced this wtf barrier at the very start of a new programming language quite a few times… I don’t think it has anything to do with curly braces or C-like syntax, just that most people prefer familiarity and small steps, same as learning a language with a latin alphabet will be much easier for most people whose native language uses it.
I’ll state it a bit more strongly - no one who has an undergrad degree in computing should be at all put off by ML-like syntax.
I think everyone’s free to be put off by what they want.
You can’t make people just like something. You can’t even make them look at the thing you find great. This is a bit of a marketing thing, if you have a new language.
Maybe we can agree that people should be open to this. But people only have finite time and you have to pick your battles. I’m a huge fan of learning new languages and yet every new one feels like a chore these days. We’re apparently not getting the magical solution (or do we, with Rust? :P)
I wrote some SML in university. I could get by. But if I won’t be as comfortable as I am in C-like languages. That comfort is built up across decades of programming in C-like languages and doesn’t come from having taken one “programming languages” course whereby the focus was shared across SML, Java and Prolog.
In other words, it’s all about familiarity, and not at all about any inherent issue with the syntax.
Well, yeah.
[Comment removed by author]
Given the popularity of python, I don’t think it’s curly braces persay. It’s true that there are people who hate python’s whitespace based syntax, but a lot of people (most?) are pretty comfortable using python. Furthermore, the existence of python as one of the most popular languages would make it easier for people to learn other languages without curly braces. Plus, there are languages like Ruby and SQL which are also pretty popular.
I think what’s actually challenging about ML based syntax is the lack of parens around function arguments. While it’s aesthetically pleasing for some people (including myself), it can also make the syntax a bit soupy. This is similar to lisps where function names get packed in with the arguments. I think this is visually difficult for novices to parse, which makes adoption difficult.
I think an ML style syntax that used parens around function arguments would do fine.
Interestingly, Scala 3 is adding an optional python inspired syntax. I haven’t used it, but from what I’ve seen some uses seem nicer and others seem a lot worse. It seems to suffer from the “soupiness” problem and might be a good counter argument to my claim.
Familiar things are easier and take less time to learn. For something which doesn’t matter a ton, like syntax, doesn’t it therefore make pragmatic and rational sense to converge on some common conventions in our programming languages?
I think it makes sense to converge on more concise, expressive conventions. So we have to scroll less, and can see more code on our screens.
Well that’s not what we have done. I don’t think re-educating tens of millions of programmers and getting them familiar with a new set of extensions is the most rational and pragmatic use of time, but that’s admittedly a partially subjective value judgement.
APL has that, so do most of the languages in the Iverson family (J, k, BQN), but it seems that the people most likely to shudder from that syntax are people with a degree in computer science.
APL syntax is terse at the cost of readibility.
Most APL programmers I know claim the syntax is in fact very readable, once you get the hang of it.
After working on enterprise projects long enough, I’ve learned that I don’t want that much expressiveness in the language I’m working in. Mostly because I don’t want to read the idioms my co-workers try to come up with.
I’m willing to give up expressiveness for myself as well in exchange, that only seems like a fair trade for not having to dig through someone’s macro heavy scala DSL again (or worse, a spot where they interleaved two DSLs).
Personal projects are another matter, of course.
Python isn’t necessarily the most expressive language around, but I’ve seen plenty of abominations written by colleagues. I think it’s more a matter of cultivating a mindset in the team that “we don’t do that kind of thing around here”. The CTO or team leads should ideally have a strong opinion about what’s acceptable and what isn’t, and enforce it. New hires should be suitably indoctrinated through code review.
Basically, the same way you’d cultivate a healthy testing or security mindset.
One might also say that they would like to converge on more simple conventions, so that a piece of code can be understood quite well in isolation, without having global understanding of everything.
These are fundamentally opposing goals, so it’s not as easy, I think.
It’s interesting… I’ve used a lot of languages over the years all over the spectrum from assembler to Perl and Python and PHP and Ruby to heavy duty C and C++ to Lisp and Haskell and other weird functional friends.
One of the things that has often tripped me up while evaluating whether Rust might be a good fit for a given project is that syntactically and ergonomically it feels a lot like C++ to me but conceptually is a lot different. I’m sure if I spent a lot of time with it that works flip around and C++ would start to feel like the odd duck but for me personally I wonder if it might be easier to learn if it looked less like C++ so that my brain didn’t automatically make incorrect assumptions about what a given piece of syntax would do.
One flip side of this is that if your new language has a a syntax that’s significantly different from mainstream, you’re going to have a smaller user base, but it’s going to be comprised of people who are open to trying new things and are more likely to have more unconventional ideas. Essentially you end up with a higher new-ideas density.
This is honestly a really weird take. “We” have collectively decided that caring about syntax is irrational and people ought not to. How did we decide that? Is there any empirical or logical evidence that syntax doesn’t matter? What would that even look like?
I can scan the structure of code in many, many languages, without expending any conscious mental effort at all.
It’s taken decades to reach the level of training where I can do that ‘on autopilot’, and it makes me feel dramatically more productive - so I feel its sudden absence very keenly.
I don’t know, we are just humans and no human is rational. I think it can be very harmful to think that we somehow are an exception.
Like, marketing works on everyone, it wouldn’t be a multi-billion dollar industry otherwise. (Also, that’s why the homo economicus is an inadequate model)
Also, I find this actually a very interesting part of PL design! You can create the best thing from a theoretical standpoint, but if it can’t be used in real life by real people, is it really good? Finding that delicate balance requires careful considerations.
I think that if you’re going to break from a standard approach you need a good reason. Even if the thing is just some silly syntactic measure like using
<>
instead of[]
for generics, you need to explain why that’s worth me having to spend the extra 3 seconds feeling unfamiliar.The other thing is that for junior developers these are not small things. The first time I looked at Rust was 2014 and I was confused enough without having additional new syntactic concepts to deal with. That’s not nothing.
Syntax is the user interface of a language. User interface design also matters for programming languages.
Controversial opinion: blaming lack of adoption on the prospective users is like blaming victims of Norman doors for not liking the door. The problem isn’t that the syntax is strange: it’s that it is bad. Blaming ‘strangeness’ serves only to protect the ego of the language developer.
Also see: how Elixir made Erlang more popular by making it “look familiar” to Rubyists (albeit with very different semantics)
It is - let me use the bad word, just in this case - literally about learning a new language, and it can get frustrating. But, maybe, in certain instances, a good amount of innovation can make the frustration bearable.
Ideally, we should have many more programming languages trying out various different takes on what a programming language should be (or do?) and the aspect of getting wide adoption should be marginal. So, in conclusion, my take here is: let people be put off. If the language is worth it, they’ll eventually want to actually learn it.
As another controversial opinion: the correct takeaway from the famous ‘The Next 700 languages’ paper is that the semantics of all languages is ultimately all the same and trivial. It’s the syntax that matters!
I’d hope not; the common semantics is awful.
A funny thought! An offshoot thought from yours: what if one viewed PLs through the lens of McLuhan’s “the medium is the message,” but considered the different syntax families (curly-brace/Algol, ML, homoiconic/lisp) as the distinct mediums? And then a for-coversation’s-sake future hypothetical McLuhan-esque PL archaeologist found that programmers used each syntax family (“medium”) in such predictably discrete ways from one to the next, that the capabilities and feature sets didn’t even matter: the way a PL looks is how it is used and therefore also its essence, even if the specialist is able to identify a rogue octopus in a tree from time to time.
Meanwhile, its likely moot, and we’ll simply converge on the LCD network-effect-syntax incumbent for the almighty sake of Adoption. The other mediums will have to wait their more civilized age, which will presumably arrive either on the other side of the great bigtech centralization or never.
It’s really sad, as the elm syntax is to me really pretty, easier to type and a bit closer to math. Maybe more exposure to different syntaxes in uni would help?
Well I’m mostly happy that the ideas behind functional languages are picked up slowly but surely. Sum types especially should really be commonplace.
This principle is understandable if you are making a language that you aim to reach massive adoption, like Rust.
But I worry it’s not a good principle for the average language designer. If anything, I am more likely to actually try a language if it doesn’t look and feel like ones I already know.
I recall the Adam Perlis quote:
If a language doesn’t invite me to solve problems differently, why wouldn’t I just stick with what I know?
From working on Objective-C, I was surprised at how much smaller the syntax strangeness budget is than the semantics strangeness budget. People preferred to move from Java to C++ than to Objective-C, even though it involved a much larger change to the object model and underlying semantics. The Smalltalk-like message syntax was too strange.
To me, the Perlis quote is about semantics, not syntax.
This is exactly the tension I’m trying to draw out with the post. You do need to do this, but if you do it too much, you won’t get adoption, if that matters to you. If it doesn’t, go wild!
I’d agree if it weren’t for what seems like evolutionary small steps and some examples where people switch from, say Ruby to Crystal for the speed, or from one lisp to another, or to another functional programming language (if we ignore what are dialects and if maybe they diverge enough to be counted).
Also I’d say there are good reasons to use modern C++ over C, and it does not really change the way I think.
I worry this kind of thinking will make C syntax and other baggage a tar pit, hard to escape from irrespective of the merits of any alternatives.
For me (C background, educated in C-brace languages, but competent at Haskell; 90% of my coding ends up being bash scripts anyway) I find Go’s reordering of declaration tokens far more confusing than leaping to a brace-less syntax like ruby or python or Haskell (all of which of course actually support braces, but nobody uses them)
I wonder if aiming for being intuitive is perhaps better than aiming for not being too strange?
Consider SQL: strange (unusual) compared to the languages around at the time it was introduced, but intuitive (easy to learn) given some prior exposure to discrete math.
Another example: dependently typed languages (e.g. Agda and Idris) are more strange than Haskell, but more intuitive (because type-level computations unify many of Haskell’s extensions in a coherent way).
I also think that being intutive is closely tied to having a nice and simple denotational semantics.
SQL has relational algebra (not precise, but good enough for intuition). Agda and Idris have Martin-Löf type theory (predicate logic). Rust, on the other hand, is an example of a language where the syntax was designed before the nice and simple semantics was found. I know people have tried to give a semantics for Rust, but doing it the other way around makes it difficult because: 1) your semantics is now constrained by your syntactic choices, and 2) the semantics can/should inform the design of the syntax but since you’ve already committed to a syntax it gets expensive to change.
maybe that confuses what I would disambiguate as grammar strangeness vs. syntax strangeness. SQL is, in most cases, just words and quotes and a semicolon. The allowed syntax is very concise and simple, and not strange at all.
[Comment removed by author]
Is the site rendering weirdly for anyone else? It’s completely glued to the left margin for me, on Firefox.
That’s just how I’ve made it for now. I’m not a fantastic designer, so maybe it’s a bad design. I have at least one bug I still need to fix where the margins are real bad on mobile.
I think that’s intentional!
[Comment removed by author]