As a "half way" point, modern (21+) java brings pattern matching switch statements to the language, but you'd probably construct the F# version in Java using a sealed "marker" interface. Something like
sealed interface Result permits ValidationError, SearchQuery, UserProfile {}
Along with the specific implementations of those ValidationError, SearchQuery and UserProfile classes and then a switch statement like:
Result res = db.query(input);
return switch(res) {
case ValidationError ve -> context.renderError(ve);
case SearchQuery sq -> context.redirect("Search", Map.of("q", sq));
case UserProfile up -> context.redirect("Profile", Map.of("user", up));
};
The sealed interface gives you compile time checks that your switch statement is exhaustive wherever you use it.
Before that pattern matching, I might have used a Function<Context, R> instead in the Result interface. This is off the top of my head without the benefit of an IDE telling me if I've done a stupid with the generics and type erasure but something like:
interface Result<R> {
public R handleWithContext(Context c);
}
class ValidationError<RenderedError> {
public RenderedError handleWithContext(Context c) {
return c.renderError(this);
}
}
class SearchQuery<Redirect> {
public Redirect handleWithContext(Context c) {
return c.redirect("Search", Map.of("q", this);
}
}
etc. In either case though I think you're right that an empty interface is something that should be examined closer.
Before that pattern matching, I might have used a Function<Context, R> instead in the Result interface. This is off the top of my head without the benefit of an IDE telling me if I've done a stupid with the generics and type erasure but something like:
etc. In either case though I think you're right that an empty interface is something that should be examined closer.