繁体   English   中英

如何在Java中模拟Haskell的“Either a b”

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

我怎样才能编写一个类型安全的Java方法来返回类a或类b的东西? 例如:

public ... either(boolean b) {
  if (b) {
    return new Integer(1);
  } else {
    return new String("hi");
  }
}

什么是最干净的方式?

(我想到的唯一一件事就是使用明显不好的异常,因为它滥用了一般语言功能的错误处理机制......

public String either(boolean b) throws IntException {
  if (b) {
    return new String("test");
  } else {
    throw new IntException(new Integer(1));
  }
}

我模拟代数数据类型的通用公式是:

  • 类型是抽象基类,构造函数是它的子类
  • 每个构造函数的数据在每个子类中定义。 (这允许具有不同数据数量的构造函数正常工作。它还消除了维护不变量的需要,例如只有一个变量是非null或类似的东西)。
  • 子类的构造函数用于构造每个构造函数的值。
  • 要解构它,可以使用instanceof检查构造函数,然后向下转换为适当的类型以获取数据。

因此,对于Either ab ,它将是这样的:

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
}

这是一种静态检查的类型安全解决方案; 这意味着您无法创建运行时错误。 请按照其含义阅读上一句。 是的,你可以用某种方式激起例外......

它非常冗长,但是嘿,这是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);
                    }
                });
    }
}

您可能想看看Functional Java和Tony Morris的博客

以下是在Functional Java中实现Either的链接。 fold在我的例子被称为either在那里。 它们有一个更复杂的fold版本,它能够返回一个值(这似乎适合函数式编程风格)。

您可以通过编写一个通用类与哈斯克尔密切的对应Either对两种类型,参数LR与两个构造(一个以在L ,并且在一个了结R和两个方法L getLeft()R getRight() ,使得他们要么返回施工时传递的值,或抛出异常。

已经提供的建议虽然可行,但并不完整,因为它们依赖于一些null引用并有效地使“Either”伪装成值的元组。 不相交的总和显然是一种类型或另一种。

我建议看看FunctionalJavaEither的实现作为一个例子。

最重要的是不要试图用另一种语言写一种语言。 通常在Java中,您希望将行为放在对象中,而不是让“脚本”在外部运行,并且get方法会破坏封装。 这里没有提出这种建议的背景。

处理这个特定小片段的一种安全方法是将其写为回调。 类似于一个非常简单的访客。

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));
    }
}

您可能希望使用纯函数实现并将值返回给调用上下文。

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));
}

(如果你想回到对返回值不感兴趣的话,请使用Void (大写'V')。)

我已经通过以下方式以类似Scala的方式实现了它。 它有点冗长(毕竟它是Java):)但它的类型安全。

public interface Choice {    
  public enum Type {
     LEFT, RIGHT
  }

  public Type getType();

  interface Get<T> {
     T value();
  }
}

public abstract class Either<A, B> implements Choice {

  private static class Base<A, B> extends Either<A, B> {
    @Override
    public Left leftValue() {
      throw new UnsupportedOperationException();
    }

    @Override
    public Right rightValue() {
      throw new UnsupportedOperationException();
    }

    @Override
    public Type getType() {
      throw new UnsupportedOperationException();
    }
  }

  public abstract Left leftValue();

  public abstract Right rightValue();

  public static <A, B> Either<A, B> left(A value) {
    return new Base<A, B>().new Left(value);
  }

  public static <A, B> Either<A, B> right(B value) {
    return new Base<A, B>().new Right(value);
  }

  public class Left extends Either<A, B> implements Get<A> {

    private A value;

    public Left(A value) {
      this.value = value;
    }

    @Override
    public Type getType() {
      return Type.LEFT;
    }

    @Override
    public Left leftValue() {
      return Left.this;
    }

    @Override
    public Right rightValue() {
      return null;
    }

    @Override
    public A value() {
      return value;    
    }
  }

  public class Right extends Either<A, B> implements Get<B> {

    private B value;

    public Right(B value) {
      this.value = value;
    }

    @Override
    public Left leftValue() {
      return null;
    }

    @Override
    public Right rightValue() {
      return this;
    }

    @Override
    public Type getType() {
      return Type.RIGHT;
    }

    @Override
    public B value() {
      return value;
    }
  }
}

然后,您可以在代码上传递Either<A,B>实例。 Type枚举主要用于switch语句。

创建Either值都很简单:

Either<A, B> underTest;

A value = new A();

underTest = Either.left(value);

assertEquals(Choice.Type.LEFT, underTest.getType());
assertSame(underTest, underTest.leftValue());
assertNull(underTest.rightValue());
assertSame(value, underTest.leftValue().value());

或者,在使用它而不是例​​外的典型情况下,

public <Error, Result> Either<Error,Result> doSomething() {
    // pseudo code
    if (ok) {
        Result value = ...
        return Either.right(value);
    } else {
        Error errorMsg = ...
        return Either.left(errorMsg);
    }
}

// somewhere in the code...

Either<Err, Res> result = doSomething();
switch(result.getType()) {
   case Choice.Type.LEFT:
      // Handle error
      Err errorValue = result.leftValue().value();
      break;
   case Choice.Type.RIGHT:
      // Process result
      Res resultValue = result.rightValue().value();
      break;
}

希望能帮助到你。

