Hacker News new | past | comments | ask | show | jobs | submit login
JEP 325: Switch Expressions (java.net)
111 points by jcdavis on Jan 19, 2018 | hide | past | favorite | 75 comments



I must have gone full lisp weenie, because I find it ridiculous when remembering the days of being a happy java dev, waiting years for someone to include a syntactic change in the spec, all whilst crossing my fingers that they will include my use case.

Clojure macros have spoiled me with cond, condp and case plus whatever else I or the community come up with. And my half thought out changes are nicely isolated to a single project or library instead of baked into the language for eternity.


Generally speaking, when a language developer adds really useful, special syntax to a language — that language users cannot define themselves — it seems like a good idea to stop, and consider whether it makes sense to restrict the usefulness of this concept to just this special case.

I.e. the Java boolean type, which can only be true or false (and not default). Could it be that there are other valuable uses of this ability, instead of using enums where you always have to handle the default case?

Can it really be the case that true/false is the only case where not having to worry about default is extremely helpful?


I'm sorry, but I'm a little confused by your comment about default cases in reply to mine about syntax extensions being in the hands of language designers vs everyone.

I am also a little unsure what you mean by not worrying about default. Even polymorphism has to deal with this, if you call a polymorphic method on an object that doesn't implement that interface you'll get at best a compile problem, at worst a cast exception. This is why some languages provide support for a "default" or "method not implemented" hook in polymorphism.

If you are arguing that polymorphism is a better way to build systems than switch/case statements then no argument here. Open constructs like polymorphism are far better for growing and evolving large systems over time.

That doesn't mean we should exclude a nice way to say "i've got a variable that i want to translate to some other value in a few different ways".

My point was only that it just seems so silly to me that everyone is discussing whether this syntax is "best" or how it "doesn't do X, Y or Z", because I remember doing that too before Rich Hickey (angelic choir noises) showed me that actually you don't have to put up with that shit. Just use a language that puts syntax extension in the hands of it's community and let them figure it out without burdening the core language with a thousand little tradeoffs for the 90% use case.


> Just use a language that puts syntax extension in the hands of it's community and let them figure it out without burdening the core language with a thousand little tradeoffs for the 90% use case.

The downside of that is that some Lisp projects can only be understood by the original authors, during the time they were actually active on it.

I have seen a few that went DSL crazy with macros.

Scalaz is another "good" example of that.


Enums and sealed types are mentioned in tfa.


My view is that this shows again that worse is better. The old form is better because it is awkward and it can only be improved by turning it into a function.

Within a function, each switch branch can be a return. Then you get the benefits:

* Make the original function shorter.

* Give a descriptive name to what you're computing.

* Make the computation reusable.

It's what I find lacking with lambdas: it makes it too easy to forego good refactoring because it makes too easy to write one-off code that in reality will be rewritten many times in different places.


No, worse is worse. Pattern matching is much, much cleaner than a switch with the branches extracted in separate functions. I don’t think that anyone using languages that support proper pattern matching can disagree.

If someone is writing the same lambda n times instead of extracting it in a common method the problem is not the lambda function, but the developer. Closures are probably the best feature in Java and they have been delayed enough.


> Pattern matching is much, much cleaner than a switch with the branches extracted in separate functions

I don't think that's what is being suggested. I think the suggestion is to move the entire switch statement to a separate function, use 'return' in the switch cases, and have the caller assign the variable once, from the return value of the function.


Re: lambdas - if one-off code is being rewritten so many times maybe it's small enough that it doesn't warrant it's own function? I've never run into that issue and love to use lambdas where possible -- in fact the use of streams api has frequently been a source of simplifying existing boilerplate loop-type code. Any time lambda code seems to be repetitive and large enough to warrant refactoring that's usually what happens -> throw it into a method and call from the lambda (or just use method reference syntax if possible).


Interesting to see Java copy modern languages good ideas.

The footgun of 'break' in switch statements has always bothered me, I strongly prefer the kotlin/swift way of doing this.


> modern languages good ideas.

Lisp introduced 'cond' when? Also, pattern matching in it's slightly more advanced form is as old as ML.

It's good to see languages finally admitting that expressions > statements. Still waiting for them to implement proper tail recursion though so I can replace statement based loops with expression based ones.


ML and Lisp are both more modern than Java. Java was archaic, at least partially as an attempt to win over C++ users.


Thank “C” for the broken case statement requiring “break”.

Pascal, and presumably Algol, had no break - following the guard condition was “a statement” (either just a single one, or an explicitly bounded block / compound statement)

