Я был очарован концепцией лени с тех пор, как я впервые подумал об этом, пока немного изучал Хаскель . Лень — это идея о том, что вычисления не выполняются до тех пор, пока они не потребуются … идея, которая распространена в мире функционального программирования и которая лучше всего работает с неизменяемыми данными.
Почему неизменный? Это широко освещалось в других местах, но суть в том, что когда у вас есть какие-либо изменяемые данные (любое поле, которое может когда-либо изменить их значение), вы добавляете время в качестве невидимого ввода в ваши выражения. Буквально, время, когда любое отдельное выражение оценивается относительно других изменений в изменчивом состоянии, будет влиять на результат выражения, часто непредсказуемым образом. В математическом мире функция всегда будет возвращать одно и то же значение для одних и тех же входных данных … в нечетком, грязном мире объектной ориентации вызов метода может возвращать разные значения в разное время в зависимости от изменяемого состояния. Не обязательно изменяемое состояние в вызываемом объекте, но в каком-то другом объекте, где-то, от которого зависит вызываемый объект.
Вот почему параллельное программирование в мире ОО кажется таким сложным . Это требует блокировки изменяемых данных, что приводит к собственным проблемам, таким как взаимоблокировки . Это может походить на колеблющийся карточный домик.
Но уберите изменчивость из этой картины, и появится совершенно другой мир. Функции ведут себя как функции; одни и те же входы: тот же результат. Побочные эффекты исчезают, потому что нет изменяемого состояния. Оценка выражений больше не связана со временем : она может оцениваться в параллельных потоках или может быть отложена до тех пор, пока она не станет абсолютно необходимой.
Последний бит — это лень. Лень — это способ загрузить ваш код до более простого и ясного выражения ваших алгоритмов … как только вы охватите лень, вы увидите, что большое количество кода, которое вы пишете (особенно с изменяемыми данными), является случаем преждевременной оптимизации ,
Вернуться к Гобелену; еще в Tapestry 4.0 (где HiveMind и использование Inversion of Control и Dependency Injection, где она была представлена) внутренний код Tapestry имел много функциональных характеристик. Базовая единица работы в контейнере Tapestry IoC — это интерфейс, а не функция … но часто эти интерфейсы имеют один метод. Это делает интерфейсы похожими на функции, готовые к такой композиции, которая возможна на функциональном языке программирования. Конечно, это немного неуклюже по сравнению с реальным функциональным языком программирования … но сила все еще там.
Tapestry 5 использует эти функции для обработки многих концепций Аспектно-ориентированного программирования; например, услуги лениво создаются, и их можно оформить и посоветовать для обеспечения сквозных задач. На самом деле Tapestry широко использует функциональную композицию для всех видов метапрограммирования .
Между тем, вне сферы Tapestry, мое знакомство с Clojure действительно продало мне функциональный подход, и я использую неизменные структуры данных, такие как теплое, утешительное одеяло. Я скучаю по всему этому, когда работаю с обычными списками и наборами из API коллекций.
Учитывая, что Tapestry делает много сложных вещей, я начал работать над простой функциональной библиотекой. То, что я создал, не так сложно, как функциональная Java ; Я думаю, что это делает меньше, но делает это более чисто. Это более сфокусировано.
Идея состоит в том, что вы создадите поток из некоторого источника (обычно из коллекции). Затем вы можете отображать, фильтровать, уменьшать, добавлять, объединять и повторять значения внутри потока. Кроме того, потоки ленивы (как в случае с Haskell и Clojure); вся оценка откладывается до тех пор, пока она не станет абсолютно необходимой, и она безопасна для потока. Вы также можете иметь бесконечные потоки.
Все начинается в статическом классе F (для F ), который имеет начальные фабрики для потоков. В этом примере метод F.range () используется для создания потока для диапазона целых чисел:
System.out.println(F.range(1, 100).filter(F.gt(10)).map( new Mapper() { public Integer map(Integer value) { return value * 2; } }).take(3).toList());
При выполнении этот код печатает следующее: [22, 24, 26]. То есть сбрасывались значения, меньшие или равные 10; затем он умножил каждое оставшееся значение на 2 и преобразовал первые три в список.
- F.range () создает ленивый поток целых чисел в диапазоне (от 1 до 99; верхний диапазон не включается).
- filter () — это метод Flow, который сохраняет только некоторые значения на основе предиката
- F.gt () — метод статической фабрики, он создает предикат, который сравнивает числовое значение из потока с предоставленным значением
- map () — это метод Flow, который применяется к каждому значению в потоке
- take () принимает ограниченное количество значений с начала потока
- toList () преобразует поток в неизменяемый список
Здесь мы сопоставили Integer с Integer, но было бы возможно отобразить другой тип На каждом этапе создается новый (неизменяемый) объект Flow.
Как насчет лени? Хорошо, если мы немного изменим код:
System.out.println(F.range(1, 100).filter(F.gt(10)).map( new Mapper() { public Integer map(Integer value) { System.out.println("** mapping " + value); return value * 2; } }).take(3).toList());
Новый вывод:
** mapping 11 ** mapping 12 ** mapping 13 [22, 24, 26]
… другими словами, хотя мы пишем код, который создает впечатление, что весь поток преобразуется вызовом map (), реальность такова, что отдельные значения из исходного потока отображаются только один раз по мере необходимости. Код, который мы пишем, фокусируется на потоке преобразований от ввода до конечного результата: «начни с диапазона, сохрани значения больше 10, умножь каждое на два, сохрани только первые три».
Это имеет значение? Не в таких тривиальных случаях, как этот пример. Функциональный код может быть переписан в стандартной Java как:
List<Integer> result = new ArrayList<Integer>(); for (int i = 1; i < 100; i++) { if (i >= 10) { result.add(i * 2); } } result = result.subList(0, 3); System.out.println(result);
Да, этот код короче, но он выполняет больше работы (вычисляя много двойных значений, которые не нужны). Мы могли бы проделать некоторую дополнительную работу, чтобы сохранить количество значений результата и завершить цикл раньше, но это еще больше увеличивает цикломатическую сложность . Дополнительная работа здесь не имеет большого значения, но если преобразования были бы более дорогостоящими (скажем, перерисовка изображений разных размеров или чтение данных из базы данных), работа, не выполненная без необходимости , стала бы весьма значительной.
И действительно ли традиционный код Java действительно короче? Что если мы создадим фабричную функцию многократного использования:
public static Mapper<Integer,Integer> multiplyBy(final int multiplicand) { return new Mapper<Integer, Integer>() { public Integer map(Integer value) { return value * multiplicand; } }; }
Тогда наши оригинальные выражения Flow становятся:
System.out.println(F.range(1, 100).filter(F.gt(10)).map(multiplyBy(2)).take(3).toList());
… это означает, что, когда у нас будет хорошая коллекция этих фабричных методов Mapper и Predicate, мы сможем получить эффективный, ленивый код, и он будет более кратким и читаемым.
В любом случае, tapestry-func находится в стадии разработки, но он очень многообещающий, и уже используется как в Tapestry 5.2, так и в коде некоторых моих клиентов.
От http://tapestryjava.blogspot.com/2010/06/who-wants-func-gotta-have-that-func.html