How can I simulate Haskell's “Either a b” in Java

后端 未结 14 2198
梦毁少年i
梦毁少年i 2020-12-07 22:40

How can I write a typesafe Java method that returns either something of class a or something of class b? For example:

public ... either(boolean b) {
  if (b)         


        
相关标签:
14条回答
  • 2020-12-07 23:09

    My general formula for simulating algebraic data types is:

    • The type is an abstract base class, and the constructors are subclasses of that
    • The data for each constructor are defined in each subclass. (This allows constructors with different numbers of data to work correctly. It also removes the need to maintain invariants like only one variable is non-null or stuff like that).
    • The constructors of the subclasses serve to construct the value for each constructor.
    • To deconstruct it, one uses instanceof to check the constructor, and downcast to the appropriate type to get the data.

    So for Either a b, it would be something like this:

    abstract class Either<A, B> { }
    class Left<A, B> extends Either<A, B> {
        public A left_value;
        public Left(A a) { left_value = a; }
    }
    class Right<A, B> extends Either<A, B> {
        public B right_value;
        public Right(B b) { right_value = b; }
    }
    
    // to construct it
    Either<A, B> foo = new Left<A, B>(some_A_value);
    Either<A, B> bar = new Right<A, B>(some_B_value);
    
    // to deconstruct it
    if (foo instanceof Left) {
        Left<A, B> foo_left = (Left<A, B>)foo;
        // do stuff with foo_left.a
    } else if (foo instanceof Right) {
        Right<A, B> foo_right = (Right<A, B>)foo;
        // do stuff with foo_right.b
    }
    
    0 讨论(0)
  • 2020-12-07 23:10

    Here is a statically checked type-safe solution; this means you cannot create runtime errors. Please read the previous sentence in the way it is meant. Yes, you can provoke exceptions in some way or the other...

    It's pretty verbose, but hey, it's Java!

    public class Either<A,B> {
        interface Function<T> {
            public void apply(T x);
        }
    
        private A left = null;
        private B right = null;
        private Either(A a,B b) {
            left = a;
            right = b;
        }
    
        public static <A,B> Either<A,B> left(A a) {
            return new Either<A,B>(a,null);
        }
        public static <A,B> Either<A,B> right(B b) {
            return new Either<A,B>(null,b);
        }
    
        /* Here's the important part: */
        public void fold(Function<A> ifLeft, Function<B> ifRight) {
            if(right == null)
                ifLeft.apply(left);
            else
                ifRight.apply(right);
        }
    
        public static void main(String[] args) {
            Either<String,Integer> e1 = Either.left("foo");
            e1.fold(
                    new Function<String>() {
                        public void apply(String x) {
                            System.out.println(x);
                        }
                    },
                    new Function<Integer>() {
                        public void apply(Integer x) {
                            System.out.println("Integer: " + x);
                        }
                    });
        }
    }
    

    You might want to look at Functional Java and Tony Morris' blog.

    Here is the link to the implementation of Either in Functional Java. The fold in my example is called either there. They have a more sophisticated version of fold, that is able to return a value (which seems appropriate for functional programming style).

    0 讨论(0)
  • 2020-12-07 23:11

    You don't need to settle with the instanceof checks or redundant fields. Surprisingly enough, Java's type system provides enough features to simulate the sum types cleanly.

    Background

    First of all, do you know that any data type can be encoded with just functions? It's called Church encoding. E.g., using the Haskell signature, the Either type could be defined as follows:

    type Either left right =
      forall output. (left -> output) -> (right -> output) -> output
    

    You can interpret it as "given a function on the left value and a function on the right value, produce the result of either of them".

    Definition

    Expanding on this idea, in Java we can define an interface called Matcher, which includes both functions and then define the Sum type in terms of how to pattern-match on it. Here's the complete code:

    /**
     * A sum class which is defined by how to pattern-match on it.
     */
    public interface Sum2<case1, case2> {
    
      <output> output match(Matcher<case1, case2, output> matcher);
    
      /**
       * A pattern-matcher for 2 cases.
       */
      interface Matcher<case1, case2, output> {
        output match1(case1 value);
        output match2(case2 value);
      }
    
      final class Case1<case1, case2> implements Sum2<case1, case2> {
        public final case1 value;
        public Case1(case1 value) {
          this.value = value;
        }
        public <output> output match(Matcher<case1, case2, output> matcher) {
          return matcher.match1(value);
        }
      }
    
      final class Case2<case1, case2> implements Sum2<case1, case2> {
        public final case2 value;
        public Case2(case2 value) {
          this.value = value;
        }
        public <output> output match(Matcher<case1, case2, output> matcher) {
          return matcher.match2(value);
        }
      }
    
    }
    

    Usage

    And then you can use it like this:

    import junit.framework.TestCase;
    
    public class Test extends TestCase {
    
      public void testSum2() {
        assertEquals("Case1(3)", longOrDoubleToString(new Sum2.Case1<>(3L)));
        assertEquals("Case2(7.1)", longOrDoubleToString(new Sum2.Case2<>(7.1D)));
      }
    
      private String longOrDoubleToString(Sum2<Long, Double> longOrDouble) {
        return longOrDouble.match(new Sum2.Matcher<Long, Double, String>() {
          public String match1(Long value) {
            return "Case1(" + value.toString() + ")";
          }
          public String match2(Double value) {
            return "Case2(" + value.toString() + ")";
          }
        });
      }
    
    }
    

    With this approach you can even find a direct resemblance of pattern-matching in such languages as Haskell and Scala.

    Library

    This code is distributed as part of my library of composite types (Sums and Products, aka Unions and Tuples) of multiple arities. It's on GitHub:

    https://github.com/nikita-volkov/composites.java

    0 讨论(0)
  • 2020-12-07 23:11

    Based on the answer by Riccardo, following code snippet worked for me:

    public class Either<L, R> {
            private L left_value;
            private R right_value;
            private boolean right;
    
            public L getLeft() {
                if(!right) {
                    return left_value;
                } else {
                    throw new IllegalArgumentException("Left is not initialized");
                }
            }
    
            public R getRight() {
                if(right) {
                    return right_value;
                } else {
                    throw new IllegalArgumentException("Right is not initialized");
                }
            }
    
            public boolean isRight() {
                return right;
            }
    
            public Either(L left_v, Void right_v) {
               this.left_value = left_v;
               this.right = false;
            }
    
            public Either(Void left_v, R right_v) {
              this.right_value = right_v;
              right = true;
            }
    
        }
    

    Usage:

    Either<String, Integer> onlyString = new Either<String, Integer>("string", null);
    Either<String, Integer> onlyInt = new Either<String, Integer>(null, new Integer(1));
    
    if(!onlyString.isRight()) {
      String s = onlyString.getLeft();
    }
    
    0 讨论(0)
  • 2020-12-07 23:12

    The big thing is not to try to write in one language whilst writing in another. Generally in Java you want to put the behaviour in the object, rather than having a "script" running outside with encapsulation destroyed by get methods. There is no context for making that kind of suggestion here.

    One safe way of dealing with this particular little fragment is to write it as a callback. Similar to a very simple visitor.

    public interface Either {
        void string(String value);
        void integer(int value);
    }
    
    public void either(Either handler, boolean b) throws IntException {
        if (b) {
            handler.string("test");
        } else {
            handler.integer(new Integer(1));
        }
    }
    

    You may well want to implement with pure functions and return a value to the calling context.

    public interface Either<R> {
        R string(String value);
        R integer(int value);
    }
    
    public <R> R either(Either<? extends R> handler, boolean b) throws IntException {
        return b ?
            handler.string("test") :
            handler.integer(new Integer(1));
    }
    

    (Use Void (capital 'V') if you want to get back to being uninterested in the return value.)

    0 讨论(0)
  • 2020-12-07 23:16

    Thanks to Derive4J algebraic data types are now very easy in Java. All you have to do is create the following class:

    import java.util.function.Function;
    
    @Data
    public abstract class Either<A, B> {
    
      Either(){}
    
      /**
       * The catamorphism for either. Folds over this either breaking into left or right.
       *
       * @param left  The function to call if this is left.
       * @param right The function to call if this is right.
       * @return The reduced value.
       */
      public abstract <X> X either(Function<A, X> left, Function<B, X> right);
    }
    

    And Derive4J will take care of creating constructors for the left and rights cases, as well as a pattern matching syntax alla Haskell, mapper methods for each sides, and more.

    0 讨论(0)
提交回复
热议问题