Nice to see plans to act something like Lisp’s “(COND ...)” or Ruby’s case statement (et al).


I can somewhat forgive C for picking a syntax that turned out to be a bad idea. (Even if possibly some prior programming language made a better choice.) Java, on the other hand, copied that syntax knowing exactly how many bugs this would result in, for the questionable benefit that Java superficially looks a little bit more like C.


Even then there was a better way to do it. E.g. C# copied the C switch/case/break also, but with a caveat that all branches had to terminate without falling through. So you have to put a break there (or a return, or a goto, or a throw - basically any statement for which the compiler knows the control flow won't get past it).

And in rare cases where you do want fallthrough, they added the ability to "goto case" from the body of one case label to another. Which is kinda nice, because it follows the original C design in treating case labels as, well, labels.


That questionable benefit is what makes many developers adopt or not a language, and the main reason why we had to endure for so long the fashion of curly braces languages.

Thankfully this is changing now, with Pascal and ML syntax becoming again fashionable.


They've been doing that for a long time. Just... slowly.


Slowly is an euphemism.. Lambda Expressions were introduced in c# in 2007, Anonymous delegates exist in c# since 2005, Java introduced closures in 2014...


I didn't mean slowly as a euphemism. It's just slowly. They're extremely methodical about changes to the core language. And, yeah, it's slower than many would like, but they do it right and the language remains stable and reliable.

Anyway, Lambda expressions have been around in, like, Lisp for at least 40 years and in mathematics for, what, 80? So everyone's slow by that account.


I emphasised the “slowly” because c# has the reputation of being “a bad copy of Java”, while apparently the opposite is true given how long it took Java to copy closures and to produce a bad copy of LINQ.


They want to prevent another half-baked implementation of a feature like they did with generics.

C# is much faster and has much more features, but because of that it's also a bit of a mess sometimes. For example regarding closures/anonymous functions it has delegates, lambdas and anonymous (inner) classes which all have overlap but aren't fully the same.

Java only has anonymous inner classes, and a convenient syntax and good support for anonymous inner classes with only 1 method (which they call lamdbas)


Lambdas are a bit more than that, because on standard JVMs they are implemented with specific bytecodes (via an invokedynamic specialization).

The only variant of Java that desugars lambas into anonymous inner classes is Android, because Google.


> Java introduced closures in 2014

Java has had closures since 1997 hasn’t it? Don’t anonymous classes close over local state? Just the syntax was verbose.

Do you mean the lambda syntax? Well that isn’t thue only thing in Java that forms a closure.

‘Closure’ and ‘anonymous function syntax’ are orthogonal.


Java anon classes couldn't capture non-final locals back when they were introduced. So they were closures, but not full-fledged. In contrast, C# anon delegates could capture any local, including writeable ones.


They still can't, captured variables need to be "effectively final" since Java 8, meaning they aren't allowed to be written after the capture.

I think that's reasonable though, capturing mutable state is a recipe for disaster, and if you really want/need to you can wrap it in a properly synchronized container (e.g. AtomicReference in Java)


In most cases, lambdas that capture mutable state effectively take ownership of it, so there's no issue there.


Anonymous inner classes are not proper closures as the other comment explained. And as you noted the syntax is so abhorrent that their use is actively discouraged by the guava developers for example. And all the source code pre-Java 8 that use anonymous inner classes to pass functions around was pretty unreadable.


Why do you think they aren’t proper closures? Because they don’t allow you to mutate captured locals? Neither do closures in Haskell, and neither do lambdas in Java 8, so by that argument Java still doesn’t have closures. Your arguments are inconsistent because you’ve claimed that Java now has closures, even though they still do not meet your definition of closures.

Apart from effective finality and implementation, the semantics of anonymous classes and lambdas is the same!


How is "break" in switch statements a footgun in Java? It is annoying boilerplate to need a "break" after each case, but how can it lead to shooting yourself in the foot?


Because I have seen more than once devs forgetting to add a 'break;', resulting in more or less subtle bugs.

The kotlin/swift way of declaring this flow does not let you do this mistake.


I recommend using ErrorProne in Your projects. There are several problems its compiler will catch and fall through begin one of them: http://errorprone.info/bugpattern/FallThrough

It might take quite a lot of work to fix (or bypass) all the reported problems in a large codebase but it is worth it.


by forgetting it in that one long clause.



I'm seeing a lot of interest in pattern matching as users that have discovered it via rust's `match` or similar want to benefit from it in $dayjob language.

