Una mirada 👀 funcional a Java 2️⃣2️⃣

Antonio Muñoz

#commitconf 2024

¿Quien soy?

#commitconf 2024

Encuesta

  • Java 22? 😎
  • Java 21? 👍
  • Java 17? 🆗
  • Java 11? ⚠️
  • Todavía Java 8? 😢
  • Y anteriores a Java 8? 🤯
#commitconf 2024

Jetbrains ecosystem survey

#commitconf 2024

Java 8️⃣ 👴

  • Lanzado en Marzo 2014 🚀
  • No existía ni tiktok 🎶 ni openai 🤖
  • Ese mismo año se lanzó spring-boot v1.0.
  • Docker y kubernetes daban sus primeros pasos 👶
#commitconf 2024

Agenda 📆

  • El largo camino a Java 22.
  • Tipos de datos algebraicos.
  • Ejemplos.
  • Futuro.
#commitconf 2024

Java Release Cadence ☕

  • Dos release al año.
  • LTS.
  • Preview features.
  • They have a plan.
#commitconf 2024

Switch expressions 🛤️

var value = switch (input) {
    case "a" -> 1;
    case "b" -> 2;
    default -> 0;
};
  • Incluido en Java 14.
  • Una nueva vida para switch.
  • Expresión.
  • yield.
#commitconf 2024

yield

var value = switch (input) {
    case "a" -> {
        yield 1;
    }
    case "b" -> {
        yield 2;
    }
    default -> {
        yield 0;
    }
};
#commitconf 2024

Records 📹

public record Movie(String title, int year, int duration) {
}
  • Incluido en Java 16.
  • Muy esperado por la comunidad.
  • Inmutables.
  • Constructor canónico.
#commitconf 2024

Records: Constructor Canónico 🏗️

public record Movie(String title, int year, int duration) {
    public Movie {
        if (title == null || title.isEmpty()) {
            throw new IllegalArgumentException();
        }
    }
}
  • Se ejecuta siempre.
#commitconf 2024

Records: Constructor Canónico 🏗️

public record Movie(String title, List<String> cast) {
    public Movie {
        cast = List.copyOf(cast);
    }
}
  • Copia defensiva para evitar modificar el contenido del record.
#commitconf 2024

Sealed classes and interfaces 🔐

public sealed interface Shape {
    record Square(int side) implements Shape {}
    record Rectangle(int weight, int height) implements Shape {}
    record Circle(int radius) implements Shape {}
}
  • Incluido en Java 17.
  • Jerarquías de clases cerradas.
  • non-sealed
#commitconf 2024

non-sealed

public sealed interface Shape {
    record Square(int side) implements Shape {}
    record Rectangle(int weight, int height) implements Shape {}
    record Circle(int radius) implements Shape {}
    non-sealed interface CustomShape extends Shape {}
}
#commitconf 2024

Pattern matching for switch

var result = switch (obj) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> obj.toString();
};
  • Incluido en Java 21.
#commitconf 2024

Null patterns

var result = switch (obj) {
    case null      -> "null";
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> obj.toString();
};
#commitconf 2024

Record patterns

var area = switch (this) {
    case Square(var side) -> side * side;
    case Rectangle(var weight, var height) -> weight * height;
    case Circle(var radius) -> Math.PI * Math.pow(radius, 2);
};
  • Incluido en Java 21.
  • Deconstructores, nos permite acceder a los componentes de los objectos.
  • Exhaustiveness.
#commitconf 2024

Guarded patterns

var result = switch (point) {
    case Point(var x, var y) when y == 0 -> processX(x);
    case Point(var x, var y) -> processXY(x, y);
};
  • Podemos añadir condiciones adicionales usando when
#commitconf 2024

Nested patterns

var result = switch (this) {
    case Square(Point(var x, var y), var side) -> ...;
    case Rectangle(Point(var x, var y), var weight, var height) -> ...;
    case Circle(Point(var x, var y), var radius) -> ...;
};
  • Podemos anidar patrones
