Как я могу моделировать Haskell "Либо b" в 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));
}
}
)
Ответы
Ответ 1
Моя общая формула для моделирования алгебраических типов данных:
- Тип - это абстрактный базовый класс, а конструкторы являются подклассами этого
- Данные для каждого конструктора определены в каждом подклассе. (Это позволяет конструкторам с различным количеством данных работать корректно, а также устраняет необходимость поддерживать инварианты, так как только одна переменная не имеет нулевого значения или что-то вроде этого).
- Конструкторы подклассов служат для построения значения для каждого конструктора.
- Чтобы деконструировать его, можно использовать
instanceof
для проверки конструктора и опускания соответствующего типа для получения данных.
Итак, для Either a b
это будет примерно так:
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
}
Ответ 2
Вот статически проверяемое безопасное решение; это означает, что вы не можете создавать ошибки времени выполнения. Пожалуйста, прочитайте предыдущее предложение в том смысле, в котором оно предназначено. Да, вы можете каким-то образом или иным образом спровоцировать исключения...
Это довольно многословие, но эй, это 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 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);
}
});
}
}
Возможно, вы захотите посмотреть Функциональная Java и Тони Моррис blog.
Здесь - ссылка на реализацию Either
в Functional Java. fold
в моем примере называется Either
. У них более сложная версия fold
, которая может возвращать значение (что кажется подходящим для стиля функционального программирования).
Ответ 3
Вы можете иметь близкое соответствие с Haskell, написав общий класс Either
, параметрический на двух типах L
и R
с двумя конструкторами (один принимает в L
, а один принимает в R
) и два метода L getLeft()
и R getRight()
, чтобы они либо возвращали значение, переданное при построении, либо генерировали исключение.
Ответ 4
Предложения, которые уже были предоставлены, хотя и осуществимы, не являются полными, поскольку они полагаются на некоторые ссылки null
и эффективно делают "Либо" маскарадом как кортеж значений. Дизъюнктная сумма, очевидно, относится к одному типу или другому.
В качестве примера я хотел бы взглянуть на реализацию FunctionalJava Either
.
Ответ 5
Самое главное - не пытаться писать на одном языке, а писать в другом. Как правило, в Java вы хотите поместить поведение в объект, вместо того, чтобы иметь "script", работающий снаружи, с инкапсуляцией, уничтоженной методами 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
(capital 'V'), если вы хотите вернуться к тому, чтобы не интересоваться возвращаемым значением.)
Ответ 6
Я реализовал его в стиле 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;
}
Надеюсь, что это поможет.
Ответ 7
Из 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; }
}
}
(начато с верхнего кода ответа и стиля)
Обратите внимание, что:
-
Финал в подклассе является необязательным. Без них вы можете подтипировать Left и Right, но все равно не напрямую. Таким образом, без final
Либо имеет ограниченную ширину, но неограниченную глубину.
-
С такими типами ADT я не вижу причин прыгать по всему андервальду против instanceof
. Булевы работают для Maybe или Lither, но в целом instanceof
- ваш лучший и единственный вариант.
Ответ 8
Благодаря 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, методах сопоставления для каждой стороны и т.д.
Ответ 9
Вам не нужно соглашаться с полями 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.
Библиотека
Этот код распространяется как часть моей библиотеки составных типов (Sums and Products, aka Unions и Tuples) из нескольких явлений. Это на GitHub:
https://github.com/nikita-volkov/composites.java
Ответ 10
Поскольку вы отметили 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
.
Ответ 11
Существует отдельная реализация Either
для Java 8 в небольшой библиотеке, "амбивалентность": http://github.com/poetix/ambivalence
Он ближе всего к стандартной реализации Scala - например, он предоставляет левые и правые выступы для операций map
и hashMap
.
Нет прямого доступа к левым или правым значениям; скорее, вы join
двух типов, предоставляя lambdas для их сопоставления в один тип результата:
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>
Ответ 12
Ближайшим, о котором я могу думать, является оболочка обоим значениям, которая позволяет вам проверить, какое значение установлено и получить:
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);
}
}
Ответ 13
На основе answer от 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();
}
Ответ 14
Измените свой дизайн, чтобы вам не понадобилась эта довольно абсурдная функция. Все, что вы делаете с возвращаемым значением, потребует какой-то конструкции if/else. Это было бы очень, очень уродливо.
Из быстрого Googling мне кажется, что единственное, что обычно используется Haskell, - это сообщение об ошибке, так что похоже, что исключения действительно исправляют замену.