Тони Хоар ввел понятие нулего указателя в язык ALGOL W еще в 1965 году: "Просто это было легко реализовать". Через много лет Тони Хоар выразил сожаление в своей речи, названной "моя ошибка на миллиард долларов".
К сожалению, подавляющее большинство языков программирования, которые были созданы десятилетиями позже, имели тот же недостаток и создатели языков программирования вместе с рядовыми разработчиками начали искать возможность обойти печально известный
NullPointerException
.
Функциональные языки программирования, такие как Haskell и Scala обошли эту проблемы структурно, упаковав нулевые значения в монады. Другие, императивные языки программирования, такие как Groovy ввели нуль-безопасный оператор разыменования (оператор ?
), для безопасной работы с потенциально пустыми переменными.
Аналогичное решение, было предложено (а затем отклонено) для Java 7.
Честно говоря, я не скучаю по нуль-безопасному оператору разыменования в Java хотя бы потому, что я представляю как большинство разработчиков начнут злоупотреблять им из соображений «на всякий случай».
Не хочу углубляться в теорию категорий и объяснить, что такое монада, есть тонны очень хороших статей на эту тему.
Предлагаю быстро реализовать собственную монаду, используя синтаксис лямбда выражений в Java 8, а затем посмотреть как ее использовать на реальном примере. В Scala, монада M
это любой класс имеющий следующие 3 метода:
def map[B](f: A => B): M[B]
def flatMap[B](f: A => M[B]): M[B]
def filter(p: A => Boolean): M[A]
Можете думать о монаде Option
как об оболочке которой нет. Таким образом Option
от обобщенного типа A
, может быть определен следующим образом:
import java.util.functions.Predicate;
public abstract class Option<A> {
public static final None NONE = new None();
public abstract <B> Option<B> map(Func1<A, B> f);
public abstract <B> Option<B> flatMap(Func1<A, Option<B>> f);
public abstract Option<A> filter(Predicate<? super A> predicate);
public abstract A getOrElse(A def);
public static <A> Some<A> some(A value) {
return new Some(value);
}
public static <A> None<A> none() {
return NONE;
}
public static <A> Option<A> asOption(A value) {
if (value == null) return none(); else return some(value);
}
}
Я также добавил описание некоторых удобных методов Some
и None
в экземпляры Option
, которые я реализую позже.
Здесь Predicate
одиночный интерфейс определенный в новом пакете java.util.functions
:
public interface Predicate<T> {
boolean test(T t);
}
который используется для определения, соответствует ли вводимый объект заданным критериям, в то время как Func1
еще один единый интерфейсный метод:
public interface Func1<A1, R> {
R apply(A1 arg1);
}
что я определил представление более общий функцию одного аргумента типа A1
, возвращающей результат типа R
.
Абстрактный класс Option
имеет две конкретные реализации, одна None
для пустых значений (то, что к чему мы привыкли, оперируя ошибочной моделью с печально известной нулевой ссылкой)
public class None<A> extends Option<A> {
None() { }
@Override
public <B> Option<B> map(Func1<A, B> f) {
return NONE;
}
@Override
public <B> Option<B> flatMap(Func1<A, Option<B>> f) {
return NONE;
}
@Override
public Option<A> filter(Predicate<? super A> predicate) {
return NONE;
}
@Override
public A getOrElse(A def) {
return def;
}
}
вторая оборачивает фактически существующее значения:
public class Some<A> extends Option<A> {
private final A value;
Some(A value) {
this.value = value;
}
@Override
public <B> Option<B> map(Func1<A, B> f) {
return some(f.apply(value));
}
@Override
public <B> Option<B> flatMap(Func1<A, Option<B>> f) {
return f.apply(value);
}
@Override
public Option<A> filter(Predicate<? super A> predicate) {
if (predicate.test(value)) return this; else return None.NONE;
}
@Override
public A getOrElse(A def) {
return value;
}
}
Теперь, испытаем Option
на конкретном примере, допустим мы имеем Map<String, String>
описывающий набор именованных параметров с соответствующими значениями. Мы хотим разработать метод:
int readPositiveIntParam(Map<String, String> params, String name) { // TODO ... }
если ключ типа String
, ассоциирован со значением типа String
, где значение это строка описывающая положительное целое число, то нужно вернуть это число. Другими словами мы хотим пройти следующий тест:
@Test
public void testMap() {
Map<String, String> param = new HashMap<String, String>();
param.put("a", "5");
param.put("b", "true");
param.put("c", "-3");
// the value of the key "a" is a String representing a positive int so return it
assertEquals(5, readPositiveIntParam(param, "a"));
// returns zero since the value of the key "b" is not an int
assertEquals(0, readPositiveIntParam(param, "b"));
// returns zero since the value of the key "c" is an int but it is negative
assertEquals(0, readPositiveIntParam(param, "c"));
// returns zero since there is no key "d" in the map
assertEquals(0, readPositiveIntParam(param, "d"));
}
Если мы не можем рассчитывать на Option
, пришлось бы написать следующий код:
int readPositiveIntParam(Map<String, String> params, String name) {
String value = params.get(name);
if (value == null) return 0;
int i = 0;
try {
i = Integer.parseInt(value);
} catch (NumberFormatException nfe) { }
if (i < 0) return 0;
return i;
}
Слишком много условных переходов и возвращающихся значений, не правда ли? Используя монаду Option
, мы можем достичь того же результата одним оператором.
int readPositiveIntParam(Map<String, String> params, String name) {
return asOption(params.get(name))
.flatMap(FunctionUtils::stringToInt)
.filter(i -> i > 0)
.getOrElse(0);
}
где мы использовали вспомогательный статический метод FunctionUtils.stringToInt()
как функцию, с синтаксисом :: введеным в Java 8.
FunctionUtils.stringToInt
определяется как:
import static Option.*;
public class FunctionUtils {
public static Option<Integer> stringToInt(String s) {
try {
return some(Integer.parseInt(s));
} catch (NumberFormatException nfe) {
return none();
}
}
}
Этот метод пытается конвертировать строку в число, в случае неудачи возвращает свойство None
. Обратите внимание мы могли бы задать такое поведение при вызове flatMap()
с использованием анонимного лямбда-выражения, но я советую разработать небольшую библиотеку вспомогательных функций, как начал делать здесь, с целью оптимального использования функционального программирования.
Думаю сравнением двух методов readPositiveIntParam
, я наглядно продемонстрировал как используя монаду Option
, можно написать код свободный от NullPointerException
, в целом же используя функциональное программирование, вы можете снизить цикломатическую сложность своего кода.
Ссылка на оригинал
Перевел
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