#commitconf 2024

Unnamed variables and patterns

var result = switch (obj) {
    case Integer _ -> "int";
    case Long _    -> "long";
    case Double _  -> "double";
    case String _  -> "string";
    default        -> "other";
};
  • Incluido en Java 22.
  • Mejora para el pattern matching.
  • Eliminar verbosidad.
#commitconf 2024

Tipos de datos algebraicos

  • AKA ADTs (algebraic data types).
  • Viene de las matemáticas.
  • Recursivo.
  • Productos y sumas de tipos.
    a + b a * b
  • Tienen propiedades algebraicas.
#commitconf 2024

Otros lenguajes con soporte ADTs

  • JVM:
    • Scala, Kotlin
  • Rust
  • C#
  • TypeScript
  • Otros lenguajes funcionales:
    • Haskell, F#, OCaml
#commitconf 2024

ADTs

  • ¿Cómo podemos representarlos en Java?
  • Un record es un producto de tipos.
  • Un sealed interface es una suma de tipos.
  • ¿Pero eso para qué me sirve?
    • Estructuras de datos.
    • Estructuras de control.
    • DSLs
    • Manejo de errores.
#commitconf 2024

ADTs: List

sealed interface List<T> {
    record NonEmpty<T>(T head, List<T> tail) implements List<T> {}
    record Empty<T>() implements List<T> {}
}
#commitconf 2024

ADTs: List (map)

sealed interface List<T> {
    default <R> List<R> map(Function<T, R> mapper) {
        return switch (this) {
            case NonEmpty<T>(var head, var tail)
                -> new NonEmpty<>(mapper.apply(head), tail.map(mapper));
            case Empty<T> _ -> new Empty<>();
        };
    }
}
#commitconf 2024

ADTs: List (filter)

sealed interface List<T> {
    default List<T> filter(Predicate<T> filter) {
        return switch (this) {
            case NonEmpty<T>(var head, var tail) when filter.test(head)
                -> new NonEmpty<>(head, tail.filter(filter));
            case NonEmpty<T>(var head, var tail)
                -> tail.filter(filter);
            case Empty<T> _ -> new Empty<>();
        };
    }
}
#commitconf 2024

ADTs: List (fold)

sealed interface List<T> {
    default T fold(T initial, BinaryOperator<T> operator) {
        return switch (this) {
            case NonEmpty<T>(var head, var tail)
                -> tail.fold(operator.apply(initial, head), operator);
            case Empty<T> _ -> initial;
        };
    }
}
#commitconf 2024

ADTs: Tree

sealed interface Tree<T> {
    record Node<T>(T value, Tree<T> left, Tree<T> right) implements Tree<T> { }
    record Leaf<T>(T value) implements Tree<T> {}
}
#commitconf 2024

ADTs: Optional

sealed interface Optional<T> {
    record Empty<T>() implements Optional<T> { }
    record Present<T>(T value) implements Optional<T> {}
}
#commitconf 2024

ADTs: Optional (map)

sealed interface Optional<T> {
    default <R> Optional<R> map(Function<T, R> mapper) {
        return switch (this) {
            case Present<T>(var value) -> new Present<>(mapper.apply(value));
            case Empty<T> _ -> new Empty<>();
        };
    }
}
#commitconf 2024

ADTs: Optional (filter)

sealed interface Optional<T> {
    default Optional<T> filter(Predicate<T> filter) {
        return switch (this) {
            case Present<T>(var value) when filter.test(value) -> this;
            case Present<T> _ -> new Empty<>();
            case Empty<T> _ -> new Empty<>();
        };
    }
}
#commitconf 2024

ADTs: Optional (fold)

sealed interface Optional<T> {
    default <R> R fold(Supplier<R> onEmpty, Function<T, R> onPresent) {
        return switch (this) {
            case Present<T>(var value) -> onPresent.apply(value);
            case Empty<T> _ -> onEmpty.get();
        };
    }
}
#commitconf 2024

