Hacker News new | ask | show | jobs
by ecshafer 1002 days ago
Java getting better pattern matching is a great change. Id really like more of the functional features to make it into Java.

I would love if Java pattern matching could at least get to the level of ruby pattern matching. Ruby pattern matching will allow you to deconstruct arrays and hashes to get pretty complicated patterns, which is really powerful. Right now it seems like Java might have that with a lambda in the pattern, but its not going to be as elegant as ruby where:

case {name: 'John', friends: [{name: 'Jane'}, {name: 'Rajesh'}]} in name:, friends: [{name: first_friend}, *] "matched: #{first_friend}" else "not matched" end #=> "matched: Jane"

But the big change here is virtual threads which should be a game changer.

4 comments

Simple solution: JRuby.

Virtual threads are going to make Ruby fibers work properly for JRuby so that’s going to be huge as well.

Charles Nutter gave an update in August. 45 minute mark he talks about virtual threads.

https://youtu.be/pzm6I4liJlg?si=vKVICrola4OmJIal

We recently added pattern matching to Dart [1], so I'm always keen to see how it compares to similar features in other languages. In case it's interesting, here's that Ruby example ported to Dart:

    print(switch ({'name': 'John', 'friends': [{'name': 'Jane'}, {'name': 'Rajesh'}]}) {
      {'friends': [{'name': var firstFriend}, ...]} => "matched: $firstFriend",
      _ => "not matched"
    });
Pretty similar! The main differences are that Dart doesn't have symbols, so the keys are string literals instead. Also, variable bindings in patterns are explicit (using "var") here to disambiguate them from named constant patterns.

[1]: https://medium.com/dartlang/announcing-dart-3-53f065a10635

> We recently added pattern matching to Dart [1]

I've been using that and I love it, in general... but can I ask you why do we need to name a variable in a pattern like this:

    switch (p) {
      Person(name: var name) => ...
    }
That's the only thing that feels a bit annoying as you have to rename the variable... In Java, this would be something like:

    Person(var name) -> ...
EDIT: I guess it's to support `Person(name: 'literal')` matches.

> Dart doesn't have symbols

