Hacker News new | ask | show | jobs
by gavinray 1416 days ago
Modern Java has first-class functions, pattern matching with structural binding, records/pure immutable data classes, the whole 9 yards:

    static void main() {
        BiFunction<Integer, Integer, Integer> add = (Integer x, Integer y) -> x + y + 5;
        Integer result = addTo(10, add);
        Integer result2 = addTo(10, (x, y) -> x + y + 5);
    }

    static Integer addTo(Integer acc, BiFunction<Integer, Integer, Integer> addFn) {
        return addFn.apply(acc, 5);
    }

    Integer eval(Expression e) {
        return switch (e) {
            case INT(var value) -> value
            case ADD(var left, var right) -> eval(left) + eval(right)
            case MULT(var left, var right) -> eval(left) * eval(right)
        }
    }
5 comments

Unfortunately, records are only guaranteed to be 'shallowly' immutable. It would be great if future Java versions provided a straightforward way to enforce immutability.
> records/pure immutable data classes

Example?

As far as I know, Java has final, which means that particular reference can't be re-assigned, but the object referred to remains mutable. You have to resort to e.g. having separate immutable and mutable interfaces or whatever to restrict a someone from mutating your object.

If you want an immutable data class more than one level deep, I don't know if there's a convenient way to do that like there is with const in C++ (or the default behaviour in Rust).

But I'm not a Java programmer, I haven't really kept up with the language. Happy to be proven wrong.

You are right, though I seldom find it a problem in practice. Also, OOP sort of makes immutability hard to define (e.g. is a getter with an internal counter of accesses immutable? In a way, it is. Also, Rust’s internal mutability pattern is similar).

Nonetheless, recently more and more standard classes are made deliberately immutable, and there was a proposal for frozen arrays as well (not sure on their status).

> You are right, though I seldom find it a problem in practice.

Although it's easy to to tell people that mutation is confusing and to avoid it, enforcing that is much easier if the compiler is on your side and will prevent mutation with const.

I've encountered unnecessary mutation (introducing implicit assumptions on the order of calls, and making things more confusing) constantly in both Java and C++, but enforcing const in C++ cuts down on that. Or at least, it forces a const_cast which I won't approve without a really good reason.

You're right about Rust of course, internal mutability is possible and maybe even common with RefCell, but culturally it seems like that's avoided. On the other hand, mutability is extremely common in Java.

Maybe I'm just traumatized from some of the horrific code heavily using mutation I've seen over the years.

JDK 17 record classes:

  record User(String name, Integer age, Boolean isActive) {}
https://docs.oracle.com/en/java/javase/18/language/records.h...
If you have mutable members you can still absolutely do myRecord.member().methodThatMutates().

The only way to stop this is to remove the mutable methods from the interface entirely, which is what I'm complaining about.

First class functions are as modern as the Iphone 5.
Pedantically, I want to point out that first class functions predate the original iPhone by a large margin ...
I meant lambda’s introduction’s date in Java.
I haven't programmed in Java since 2005, so forgive me. But isn't a lambda automatically converted to an object of the necessary single-method type? That feels a little magical and suggests that there really isn't such a thing as a genuine first-class function in Java, as there is no way to define a function, and no universal type to assign to such a thing.
Well, the standard library does have some function types, like Function<A,B>, Supplier<A>, etc. You can just initialize a variable of these types and pass them around. I don’t see how are these not genuine first-class entities. The lambda syntax is also quite pleasant.

(Also, behind the scenes they often compile to static functions and called through the invokedynamic instruction)

This is destructuring but not pattern matching.
How is this not pattern matching?

In Scala, I would write:

  enum Expr:
    case INT(value: Int)
    case ADD(left: Expr, right: Expr)
    case MULT(left, Expr, right: Expr)
  
  def eval(e: Expr): Int = e match
    case INT(value) => value
    case ADD(l, r) => eval(l) + eval(r)
    case MULT(l, r) => eval(l) + eval(r)
This "match" syntax is the example given in the Scala docs for Pattern Matching:

https://docs.scala-lang.org/tour/pattern-matching.html

The fact that Java happens to use "switch" instead of "match" is one of syntax, not semantics.

JDK 17/18 introduces Sealed Types, which allow you to create ADT's

  sealed interface Expr {
    record INT(Integer value) implements Expr {}
    record ADD(Expr l, Expr r) implements Expr {}
    // etc
  }
When you "switch" over sealed types, the switch expression is exhaustive if all members have branches and requires no default case + is typesound.
Can you pattern match deeply? Can you make a case for ADD where the first param is a MULT with the second param of the MULT being INT(5)?
It is underway (destructors are not yet finalized).
Does it support matching patterns in data structures?

    f [ [1, _]. [3, _] ] = ...

    f [ [], [10, 20] ] = ...
Given that java doesn’t have too much syntax sugar for data structures, I doubt this will come, but other kinds of destructors will be possible.