Показаны сообщения с ярлыком functionaljava. Показать все сообщения
Показаны сообщения с ярлыком functionaljava. Показать все сообщения

среда, 17 июня 2015 г.

Методы, принимающие на вход Object

Возможно я буду уже 1000-ным (или 10000-ным) человеком, который это говорит, но я всё же повторю ещё раз.

Методы, у которых есть аргументы типа Object – это полнейший пиздец.

Примеры таких методов:
  • Object.equals(Object)
  • Objects.equals(Object, Object)
  • Collection.contains(Object)
  • Collection.remove(Object)
  • Map.containsKey(Object)
  • Map.containsValue(Object)
Почему это лютейший пиздец, я поясню на примере. Допустим, у вас в программе есть переменная типа Set<Integer> и где-то в коде есть проверка:
Integer x = ...;
if (set.contains(x)) {
  // Do something
}
Теперь в один прекрасный день вам нужно поменять тип множества, скажем, на Set<String>. Вы меняете, и... код компилируется! Но проверка теперь работает некорректно, т.к. вы проверяете, содержит ли ваше множество число, что всегда будет возвращать false. Вы сделали фатальную ошибку в программе, но компилятор вас об этом не предупредил!
Так жить нельзя. Java – это пример статического языка, который не является типобезопасным. Статически типизированный и нетипобезопасный язык – что это если не пиздец?
Есть ли решение у этой проблемы? Ну не знаю, equals(Object) навсегда вшит во все объекты. Так будет в Java 9, 10, 11 и в 20. Поменять сигнатуру метода невозможно, т.к. это нарушит обратную совместимость. Можно использовать typeclass-подход, который используется в functionaljava, но разве кто-нибудь так будет делать?
Остаётся надеяться на IDE и статические анализаторы, хотя это частичное решение, не полное.

вторник, 19 мая 2015 г.

Числа с плавающей точкой не являются ассоциативными

Ассоциативность  это свойство, при котором для любых трёх элементов множества x, y, z выполняется равенство (x + y) + z = x + (y + z). Многие множества удовлетворяют этому свойству:
  • Целые числа (в том числе машинные целые числа) по операции сложения / умножения.
  • Строки по операции конкатенации.
  • Предикаты по операции конъюнкции / дизъюнкции.  
  • ... и многие многие другие.
Однако некоторые множества не являются ассоциативными, например, числа с плавающей точкой (по сложению / умножению).
Это легко проверить. Для этого мы воспользуемся библиотекой functionaljava, в которой есть возможность тестирования свойств (аля Haskell QuickCheck). Тест будет выглядеть следующим образом:
Property prop = Property.property(Arbitrary.arbDouble, Arbitrary.arbDouble, Arbitrary.arbDouble,
        (x, y, z) -> Property.prop((x + y) + z == x + (y + z)));

CheckResult check = prop.check();

System.out.println("Passed: " + check.isPassed());
Если выполнить этот код, то он выведет false. У класса CheckResult также можно спросить, какие именно числа нарушают ассоциативность:
for (List<Arg<?>> args : check.args()) {
    double x = (Double) args.index(0).value();
    double y = (Double) args.index(1).value();
    double z = (Double) args.index(2).value();
    System.out.printf("(%s + %s) + %s = %s%n", x, y, z, (x + y) + z);
    System.out.printf("%s + (%s + %s) = %s%n", x, y, z, x + (y + z));
}
Этот код выведет примерно следующее:
(5.094895128230203 + -7.839963840335238) + -1.8589779099062191 = -4.604046622011254
5.094895128230203 + (-7.839963840335238 + -1.8589779099062191) = -4.6040466220112535
Как видите, при разной расстановке скобок результаты будут разными.

понедельник, 26 января 2015 г.

Ликбез по Either

Есть два типа Either:
Симметричный Either реализован полностью зеркально и не отдаёт предпочтения ни левому, ни правому параметру. Это означает, что методы map и flatMap не имеют смысла, т.к. непонятно, над каким параметром производить действие: над левым или над правым. Поэтому вводятся дополнительные сущности LeftProjection и RightProjection, и чтобы можно было писать for-comprehension, нужно сначала позвать методы either.left() или either.right().
Scalaz и Haskell используют другой подход. Так как в 99% случаев Either используются для возвращения из функций либо значений, либо ошибок, то можно договориться, что мы значение всегда кладём в Right и на нём же реализуем методы map и flatMap. Таким образом, необходимость в проекциях отпадает, и мы спокойно пишем for-comprehension:
val either1: String \/ Int = \/-(3)
val either2: String \/ Int = \/-(4)
 
val either3 = for {
  val1 <- either1
  val2 <- either2
} yield val1 + val2
 
println(either3) // Печатает \/-(7)

Мне больше нравится смещённый вправо Either, так как код становится чище и лаконичнее. А если возникнет необходимость сделать for-comprehension с левой частью, можно использовать метод swap.