ADTs: Either

sealed interface Either<L, R> {
    record Left<L, R>(L left) implements Either<L, R> { }
    record Right<L, R>(R right) implements Either<L, R> {}
}
#commitconf 2024

ADTs: DSLs

sealed interface Expr {
    record Val(int value) implements Expr {}
    record Sum(Expr left, Expr right) implements Expr {}
    record Diff(Expr left, Expr right) implements Expr {}
    record Times(Expr left, Expr right) implements Expr {}
}
#commitconf 2024

ADTs: DSLs

sealed interface Expr {
    default int evaluate() {
        return switch (this) {
            case Val(var value) -> value;
            case Sum(var left, var right) -> left.evaluate() + right.evaluate();
            case Diff(var left, var right) -> left.evaluate() - right.evaluate();
            case Times(var left, var right) -> left.evaluate() * right.evaluate();
        };
    }
}
#commitconf 2024

ADTs: DSLs

sealed interface Expr {
    default String asString() {
        return switch (this) {
            case Val(var value) -> String.valueOf(value);
            case Sum(var left, var right) -> "(" + left.asString() + "+" + right.asString() + ")";
            case Diff(var left, var right) -> "(" + left.asString() + "-" + right.asString() + ")";
            case Times(var left, var right) -> "(" + left.asString() + "*" + right.asString() + ")";
        };
    }
}
#commitconf 2024

ADTs: DSLs

sealed interface Expr {
    default void print() {
        System.out.println(asString() + "=" + evaluate());
    }
}
#commitconf 2024

ADTs: DSLs

static void main() {
    sum(val(1), val(2)).print();
    times(diff(val(10), val(8)), val(2)).print();
}
(1+2)=3
((10-8)*2)=4
#commitconf 2024

ADTs: Json

sealed interface Json {
    enum JsonNull implements Json { NULL }
    enum JsonBoolean implements Json { TRUE, FALSE }
    record JsonString(String value) implements Json {}
    record JsonNumber(Number value) implements Json {}
    record JsonObject(Map<String, Json> value) implements Json {}
    record JsonArray(List<Json> value) implements Json {}
}
#commitconf 2024

ADTs: Json

sealed interface Json {
    default String asString() {
        return switch (this) {
            case JsonNull _ -> "null";
            case JsonBoolean b -> switch (b) {
                case TRUE -> "true";
                case FALSE -> "false";
            };
            ...
        };
    }
}
#commitconf 2024

ADTs: Json

sealed interface Json {
    default String asString() {
        return switch (this) {
            ...
            case JsonString(var value) -> "\"" + value + "\"";
            case JsonNumber(var value) -> String.valueOf(value);
            ...
        };
    }
}
#commitconf 2024

ADTs: Json

sealed interface Json {
    default String asString() {
        return switch (this) {
            ...
            case JsonObject(var map)
                -> map.entrySet().stream()
                    .map(entry ->  "\"" + entry.getKey() + "\":" + entry.getValue().asString())
                    .collect(joining(",", "{", "}"));
            ...
        };
    }
}
#commitconf 2024

ADTs: Json

sealed interface Json {
    default String asString() {
        return switch (this) {
            ...
            case JsonArray(var array)
                -> array.stream()
                    .map(Json::asString)
                    .collect(joining(",", "[", "]"));
        };
    }
}
#commitconf 2024

ADTs: Json

static void main() {
    var json = array(
        object(
            entry("name", string("Toni")),
            entry("age", number(46)),
            entry("old", JsonBoolean.TRUE)),
        object(
            entry("name", string("Baby")),
            entry("age", JsonNull.NULL),
            entry("old", JsonBoolean.FALSE))
    );
    System.out.println(json.asString());
}
#commitconf 2024

ADTs: Json

