Подтвердить что ты не робот

Как я могу моделировать 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));
  }
}

)

4b9b3361

Ответ 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, - это сообщение об ошибке, так что похоже, что исключения действительно исправляют замену.