C# has seen a similar push to implement pattern matching, down to the use of `_` as discard. The first iteration of this feature in C# 7 is feature light, but it's a good start, and they've promised more to come. The team behind C# has become really good at getting over NIH and embracing new technologies and techniques while somehow preserving backwards compatibility and ergonomics. Anders Hejlsberg doesn't kid around.

Here's an example cribbed from an official MSFT blog [0]:

    public static int Count<T>(this IEnumerable<T> e)
    {
        switch (e)
        {
            case ICollection<T> c: return c.Count;
            case IReadOnlyCollection<T> c: return c.Count;
            // Matches concurrent collections
            case IProducerConsumerCollection<T> pc: return pc.Count;
            // Matches if e is not null
            case IEnumerable<T> _: return e.Count();
            // Default case is handled when e is null
            default: return 0;
        }
    }
A lot of people have lamented this as the death of F#, but I think MSFT is making the right calls here.

EDIT:

Little-known fact: C# actually does have discriminated unions, and has had for a long time (from the start?) in the form of the `Exception` base class. In fact, pattern matching in C# can be "pulled off" (read: hacked together as a proof-of-concept) without the changes from C# 7 by simply using the extended catch syntax:

    class MyType1 : System.Exception
    { /* ... */ }

    class MyType2 : System.Exception
    { /* ... */ }

    void foo()
    {
        throw something ? new MyType1() : new MyType2();
    }

    void bar()
    {
        try
        {
            foo()
        }
        catch (MyType1 mtype)
        {
            //take type-specific action here
        }
        catch (MyType2 mtype)
        {
            //take type-specific action here
        }
        catch (Exception _)
        {
            //the _ was optional, but this is the `default` block
        }
    }


[0]: https://blogs.msdn.microsoft.com/seteplia/2017/10/16/dissect...


I might be wrong, but I'd say that the interest in switch expressions and pattern matching from the Java community most likely comes from Scala (a JVM language) rather than Rust. Here's an example from the Scala docs:

  def showNotification(notification: Notification): String = {
    notification match {
      case Email(email, title, _) =>
        s"You got an email from $email with title: $title"
      case SMS(number, message) =>
        s"You got an SMS from $number! Message: $message"
      case VoiceRecording(name, link) =>
        s"you received a Voice Recording from $name! Click the link to hear it: $link"
    }
}


No need for the '{' after "String = ". It'll make your code less crowded.


My interest in pattern matching goes back to Caml Light and Standard ML, learned in 1996.

Rust is not the source of all innovations.

> Anders Hejlsberg doesn't kid around.

Except he doesn't do much C# nowadays, Mads Torgersen is the actual C# chief architect.

While Anders still advises on C#, he has switched focus to Typescript.


Hejlsberg isn't involved with C# any more.


Doh, can’t we have pattern matching - with destructuring of course - and be done with it?

(And tuples, ADTs ah the list goes on..)


There's a separate JEP [1] for pattern matching, and the switch expressions are both preparation, companion and requirement for it.

[1]: http://openjdk.java.net/jeps/305


Nice, I don't see how I could – at the very least - upvote for this proposal. How does that work?


Assignable return values: +1

Concise AND expressions (eg "case UP, DOWN -> 1;"): +1

Using keyword 'break' instead of 'return': -1


'break' feels ugly, but doesn't 'return' already have a different meaning here?

You could do something context sensitive to make 'return' act differently in a switch expression versus a switch statement, but that seems more confusing.


> You could do something context sensitive to make 'return' act differently in a switch expression versus a switch statement

But you shouldn't. That would break existing code. What if you actually want to return something from within a switch statement?


You ignored the parts where I said "already has a different meaning" and "that seems more confusing".


You mean concise OR expressions, right?


I’m not sure. Reading the JEP, I inferred the separate clauses were the ORs and each clause’s compound expression could have ANDs. But what you say is likely right.


Glad to see conditional expressions becoming popular. TBH it's hard for me to understand why anyone ever felt that switch/if should be statements. I guess being too immersed in assembly programming?


I don't need this. In most cases everything can be solved with different methods overrided by type. This is OOP way.

But it's makes language more complicated and what is more important - it makes harder to refactor, especially via IDE. So my opinition: we don't need this and this doesn't have any value besides marketing.


The problem with OOP way is that it's overly wordy and complex in this case. Much like switch statements in procedural code before the advent of OOP.

Type-based function overloads are not specific to OOP, by the way.


This is inconsistent and no longer Java.

The -> is not a good idea and break <something> should be return <something>. The -> throw e; "sugar" is just ridiculous!


IMO, return <something> may be ambiguous in switch expression.


Yeah, but return gives the value of an expression and break is a control flow keyword.