来自http://blog.tmorris.net/posts/maybe-in-java/我了解到你可以将外部类的构造函数设为私有,因此只有嵌套类可以对它进行子类化。 这个技巧就像上面最好的类型一样安全,但更简洁,适用于你想要的任何ADT,如Scala的案例类。

public abstract class Either<A, B> {
    private Either() { } // makes this a safe ADT
    public abstract boolean isRight();
    public final static class Left<L, R> extends Either<L, R>  {
        public final L left_value;
        public Left(L l) { left_value = l; }
        public boolean isRight() { return false; }
    }
    public final static class Right<L, R> extends Either<L, R>  {
        public final R right_value;
        public Right(R r) { right_value = r; }
        public boolean isRight() { return true; }
    }
}

(从最佳答案的代码和风格开始)

注意:

  • 子类的决赛是可选的。 如果没有它们,您可以左键和右键,但仍然不能直接键入。 因此没有final s要么宽度有限但是无限深度。

  • 有了这样的ADT的,我认为没有理由跳上全抗instanceof行列。 布尔值适用于Maybe或Either,但通常instanceof是您最好的唯一选择。

感谢Derive4J代数数据类型现在在Java中非常容易。 您所要做的就是创建以下类:

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);
}

Derive4J将负责为左侧和权限案例创建构造函数,以及模式匹配语法alla Haskell,每侧的映射器方法等。

在一个小型库中有一个独立的Either for Java 8实现,“矛盾”: http//github.com/poetix/ambivalence

它最接近Scala标准实现 - 例如,它为maphashMap操作提供左右投影。

无法直接访问左侧或右侧值; 相反,您通过提供lambdas将它们映射到单个结果类型来join这两种类型:

Either<String, Integer> either1 = Either.ofLeft("foo");
Either<String, Integer> either2 = Either.ofRight(23);
String result1 = either1.join(String::toUpperCase, Object::toString);
String result2 = either2.join(String::toUpperCase, Object::toString);

你可以从Maven中心获得它:

<dependency>
    <groupId>com.codepoetics</groupId>
    <artifactId>ambivalence</artifactId>
    <version>0.2</version>
</dependency>

您无需使用instanceof检查或冗余字段。 令人惊讶的是,Java的类型系统提供了足够的功能来干净地模拟总和类型。

背景

首先,您是否知道任何数据类型都可以使用函数进行编码? 它被称为教会编码 例如,使用Haskell签名, Either类型可以定义如下:

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

您可以将其解释为“给定左值上的函数和右值上的函数,生成其中任何一个的结果”。

定义

扩展这个想法,在Java中我们可以定义一个名为Matcher的接口,它包含两个函数,然后根据如何在其上进行模式匹配来定义Sum类型。 这是完整的代码:

/**
 * 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);
    }
  }

}

用法

然后你可以像这样使用它:

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() + ")";
      }
    });
  }

}

通过这种方法,您甚至可以在Haskell和Scala等语言中找到模式匹配的直接相似性。

图书馆

此代码作为我的多个arities的复合类型库(Sums and Products,又名Unions和Tuples)的一部分进行分发。 它在GitHub上:

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

既然你已经标记了Scala,我会给出一个Scala答案。 只需使用现有的Either类。 这是一个示例用法:

def whatIsIt(flag: Boolean): Either[Int,String] = 
  if(flag) Left(123) else Right("hello")

//and then later on...

val x = whatIsIt(true)
x match {
  case Left(i) => println("It was an int: " + i)
  case Right(s) => println("It was a string: " + s)
}

这是完全类型安全的; 你不会有擦除或类似的问题......如果你根本不能使用Scala,至少可以用它作为你如何实现自己的Either类的一个例子。

根据Riccardo的回答 ,以下代码片段为我工作:

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;
        }

    }

用法:

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();
}

我能想到的最接近的是两个值的包装器,它允许您检查设置的值并检索它:

class Either<TLeft, TRight> {
    boolean isLeft;

    TLeft left;
    TRight right;

    Either(boolean isLeft, TLeft left1, TRight right) {
        isLeft = isLeft;
        left = left;
        this.right = right;
    }

    public boolean isLeft() {
        return isLeft;
    }

    public TLeft getLeft() {
        if (isLeft()) {
            return left;
        } else {
            throw new RuntimeException();
        }
    }

    public TRight getRight() {
        if (!isLeft()) {
            return right;
        } else {
            throw new RuntimeException();
        }
    }

    public static <L, R> Either<L, R> newLeft(L left, Class<R> rightType) {
        return new Either<L, R>(true, left, null);
    }

    public static <L, R> Either<L, R> newRight(Class<L> leftType, R right) {
        return new Either<L, R>(false, null, right);
    }
}

class Main {
    public static void main(String[] args) {
        Either<String,Integer> foo;
        foo = getString();
        foo = getInteger();
    }

    private static Either<String, Integer> getInteger() {
        return Either.newRight(String.class, 123);
    }

    private static Either<String, Integer> getString() {
        return Either.newLeft("abc", Integer.class);
    }   
}

改变你的设计,这样你就不需要这个荒谬的功能了。 你对返回值做的任何事情都需要某种if / else结构。 它会非常非常难看。

从一个快速的谷歌搜索,在我看来,Haskell的Either唯一常用的是错误报告,所以看起来异常实际上是纠正替换。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM