Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Switch expression with void return type

Is there any way to force an exhaustive check of all enum values when the switch branches call methods with void return type? It's quite ugly to hard-code a yield just to coax the compiler to demand exhaustiveness.

This is my current pattern (the handle methods have void return type)

int unused = switch (event.getEventType()) {
    case ORDER   -> { handle((OrderEvent) event); yield 0; }
    case INVOICE -> { handle((InvoiceEvent) event); yield 0; }
    case PAYMENT -> { handle((PaymentEvent) event); yield 0; }
};

The reason I want to use an expression is to get a compilation error when a new enum value is added and is not handled.

like image 832
Arboreal Shark Avatar asked Feb 15 '21 07:02

Arboreal Shark


Video Answer


3 Answers

The statement of the question is a bit of an "XY problem"; what you want is totality checking, but you're asking for it to be treated as an expression, not because you want an expression, but because you want the totality checking that comes with expression-hood.

One of the items of "technical debt" left from the addition of switch expressions is the ability for switch statements to opt into the same totality checking that switch expressions get. We could not retroactively change this about switch statements -- switch statements have always been allowed to be partial -- but you are correct that it would be nice to be able to get this sort of type checking. As you surmise, turning it into a void expression switch is one way to get there, but it is indeed ugly, and worse, will not be easily discoverable. It is on our list to find a way to allow you to opt back into totality checking for switch statements. There have been discussions on the amber-spec-experts list about this; it is related to several other possible features, and design discussions are still ongoing.

like image 33
Brian Goetz Avatar answered Nov 04 '22 16:11

Brian Goetz


Maybe yield a Consumer of Event, so you yield something useful, the trade off is one more line for consumer.accept.

Consumer<Event> consumer = switch (event.getEventType()) {
    case ORDER -> e -> handle((OrderEvent) e);
    case INVOICE -> e -> handle((InvoiceEvent) e);
    case PAYMENT -> e -> handle((PaymentEvent) e);
};
consumer.accept(event);

Continue if you concern performance

Based on the comment concerning performance penalty, a benchmark is performed to compare following scenarios:

  1. Using consumer and handle is instance method
  2. Using consumer and handle is static method
  3. Not using consumer and handle is instance method
  4. Not using consumer and handle is static method

To see

  • Is using Consumer has large performance impact?
  • Is there any difference for static and instance handle method?

And the result is:

# Run complete. Total time: 00:20:30

Benchmark                                          Mode  Cnt      Score     Error   Units
SwitchExpressionBenchMark.consumerHandle          thrpt  300  49343.496 ±  91.324  ops/ms
SwitchExpressionBenchMark.consumerStaticHandle    thrpt  300  49312.273 ± 112.630  ops/ms
SwitchExpressionBenchMark.noConsumerHandle        thrpt  300  49353.232 ± 106.522  ops/ms
SwitchExpressionBenchMark.noConsumerStaticHandle  thrpt  300  49496.614 ± 122.916  ops/ms

By observing the result, there is no much different between the 4 scenarios.

  • Using Consumer does not have significant performance impact.
  • The performance different between static and instance handle method is neglectable.

The benchmark is performed with:
CPU: Intel(R) Core(TM) i7-8750H
Memory: 16G
JMH version: 1.19
VM version: JDK 15.0.2

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Warmup(iterations = 30, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 30, time = 500, timeUnit = TimeUnit.MILLISECONDS)
public class SwitchExpressionBenchMark {
    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }

    @Benchmark
    public void consumerStaticHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        Consumer<Event> consumer = switch (event.getEventType()) {
            case ORDER -> e -> staticHandle((OrderEvent) e);
            case INVOICE -> e -> staticHandle((InvoiceEvent) e);
            case PAYMENT -> e -> staticHandle((PaymentEvent) e);
        };
        consumer.accept(event);
    }

    @Benchmark
    public void consumerHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        Consumer<Event> consumer = switch (event.getEventType()) {
            case ORDER -> e -> this.handle((OrderEvent) e);
            case INVOICE -> e -> this.handle((InvoiceEvent) e);
            case PAYMENT -> e -> this.handle((PaymentEvent) e);
        };
        consumer.accept(event);
    }

    @Benchmark
    public void noConsumerHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        int unused = switch (event.getEventType()) {
            case ORDER -> {
                this.handle((OrderEvent) event);
                yield 0;
            }
            case INVOICE -> {
                this.handle((InvoiceEvent) event);
                yield 0;
            }
            case PAYMENT -> {
                this.handle((PaymentEvent) event);
                yield 0;
            }
        };
    }

    @Benchmark
    public void noConsumerStaticHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        int unused = switch (event.getEventType()) {
            case ORDER -> {
                staticHandle((OrderEvent) event);
                yield 0;
            }
            case INVOICE -> {
                staticHandle((InvoiceEvent) event);
                yield 0;
            }
            case PAYMENT -> {
                staticHandle((PaymentEvent) event);
                yield 0;
            }
        };
    }

    private static void staticHandle(PaymentEvent event) {
        doSomeJob();
    }

    private static void staticHandle(InvoiceEvent event) {
        doSomeJob();
    }

    private static void staticHandle(OrderEvent event) {
        doSomeJob();
    }

    private void handle(PaymentEvent event) {
        doSomeJob();
    }

    private void handle(InvoiceEvent event) {
        doSomeJob();
    }

    private void handle(OrderEvent event) {
        doSomeJob();
    }

    private static void doSomeJob() {
        Blackhole.consumeCPU(16);
    }

    private enum EventType {
        ORDER, INVOICE, PAYMENT
    }

    public static class Event {
        public EventType getEventType() {
            return eventType;
        }

        public void setEventType(EventType eventType) {
            this.eventType = eventType;
        }

        private EventType eventType;

        public double getD() {
            return d;
        }

        public void setD(double d) {
            this.d = d;
        }


        private double d;
    }

    public static class OrderEvent extends Event {
    }

    @State(Scope.Thread)
    public static class InvoiceEvent extends Event {
        @Setup(Level.Trial)
        public void doSetup() {
            this.setEventType(EventType.INVOICE);
        }
    }

    public static class PaymentEvent extends Event {
    }
}
like image 74
samabcde Avatar answered Nov 04 '22 15:11

samabcde


If you have test classes (say JUNIT test cases) which you build and run before releasing your main code, then you could drop a simple guard function into any existing test class for each enum you want to watch:

String checkForEnumChanged(YourEnum guard) {
    return switch (guard) {
        case ORDER -> "OK";
        case INVOICE -> "OK";
        case PAYMENT -> "OK";
    };
}

This means you can keep your main application code clear of the yield 0; style of switch and get a compile error in the test classes when the enum values are edited.

like image 42
DuncG Avatar answered Nov 04 '22 15:11

DuncG