[{"old":true,"name":"Toni","age":46},{"old":false,"name":"Baby","age":null}]
#commitconf 2024

ADTs: Errores

interface MovieRepository {
    MovieResponse create(Movie movie);
}

sealed interface MovieResponse permits MovieCreated, MovieError {}

record MovieCreated(UUID id) implements MovieResponse {}
#commitconf 2024

ADTs: Errores

sealed interface MovieError extends MovieResponse {
    record DuplicatedMovie(UUID id) implements MovieError {}
    record InvalidDuration(int duration) implements MovieError {}
    record InvalidYear(int year) implements MovieError {}
    record InvalidStars(int stats) implements MovieError {}
    record EmptyTitle() implements MovieError {}
    record EmptyDirector() implements MovieError {}
    record EmptyCast() implements MovieError {}
    record DuplicatedActor(String actor) implements MovieError {}
}
  • Definir errores de dominio.
#commitconf 2024

ADTs: Errores

@PostMapping("/movies")
public ResponseEntity<UUID> create(@RequestBody Movie movie) {
    var result = repository.create(movie);
    return switch (result) {
        case MovieCreated(var id) -> ResponseEntity.ok(id);
        case MovieError e -> ResponseEntity.of(toProblem(e)).build();
    };
}
#commitconf 2024

ADTs: Errores

static ProblemDetail toProblem(CreateMovieResponse.MovieError error) {
    var detail = switch (error) {
      case DuplicatedMovie(int id) -> "duplicated movie with id: " + id;
      case InvalidDuration(var duration) -> "invalid duration: " + duration;
      case InvalidYear(var duration) -> "invalid year: " + duration;
      case InvalidStars(var stars) -> "invalid stars: " + stars;
      case EmptyTitle() -> "title cannot be empty";
      case EmptyDirector() -> "director cannot be empty";
      case EmptyCast() -> "cast cannot be empty";
      case DuplicatedActor(var actor) -> "duplicated actor: " + actor;
    };
    return ProblemDetail.forStatusAndDetail(BAD_REQUEST, detail);
}
#commitconf 2024

ADTs: Either

interface MovieRepository {
    Either<MovieError, UUID> create(Movie movie);
}
@PostMapping("/movies")
public ResponseEntity<UUID> create(@RequestBody Movie movie) {
    var result = repository.create(movie);
    return result.fold(
        error -> ResponseEntity.of(toProblem(error)).build(),
        ResponseEntity::ok);
}
#commitconf 2024

Próximamente ⌚

#commitconf 2024

Primitive types in patterns

var result = switch (obj) {
    case int i     -> String.format("int %d", i);
    case long l    -> String.format("long %d", l);
    case double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> obj.toString();
};
  • Preview en Java 23 (sep 2024).
#commitconf 2024

Primitive types in patterns

jshell> int i = 0;
i ==> 0
jshell> i instanceof byte b
$2 ==> true
jshell> int i = 123213;
i ==> 123213
jshell> i instanceof byte b
$4 ==> false
  • Castings seguros
#commitconf 2024

Bola de cristal 🔮

#commitconf 2024

Derived Record Creation

Point newPoint = oldPoint with {
    x *= 2;
    y *= 2;
};
  • Draft.
  • AKA withers.
#commitconf 2024

Static patterns

var result = switch (optional) {
    case Optional.of(var value) -> value.toString();
    case Optional.empty() -> "empty";
};
  • Early work.
  • AKA deconstructors.
  • Mejora para pattern matching.
  • Cualquier clase.
#commitconf 2024

Constant patterns

var result = switch (optional) {
    case Point(0, var y) -> process(y);
    case Point(var x, var y) -> process(x, y);
};
#commitconf 2024

¿Qué falta todavía? 🤓

  • Tail recursion.
  • Soporte de tipos de datos primitivos en genéricos.
#commitconf 2024

¿Preguntas?

#commitconf 2024