the only problem/wierdness might happen with -> is if switch will return a lambda. i.e. case "Foo" -> () -> "newfoo" looks silly at first. well scala supports that and it looks strange at first but in functional programming, these patterns aren't as strange as they first look.


People already use return if the switch is the last statement in a function, so, all this "sugar" is only confusing, inconsistent, and not improving the readability or the learning curve.


well I think the sugar is fine. just the none sugar is kinda.. wierd. who would ever write: 'case "foo": break 1'

it's just unnecessary. (but they probably do it for backwards compat or so, no idea..)


They can't use "return", because it already means something else in Java, including inside a switch statement.


break <something> should not be return <something>. What if you actually want to return something from within a switch? More to the point, what about existing code that uses return in a switch statement.


That would be a terrible coding practice!


-> is already in for lambdas.


Yeah, but this is not a function.


Arrows has been historically used for both functions and matches in languages that support both. E.g. Ocaml:

   let rec fib = fun x ->
      match x with 
      | 0 -> 1
      | 1 -> 1
      | _ -> fib (x - 1) + fib (x - 2)


"maps to" ?


Not a function statement, I meant.


I want this in Go!


Don't count on it. Go decided against if-expressions despite them being much more common, so I don't see pattern-matching-like construction coming to go.


Go is a kind of safer C with GC, it won't move much beyond that.

Better appreciate it for that, than wishing for features that will never come.


This looks like a backport of Kotlin's `when` expression.


Is that a bad thing?

Also it's much more similar to Scala's "match" expression, which is also on the JVM and predates Kotlin by about 7 years.


Languages shouldn’t try so hard to wrap syntax around one particular use for something, because you end up tearing the whole thing apart in maintenance. It’s sort of optimizing the writing of the code, which isn’t the worst part.

Here, “int result = switch...” seems nice for that one situation, “until it isn’t” (and in my experience these “until” events can come as soon as the very next time you have to maintain the thing). Oh, wait: I actually want to log something in one of the cases in addition to the assignment but since the entire thing is a fancy assignment expression now I have to rewrite 90% of it to add a glorified print statement for one of them!?

And it’s not just Java. In “new” C++ for instance, the highly-specialized syntax for loop iteration is nice but it has the same problem: sometimes the change you’re making does need iterators, unusual increment points, etc. and you end up peeling apart everything that was supposed to be so helpful and rewriting way more than you should need to.


You're completely missing the point. The switch expression is exactly that, an expression. It can be used wherever you can use an expression. This is fundamental to programming languages and especially in functional programming languages where it is a requirement of everything in the language.

You can use logging/side effect in it. You can not assign it to a variable, all valid in Java.

Consider the if expression (ternary ?: operator in Java). In most modern languages (e.g. Scala, Kotlin, Rust) there is no ternary operator, and only an if-expression:

    int value = if(something) 1 else 2 
According to you, this is wrong, but it isn't, you can still use it as a statement:

    if(something) {
       doThis()
    } else {
       doThat()
    }
Just like you can have a statement (line) with just "1+1" or "doThis()", you can have a statement of just an if-expression.

And you can also add side-effect (logging) withing expressions:

    int value = if(something) {
        log("Uh oh");
        -1
    } else {
        1
    }
Making a language construct an expression is the complete opposite of tying it to a particular use-case, it's giving it the ultimate flexibility because you can use expressions everywhere.


I’m not debating why expressions are part of languages. I’m saying that I shouldn’t have to tear apart half the code just to make a change.

And there are very simple examples of that problem, right in the article:

1. Allowing the entire switch expression to bind to the same assignment makes it difficult to maintain when adding a case that will not perform that assignment. Now, instead of adding one line, you’re tearing apart half the block and trying to find a way to make equivalent new code.

2. Even adding a single line to a case may be impossible without rewriting the case. Given some 'case "Bar" -> 2;', the maintainer has to redo the entire thing: rewrite it as 'case "Bar":' with a colon, add 'break 2;' to preserve original behavior, and then add their stuff. That’s just broken; it won’t even produce a clean "diff" because you’re “changing” lines that don’t have anything to do with your new code. And you can bet at some point that somebody may just plain forget to add back the "break" when rewriting the expression needlessly, introducing a bug.


You probably missed the part below the first paragraph. You can have additional statements in case branches.


I don't get you. You can still use the old syntax without the arrow and add your print before you break with a value inside a switch expr.


I don't agree. Language designers are optimizing common use cases and that is a good thing.




Join us for AI Startup School this June 16-17 in San Francisco!

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

Search: