Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

When I looked into Go I found it a bit surprising that someone had created a non-expression-based language as late as ~2009.

I have not familiarized myself with the arguments against expression-based design but as a naive individual contributor/end-user-of-languages, expressions seem like one of the few software engineering decisions that doesn't actually "depend," but rather, designing languages around expressions seems to be unequivocally superior.



I used to be skeptical about introducing complex expressions to C-syntax languages for a long time until I saw how well Kotlin handled `when`.

Now every time I use typescript or go I have trouble trying to express what I want to say because `when` and similar expressions are just such a convenient way to think about a problem.

In go that means I usually end up extracting that code into a separate function with a single large `switch` statement with every case containing a `return` statement.


Yeah expression based languages are a pleasure to work with. A lot of what was good about CoffeeScript was absorbed into ES6, but the expression oriented nature of it was never fully replicated in JS/TS.


More to the point, the comment in the code mentions the “combinatorial” explosion of conditions that have to be carefully maintained by fallible meat brains.

In most modern languages something like this could be implemented using composition with interfaces or traits. Especially in Rust it’s possible to write very robust code that has identical performance to the if-else spaghetti, but is proven correct by the compiler.

I’m on mobile, so it’s hard to read through the code, but I noticed one section that tries to find an existing volume to use, and if it can’t, then it will provision one instead.

This could be three classes that implement the same interface:

    class ExistingVolume : IVolumeAllocator
    class CreateVolume : IVolumeAllocator
    class SeqVolumeAllocator : IVolumeAllocator
The last class takes a list of IVolumeAllocator abstract types as its input during construction and will try them in sequence. It could find and then allocate, or find in many different places before giving up and allocating, or allocating from different pools trying them in order.

Far more flexible and robust than carefully commented if-else statements!

Similarly, there's a number of "feature gate" if-else statements adding to the complexity. Let's say the CreateVolume class has two variants, the original 'v1' and an experimental 'v2' version. Then you could construct a SeqVolumeAllocator thus:

    allocator = new SeqVolumeAllocator( 
        featureFlag  ? new CreateVolumeV2() : new CreateVolumeV1(),
        new ExistingVolume() );
And then you never have to worry about the feature flag breaking control flow or error handling somewhere in a bizarre way.

See the legendary Andrei Alexandrescu demonstrating about a similar design in his CppCon talk “std::allocator is to allocation what std::vector is to vexation”: https://youtu.be/LIb3L4vKZ7U


instead of "expression" you meant "exception", right?


In the statement/expression-oriented axis of languages, Go is a statement oriented language (like C, Pascal, Ada, lots of others). This is in contrast to expression oriented languages like the Lisp family, most, if not all, functional languages, Ruby, Smalltalk and some others.

Expressions produce a value, statements do not. That's the key distinction. In C, if statements do not produce a value. In Lisp, if expressions do. This changes where the expression/statement is able to be used and, consequently, how you might construct programs.


A simple example for anyone who might not appreciate why this can be so nice.

In languages where if is a statement (aka returns no value), you'd write code like

int value;

if(condition) { value = 5; } else { value = 10; }

Instead of just int value = if(condition) {5} else {10}

Some languages leave ifs as statements but add trinary as a way to get the same effect which is an acceptable workaround, but at least for me there are times I appreciate an if statement because it stands out more making it obvious what I'm doing.


It’s only an acceptable workaround in the case of two conditions, but you’re still out of luck if you have >2 branches and no match expression.


Not necessarily. In many Lisps you can bind the result of a condition like so.

  (let [thing (cond
                pred-1 form-1
                ...
                pred-n form-n)]
    (do something with thing))
This makes laying out React components in ClojureScript feel "natural" compared to JSX/TSX, where instead one nests ternaries or performs a handful of early returns. Both of these options negatively impact readability of code.


cond isn't a trinary operator it is more of a switch statement.

Trinary is expressly

let x = condition ? truevalue : falsevalue

You can do shenanigans by having something along the lines of

let x = condition1 ? truevalue1 : (condition2 ? truevalue2 : falsevalue)


cond is neither an operator nor a statement, it's an expression. This is a demonstration of a conditional expression handling multiple conditions, which GP wanted.

More importantly, pattern matching is not necessary here.


You misunderstood. They were talking specifically about languages that only have ternary operators as a way to do if-as-expression, and why they prefer languages with either real if-else if-else expressions or full switch/pattern matching as expression.


You can technically do some craziness with nested ternary operators but they look awful and if you write them you will regret it later.


True, but I would have to categorize that as an unacceptable workaround haha


How would this work if I need to update multiple variables?

  int value1 = 0;
  int value2 = 0;
  if (condition) {
    value1 = 8;
    value2 = 16;
  } else {
    value1 = 128;
    value2 = 256;
  }
Would I have to repeat the if expression twice?

  int value1 = if (condition) { 8 } else { 128 };
  int value2 = if (condition) { 16 } else { 256 };


Depends on the language a bit, but a common feature in these languages is the tuple. Using a tuple you would end up with something like:

let (value1, value2) = if (condition) { (8, 16) } else { (16, 256) }

Or else you’d just use some other sort of compound value like a struct or something. Tuple is just convenient for doing it on the fly.


hah we gave basically the same example on the same minute.

I love destructuring so much, I don't know if I'd want to use a language without it anymore.


It’s actually so painful to go back to languages without destructuring and pattern matching.


As someone who writes a fair bit of c# making switch and if's into expressions and adding Discriminated Unions (which they are actually working on) are my biggest "please give me this."

Plus side I dabble in f# which is so much more expressive.


Same for me in the Scala vs. Java world, it's hard once you get used to how awesome expressions over statements and algebraic data types/case enums/"discriminated unions" are. But I haven't done much C# (yet) myself, could you clarify for me: does C# have discriminated unions? I didn't think the language supported that (only F# has them)?


The c# team is working on a version of them they are calling Typed Unions, not guaranteed yet but there is an official proposal that I believe is 2 weeks old.

https://github.com/dotnet/csharplang/blob/main/proposals/Typ...


Cool, thanks for answering


Depends on the language. if you have destructuring you can do it all at once.

So like I believe you can do this in Rust (haven't written it in a while, I know it has destructuring of tuples)

let (a, b) = if (condition) { (1, "hello") } else { (3,"goodbye") }


.. save yourself an else :

int value1 = 128;

int value2 = 256;

if (condition) {

    value1 = 8;

    value2 = 16;

  }


expression as-in s-expression (ex: lisp)


I'm assuming you mean "non-exception". Apologies if I assume incorrectly.

In case I'm correct, this is from Andrew Gerrand, one of the creators of Go:

The reason we didn't include exceptions in Go is not because of expense. It's because exceptions thread an invisible second control flow through your programs making them less readable and harder to reason about.

In Go the code does what it says. The error is handled or it is not. You may find Go's error handling verbose, but a lot of programmers find this a great relief.

In short, we didn't include exceptions because we don't need them. Why add all that complexity for such contentious gains?

https://news.ycombinator.com/item?id=4159672


You appear to have misread "expression" as "exception"; this is completely unrelated. An expression-based language is one that lets you do `let blah = if foo then bar else baz`, for example.


I don't think he misread, because I also was puzzled. I had never heard of the term "expression" used in this way, and I imagine I'm not alone. I do greatly appreciate the clarification from you and jtsummers though. I knew of the distinction, but I didn't know of a term for it until today.


i honestly struggle with this because its a "i know when i see it" thing, ex. here, const boo = foo ? bar : baz suffices which brings in ~every language I know.

My poor attempt at a definition, covers it in practice in languages I'm familiar, but not in theory, I assume: a language where switch statements return a value


Go doesn’t have a ternary operator, you are supposed to write something like

    boo := bar
    if foo {
        boo = baz
    }
One of the many cases where Go’s designers decided they would ban something they disliked about C (in this case, complicated ternary operator chains), but thought Google programmers were too stupid to understand any idea from a more modern language than C, so didn’t add any replacement.

(I’m not exaggerating or being flippant: Google programmers being too stupid to understand modern programming languages has literally been cited as one of the main design goals of Go).


The second you add a tenary operator people are gonna nest them, but the same is true for if/switch/match expressions unfortunately. I don't think they meant stupid literally, it's more like KISS philosophy applied to language design for maintainablity/readability/code quality reasons. Google employs some of the smartest programmers in the world.


Nesting them is not so bad if the syntax makes it obvious what the precedence is, which isn't true of C, but is of Rust for example.

Anyway, complicated code should be avoided whenever possible, true, but banning the ternary operator (and similar constructs like match/switch statements as expressions) does nothing to make code simpler. It just forces you to transform

    let x = (some complicated nested expression);
into

    var x;
    // (some complicated nested tree of statements where `x` is set conditionally in several different places)


A language in which matching on a structure is not a statement but instead returns a value; the special case of matching on a boolean (this is often spelled `if`); one which doesn't have a `throw` statement (but instead models it as a generic function `Exception -> 'a`, for example); etc.

The `if` statement is just less ergonomic than the ternary operator, because statements don't compose as well as expressions do. A language which has a lazy ternary operator, and which lets you use an expression of type `unit` as a statement, does not require an `if` statement at all, because `if a then (b : unit) else (c : unit)` is identically `a ? b : c`. The converse is not true: you can't use `if` statements to mimic the ternary operator without explicitly setting up some state to mutate.


> I assume: a language where switch statements return a value

A language where everything is a value. Yes, a switch statement could be considered a value. More specifically - these are expressions that can be (but don't necessarily have to be) evaluated into a value. The most practical and introductory example of this is probably Ruby (called case: http://ruby-doc.com/docs/ProgrammingRuby/html/tut_expression...).

Python, JS, Ruby all have facilities to do this to varying extents. For a "true" expression-based language you will want to look at something like Clojure.

https://en.wikipedia.org/wiki/Expression_(computer_science)


Yeah you nailed the limitation. Switch type expression that returns a value is a pretty universal feature in expression based languages, often in the form of a pattern matching based expression.

Check out the ‘case’ statement in elixir for an example.

In languages that support it, it usually becomes an incredibly commonly used expression because it’s just so applicable and practical.




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

Search: