4 ms·
That blog post is a poor example. It replaced a simple straightforward if-else loop with a hard to understand abstraction spaghetti. A few unit tests and you'd
by another_twist 11mo ago
That blog post is a poor example. It replaced a simple straightforward if-else loop with a hard to understand abstraction spaghetti. A few unit tests and you'd be well on your way to shipping rather than messing around with beautiful patterns.
- sedatk 11mo agoObviously, because it was a fictional, simplified example for the post. My real use case was more complicated and involved multiple developers working on different parts of that flow. The problem with if-else chains is it's easy for a programmer to forget to handle a case that another developer added in the called component. Unit tests can't help a spec miscommunication. But, visitor pattern can as it forces the handling logic to be complete. Hence my example at the end using discriminated unions and exhaustive pattern matching in F#. Much, much simpler with the same benefits.
- tpmoney 11mo agoAs 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.
- sedatk 11mo agoSealed interface doesn’t look bad for some use cases. I wish C# had it too.
- neonsunset 11mo ago[dead]