Switch expression with void return type
Asked Answered
R

5

40

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.

Rainbolt answered 15/2, 2021 at 7:56 Comment(7)
You're using a switch expression where you should be using a switch statement.Urey
@Urey The purpose of the expression is to get a compilation error when a new enum value is added.Rainbolt
The Consumer could be accompanied with the suggestions made at How to ensure completeness in an enum switch at compile time?. See also the Visitor pattern described here.Adjourn
Given your edit, what about yielding the cast event and then call handle passing the switch expression itself as an argument?Tarbes
Are you able to refactor event so that handle() is moved there? Then no switch is needed, just event.handle().Dunaway
Some IDEs and other tools have an option for emitting warnings for non-exhaustive switches. In Eclipse this is controlled with Preferences > Java > Compiler > Errors/Warnings > Incomplete 'switch' cases on enum.Lowell
Related: https://mcmap.net/q/408598/-force-exhaustive-switch/1108305Ablepsia
E
23

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 {
    }
}
Eluviation answered 15/2, 2021 at 9:22 Comment(6)
Am I wrong to assume this solution has a negative performance impact by allocating a lambda function during each execution? If the switch expression is in a critical hot path it may be significant.Rainbolt
@ArborealShark All of the lambdas in this particular example are non-capturing, and therefore instances would be memoized and cached at the capture site, with zero performance overhead.Cribriform
@BrianGoetz only when those handle methods are staticAnking
Note that the differences of the Score values are all in the order of magnitude of the reported Error, so the actual conclusion is that they are basically all the same. Or that a better test setup is needed.Anking
@Holger, thx for your comment. Do you have some idea on what "better setup" is needed? Is it due to long runtime of doSomeJob making the difference(due to using consumer) not appearance(performance penalty is small and hidden by error)? Or I should try changing the benchmark parameter?Eluviation
The fact that doSomeJob()’s return value is not used may affect the result (use JMH’s black hole to consume the value). Besides that, you may try with different parameters, e.g. warmup, to see whether they have an impact on the result. If that all doesn’t change the results, it may simply be the case the the approaches do not differ significantly.Anking
C
22

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.

Cribriform answered 16/2, 2021 at 0:49 Comment(3)
So the -> style switch statements just got rid of the break; but are otherwise identical to the legacy ones regarding their behaviour? I trust you tried your best, but it is hard to believe that "completely new language features" with no legacy code base that needs to stay compatible can't define new rules.Deterioration
@SebastianS I think framing "-> style switch statements" as a "feature" in their own right is already on the wrong track; it is a combination of features that you can select or not. There are switch statements and switch expressions; they have different properties (yielding a value, exhaustiveness, etc.) Similarly, there are colon and -> case labels, again, with different properties. You can mix and match as appropriate, just as you can combine other features like putting conditionals inside loops. (And, describing it as "just" anything is almost surely wrong.)Cribriform
Thanks for the explanation. So far I associated switch expressions with -> and "old" switch statements with : and break;. Leading to the assumption that -> would imply exhaustiveness checks, even when not yielding any result.Deterioration
M
9

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.

Motive answered 15/2, 2021 at 12:28 Comment(2)
but effectively you are yielding Strings now and the question over them being unused is what the OP is concerned about, also a unit test can without using switch expression also suffice to guard the behavior, the question is related to compile time.Adjourn
@Adjourn This code is not intended for use in the OP application, it is there to cause compile fail in the test code which alerts OP of need to handle the enum change in non-yield switches used in main codebase.Motive
S
0

Add a delegate

Add a delegate method to forward the request and return a Void type

public class SwitchTest {
    
    enum EventType {
        ORDER,
        INVOICE,
        PARCELDELIVERY
    }

    interface Event {

        EventType getType();
    }

    static class OrderType implements Event {

        @Override
        public EventType getType() {
            return EventType.ORDER;
        }
    }

    static class InvoiceType implements Event {

        @Override
        public EventType getType() {
            return EventType.INVOICE;
        }
    }

    static void handle(Event e) {
        System.out.println(e.getType());
    }

    static Void switchExpressionDelegate(Event e) {
        handle(e);
        return null;
    }

    public static void main(String[] args) {
        Event event = new OrderType();
        Void nullNoop = switch (event.getType()) {
            case ORDER -> switchExpressionDelegate(event);
            case INVOICE -> switchExpressionDelegate(event);
            case PARCELDELIVERY -> switchExpressionDelegate(event);
        };
    }
}

Exact type

Assuming the handle method has exact type, then a parallel hierarchy of delegate methods have to be added. (this does not look good though)


    static Void switchExpressionDelegate(OrderType e) {
        handle(e);
        return null;
    }

    static Void switchExpressionDelegate(InvoiceType e) {
        handle(e);
        return null;
    }

    public static void main(String[] args) {
        Event event = new OrderType();
        Void nullNoop = switch (event.getType()) {
            case ORDER -> switchExpressionDelegate((OrderType) event);
            case INVOICE -> switchExpressionDelegate((InvoiceType) event);
            case PARCELDELIVERY -> switchExpressionDelegate((OrderType) event); // can throw error in an actual implementation
        };
    }

Adapter

If adding new classes is an option, then adapter classes can be added

All the above looks round about

As pointed by other answer by sambabcde, the best option seems to be using a Consumer

    public static void main(String[] args) {
        Event event = new OrderType();
        Consumer<Void> nullNoop = switch (event.getType()) {
            case ORDER -> e -> handle((OrderType) event);
            case INVOICE -> e -> handle((InvoiceType) event);
            case PARCELDELIVERY -> e -> handle((OrderType) event);
        };
        nullNoop.accept(null);
    }
Supplemental answered 20/2, 2021 at 7:51 Comment(0)
U
0

How about Runnable :

Runnable limitOperationRunnable = switch (limitOperation) {
  case INSERT -> () -> ...;
  case UPDATE -> () -> ...;
  case DELETE -> () -> ...;
};
limitOperationRunnable.run();
Unmoral answered 21/2, 2022 at 9:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.