That's weird, as I actually use sometimes `#sym` (which has type `Symbol`)??

    print((#sym).runtimeType);
This prints `Symbol` :)

I know you know Dart in and out, but could you explain why this is not actually a symbol in the way Ruby symbols are?

We require "var" before variable patterns because we also allow named constants in patterns (which match if the value is equal to the constant's value):

    const pi = 3.14; // Close enough.

    switch (value) {
      (pi, var pi) => ...
    }
This case matches a record whose first field is equal to 3.14 and binds the second field to a new variable named "pi". Of course, in practice, you wouldn't actually shadow a constant like this, but we didn't want pattern syntax to require name resolution to be unambiguous, so in contexts where a constant pattern is allowed, we require you to write "var", "final", or a type to indicate when you want to declare a variable.

Swift's pattern syntax works pretty much the same way.

> > Dart doesn't have symbols

> That's weird, as I actually use sometimes `#sym` (which has type `Symbol`)??

Oh, right. I always forget about those. Yes, technically we have symbols, but they are virtually unused and are a mostly pointless wart on the language. It's not idiomatic to use them like it is in Ruby.

I don't find symbols to be pointless. They are useful as "interned strings" and that's exactly what I need sometimes. I could use `const myThing = "my thing";` for that purpose (but with symbols I don't need to declare it anywhere, just use it... for better or worse!), I suppose, but before `const` existed, I believe symbols were the only way to do that?
> They are useful as "interned strings" and that's exactly what I need sometimes.

I'm not sure exactly what you mean by "need" here, but as far as I know, Dart doesn't make any promises about the memory management or efficiency of either strings or symbols.

If I were you, I'd just use strings.

I had to double check this, because basically the only use of symbols in any languages is to provide a constant value you can treat as an internalized string (like Common Lisp keywords, for example). The would be entirely useless if that were not the case.

Luckily, the current Dart specification does guarantee this (section 17.8):

"Assume that i ∈ 1, 2, and that oi is the value of a constant expression which is a symbol based on the string si. If s1 == s2 then o1 and o2 is the same object. That is, symbol instances are canonicalized."

Apparently, there's even special treatment for "private symbols", which are only the same object "in the same library". TIL.

Source: https://spec.dart.dev/DartLangSpecDraft.pdf

EDIT: there's even a whole sentence justifying the existence of symbols as being related to reflection, actually... they say Dart literal Strings are already "canonicalized" so that fact about Symbols is not enough for that.... hence you're right that String literals are just as good for the use-cases I had in mind. I guess I will use String literals from now on after all.

EDIT 2: > I'm not sure exactly what you mean by "need"

Hopefully it's clear what I "needed" now... basically, interned Strings to avoid wastefully comparing bytes when a pointer comparison would suffice as all values are known at compile-time.

Pattern matching is a neat tool to keep in the toolbox. When it's the right tool for the job, it is really cool and is a lot cleaner than a bunch of conditional checks. However, I rarely reach for it. Maybe my use cases are unusual? I am genuinely curious how often other developers find pattern matching to be the best tool for the job.
Pattern matching is what makes sum types ergonomic enough to be used. Many a Java design doesn't use said interface-based sum types because it's so cumbersome to use them. But whena language has pattern matching, then suddenly designing with sum types in mind is done a lot, and therefore you see examples of good pattern matching everywhere.

When I teach Scala, a very high percentage of the teaching time is ultimately down to re-introducing how to design business domains, because seasoned devs just reach for large classes with a million optional fields, which not only can represent valid systems states, but thousands of invalid ones.

In languages that have strong support for pattern matching, whether it be on values or types, I find myself reaching for it instead of conditionals. It's all about the explicitness for me. You have to list out all the cases you care about, so there's no room for ambiguity. Plus, the compiler will usually warn you if you've missed a case, which is like a built-in bug catcher. It's also great for working with immutable data, less state to worry about. And let's talk about readability; the code basically documents itself because you can see the shape of the data right in front of you. You can even destructure data on the fly, pulling out exactly what you need. If you're using a statically-typed language, pattern matching adds an extra layer of type safety. And, not to forget, it nudges you toward a more functional style of coding, which I find leads to cleaner, more modular code. So yeah, I reach for pattern matching quite a bit; it often feels like the right tool for the job.
When available, I pretty much always use pattern matching. It tends to shorten code while not reducing clarity (often increasing it) which means fewer opportunities for errors to creep in. Statically typed languages that can detect incomplete case handling also reduces the chances for some errors (as long as you don't make a catch-all case) but also helps when you change something so that a new case is needed. It also tends to shift the code to the left, reducing the indentation. So shorter, clearer, less unnecessary indentation. Generally a positive.
It probably depends on the language you're using. Pattern matching is awesome in Erlang and Elixir. In most other languages it ranges from "nice" to "bleh".
Pattern matching is awesome in Rust. It carries the stellar legacy of Haskell.
Patterns are somewhat nice to have, but for me they’re difficult to read, and not because my brain isn’t used to them. The simple identifier instanceof is about all I’ll use _most_ of the time. Otherwise, yes they are more concise, but lose too much information in the process.

I’d rather see a boatload load of other features before patterns. I’ve been experimenting with project manifold[1]. _That_ is the path Java sb on. Just my take.

1. https://github.com/manifold-systems/manifold

One example for you: anytime you needed to use the "Visitor pattern" to do a transformation from one representation to another - you don't need it now. Sealed classes and pattern matching will be more succinct and easier to reason about.
I think that you can replace almost any If else with pattern matching. Pattern matching makes type checks easier, which if you are really heavily using types through your program, makes pattern matching even better.
I really like that Ruby throws NoMatchingPatternError if none of the patterns match. It's a bit like the much-acclaimed exhaustive pattern matching in static languages (though at runtime rather than compile-time, obviously) and better than just silently falling off the end, which IIRC is what Python's pattern matching does.
In Python you can terminate a for loop with else, which will be run whenever the loop runs to the end without breaking
That's not particularly relevant to the nice pattern matching property I mentioned. If you need to manually write supplementary code to get the exhaustiveness safety then that's back into the realm of bog-standard defensive programming.

Here's what I mean. The Ruby will throw NoMatchingPatternError and the Python will silently do nothing.

    x = [10, "figs"]

    case x
    in [n, "apples"]
      :foo
    in [n, "oranges"]
      :bar
    end

    # ---

    x = [10, "figs"]

    match x:
        case [n, "apples"]:
                ...
        case [n, "oranges"]:
                ...
You can use ‘case _’ in that case … it’s not a big deal to opt into this default behaviour.
I know, that's why I mentioned the manual part. What I'm getting from this exchange is that Python is your team and no criticism can be allowed to stand.
No I’m just trying to add substance to the discussion.

I like Ruby but I wouldn’t use it for the things I use Python for.

Neat. Will check it out.

I recently spotted a (new to me) foreach / else construct in a templating language (sorry, forget which one); else is invoked if the list is empty. Nice sugar for common outputs like "no items found".

I appreciate modest syntactic sugar.

For instance, my #1 sugar wish is for Java's foreach is to do nothing when the list reference is null. Versus tossing a NPE.

Eliminates an unnecessary null check and makes the world a little bit more null-safe.

> else is invoked if the list is empty.

for / else should do that too …