¡Gracias! 💖

#commitconf 2024

JEPs

#commitconf 2024

Documentación Oficial

#commitconf 2024

Artículos / Videos

#commitconf 2024

Enlaces

#commitconf 2024

por favor, sed benevolentes con mis errores

preguntar a la audiencia qué version de Java usan en pro

Java 20 no es LTS Java 21 todavía no aparece en las respuestas porque cuando se realizó la encuesta no se había lanzado todavía Todavía Java 8 es el más usado

Rajoy Presidente JuanCar I abdicó en Felipe VI Obama Papa Francisco I desde 2013

en Marzo y Septiembre en los últimos 10 años han ido introduciendo una serie de cambios que como resultado han dado a poder modelar e implementar ADTs. probablemente en java 23 la preview de string interpolation se eliminará. ha recibido un feedback muy negativo y se están planteando rediseñar completamente la nueva funcionalidad.

se puede: asignar a una variable devolver en un return usarlo en una lambda anidar un switch dentro de un case de otro switch eso significa que tiene que ser exhaustivo

no son un remplazo para los POJOs tradicionales son contenedores de datos inmutables NO USAR COMO Entidades de Hibernate

incluso durante la deserialización. en un POJO normal, al deserializar el objeto no se llama al constructor nunca, los atributos del objeto se rellenan desde fuera de manera insegura si tuviéramos una validación en el constructor se la saltaría

alguien desde fuera podría modificar la lista o si la lista contuviera otros objetos que no son inmutables

todavía no soporta primitivos

antes no era posible usarlo

antes la regla era siempre añadir un default en los switch para no llevarnos sorpresas, pero ahora se convierte en un antipatrón. Debemos dejar trabajar al compilador y que verifique si el switch es exhaustivo y si modificamos la definición de Shape añadiendo nuevos tipos, el compilador fallará en este switch para indicar que nos falta cubrir un caso.

las mismas propiedades que aprendimos en la escuela primaria no voy a entrar en más detalle en esto simplemente para que sepáis que tiene una base matemática

No estoy diciendo que usemos este tipo de dato tal cual es solo un ejemplo

la implementación es recursiva por lo que si la lista es muy larga tendremos un StackOverflowException

podemos implementar map, fold y otros muchos métodos

es un DSL muy simple para definir expresiones matemáticas

usando pattern matching podemos implementar la evaluación de la expresión

y generar un string que describa la expresión

en OOP tradicional habríamos necesitado sobre escribir el método asString en cada subclase o utilizar el patron visitor. Pattern Matching prácticamente jubila el patrón visitor. Es otra manera de hacer Polimorfismo, usando pattern matching

imaginemos que tenemos una aplicación para gestionar nuestras películas preferidas

en el controlador aplicamos pattern matching para devolver el identificador generado, o un error describiendo con todo el detalle posible qué ha ocurrido

aquí uso una novedad de spring-boot 3.x que son los ProblemDetail en caso de ser necesario podría devolver un status code diferente

usualmente esto evoluciona al uso de Either ya que tiene muchas más posibilidades tienes dos canales uno para errores (Left) y otro para el resultado <Right> Either en si también es una suma de tipos, porque puede ser o Left o Right

en el controlador podemos usar el método fold para construir la respuesta

si tenéis sdkman instalado podéis instalar una version EA de java 23 que ya incluye esta funcionalidad

podemos saltarnos esta parte si vamos mal de tiempo

los ADT son recursivos por lo que muchos algoritmos se implementan usando recursividad y java todavía no es un lenguaje que se pueda considerar stack safe aunque en java 23 será posible usar tipos primitivos en pattern matching, todavía no esta soportado para tipos genéricos

recomiendo leerlos ya que son muy instructivos y están muy bien redactados

TODO: why use pattern matching? - reduce cognitive load. - pattern exhaustiveness. meta: - target audience - manage expectations - content - confirm expectations - links to follow