menu

Повышение читаемости кода и функциональное программирование на Java Часть 2

Увеличиваем читаемость generic-ов – typedef для бедных

Плохо:

class FooEverythingDoer 
{
 ...
 Map<String, String> getProperties(Foo foo) {...}
 void putProperties(Foo foo, Map<String, String> properties) {...}
 Map<Foo, Map<String, String>> getPropertiesBatch(Iterable<Foo> foos) {...}
 Foo findByProperties(Map<String, String> partOfProperties) {...}
 ...
}

Хорошо:

class Properties extends Map<String,String> 
{
 (Конструкторы с сигнатурами базового класса)
}


class FooEverythingDoer 
{
 ...
 Properties getProperties(Foo foo) {...}
 void putProperties(Foo foo, Properties properties) {...}
 Map<Foo, Properties> getPropertiesBatch(Iterable<Foo> foos) {...}
 Foo findByProperties(Properties partOfProperties) {...}
 ...
}

Плохо:

class MagicBarMerger 
{
 public void mergeIntoDb(List<MagicBar> bars) 
 {
 List<MagicBar> existingBars = barDao.getAllBars();
 Map<Integer, List<Pair<MagicBar, MagicBar>>> pairsById = 
 joinOnId(bars, existingBars);
 List<MagicBar> merged = new ArrayList<MagicBar>();
 for(Pair<MagicBar, MagicBar> pair : pairsById) 
 {
 merged.add(merge(pair));
 }
 barDao.removeAllBars():
 barDao.putBarsBatch(merged);
 }

 private Map<Integer, Pair<MagicBar, MagicBar>> joinOnId(
 List<MagicBar> as, List<MagicBar> bs) 
 {
 ....
 }

 private MagicBar merge(Pair<MagicBar, MagicBar> bars) 
 {
 ....
 }
}

Хорошо:

class MagicBarMerger 
{
 private static class Bars extends Pair<MagicBar,MagicBar> 
 {
 (Конструктор с сигнатурой базового класса)
 }

 public void mergeIntoDb(List<MagicBar> bars) 
 {
 List<MagicBar> existingBars = barDao.getAllBars();
 Map<Integer, Bars> pairsById = joinOnId(bars, existingBars);
 List<MagicBar> merged = new ArrayList<MagicBar>();
 for(Bars bars : pairsById) {
 merged.add(merge(bars));
 }
 barDao.removeAllBars():
 barDao.putBarsBatch(merged);
}

 private Map<Integer, Bars> joinOnId(List<MagicBar> as, List<MagicBar> bs) 
 {
 ....
 }

 private MagicBar merge(Bars bars) 
 {
 ....
 }
}

Этот прием отчасти решает и проблему читаемости анонимных классов с типовыми параметрами:

Плохо:

import static CollectionUtils.*;

private static final Function<Customer, List<Order>> GET_ORDERS = 
 new Function<Customer, List<Order>>() 
 {
 public List<Order> apply(Customer customer) 
 {
 return customer.getOrders();
 }
 };

Хорошо:

interface CustomerFun<T> extends Function<Customer,T> {}
interface CustomerListFun<T> extends CustomerFun<List<T>> {}

import static CollectionUtils.*;

private static final CustomerListFun<Order> GET_ORDERS = 
 new CustomerListFun<Order>() 
 {
 public List<Order> apply(Customer customer) 
 {
 return customer.getOrders();
 }
 };

Увеличиваем читаемость generic-ов-2, используем вывод типов Java

Плохо:

Map<Integer, List<String>> namesById = new HashMap<Integer, List<String>>();

Хорошо:

public class CollectionFactory 
{
 public static <K,V> Map<K,V> newMap() 
 {
 return new HashMap<K,V>();
 }
}

import static CollectionUtils.newMap;
Map<Integer, List<String>> namesById = newMap();

Функции ar(T... ts) и set(T... ts) в правиле 1 – из этой же оперы.

Вполне можно в класс CollectionFactory положить обертки для всех конструкторов традиционных коллекций, а конструкторы Map'ов и Set'ов назвать не просто newMap/newSet, а соответственно newUnorderedMap/newLinkedMap и newUnorderedSet/newLinkedSet, тем самым сделав акцент на наличии или отсутствии требования упорядоченности и раз и навсегда избавив себя от связанных с этим проблем.

Практика показывает, что этот прием, будучи тайком внедренным в общую библиотеку, приживается среди коллег легче всего :)

Увеличиваем отлаживаемость этого хозяйства

В отладчике неприятно бывает увидеть, забравшись в кишки объекту, что его класс называется FooProcessor$3$1, а у его полей с именами innerProcessor и value – класс FooProcessor$4$2 и значение "1".

Поэтому следует все классы, объекты которых "уплывают" из локальной области видимости метода (а таких абсолютное большинство, согласно правилу №3), делать неанонимными и давать им осмысленные имена. Еще лучше реализовывать в классе метод toString, отображающий внутреннее состояние объекта. Это совсем недолго, а при отладке помогает неимоверно. Кроме того, избегание анонимных классов уменьшает число проблем с сериализацией.

Комбинаторы высшего порядка типа AND или OR часто применяются для склеивания заранее неизвестного числа операндов. При этом в памяти создается рекурсивная структура объектов – AND(x, AND(y, And(z, AND(...)))). Такую структуру неприятно просматривать в отладчике, и еще неприятней отлаживать ее пошагово. Поэтому иногда оправдано сделать, чтобы класс AND склеивал не 2, а произвольное число фильтров, а статический factory-метод Filters.and(first, second) (вы ведь не забыли абстрагировать в него конструктор, не так ли? ;) ) проверял, не является ли first или second уже и так AND'ом и, по возможности, склеивал их в один большой AND. Тогда рекурсивная структура превратится в итеративную и будет одно удовольствие смотреть ее в отладчике, сериализовать в XML и шастать отладчиком по циклу над слагаемыми.

Теперь тяжелая артиллерия, имеющая отношение собственно к ФП:

Прочитайте J vocabulary...

...и поймите, что делают примитивные функции и комбинаторы. Применять необязательно, достаточно просто понять, что они делают.http://www.jsoftware.com/help/dictionary/vocabul.htm. J – уникальный пример функционального языка без статической типизации и даже без замыканий. По сути, это означает, что большинство комбинаторов J можно реализовать и использовать и на Java, не скатываясь на анонимные классы. Мои любимые комбинаторы - "&." (f &. g = f-1 . g . f), "/." (x f/. y = вектор значений f(g) для каждой группы g вектора x по ключу y) и "/:" (x /: y - вектор x, отсортированный по вектору y).

Карринг по последнему аргументу

Функцию от нескольких аргументов можно интерпретировать как ответ на вопрос «Как Y зависит от x1, x2, …, xn?». Можно зафиксировать некоторые из xi и получить, например, ответ на вопрос «Как Y зависит от x2, x3, .., xn, если x1 = 5?» - получится функция от меньшего числа аргументов. Этот прием – фиксация некоторых аргументов – называется «карринг» и широко используется в функциональных языках.

Можно сказать, что функция от n аргументов – это функция от одного (первого) аргумента, возвращающая функцию от n-1 последующих. Обычно поддерживается фиксация нескольких первых аргументов функции – например, в случае функции plus(x, y) от двух числовых аргументов можно зафиксировать первый: plus(5) – и получится функция от одного аргумента y: plus(5)(y) = plus(5, y).

Вот более полезный пример: у функции matches(regex, str), проверяющей, подходит ли строка str под регулярное выражение regex, можно «закаррить» первый аргумент и получить самостоятельный «фильтратор по регулярному выражению regex», который можно сохранить в переменную, передать другой функции, применить к любой строке. Вскоре мы увидим более конкретные примеры.

Java не поддерживает карринг напрямую, однако ничто не мешает воспользоваться самой идеей.

В языках, поддерживающих карринг, существует «правило хорошего тона»: последний аргумент должен быть самым часто изменяющимся – тогда его будет удобно «каррить», поскольку будет иметь смысл сохранить функцию, частично примененную ко всем аргументам, кроме последнего, с тем, чтобы затем применить ее к нескольким различных значениям последнего аргумента..

Плохо:

public class CollectionUtils 
{
 public static <T> List<T> filter(Filter<T> filter, List<T> ts) {...}
 public static <K, V> List<V> map(Function<K, V> fun, List<K> ts) {...}
}

Хорошо:

public class CollectionUtils 
{
 public static <T> Function<List<T>, List<T>> filter(Filter<T> filter)
 {...}
 public static <K, V> Function<List<K>, List<V>> map(Function<K, V> fun)
 {...}
}

Почему:

Потому что теперь комбинаторам высшего порядка есть что комбинировать – вместо «фильтров» и «отображений», которые можно лишь применить к каким-нибудь конкретным спискам, появились «фильтраторы» и «отображатели», из которых можно собирать более сложные функции:

public class FP 
{
 public static <A,B,C> Function<A,C> 
 chain(Function<A,B> first, Function<B,C> second) {...}
}

или так:

public abstract class Function<K,V> {
 public abstract V apply(K argument);
 
 public Function<K,U> then(Function<V,U> second) 
 {
 return FP.chain(this, second);
 }
}

А теперь можно писать что-нибудь такое.

Пусть тип Order имеет три свойства (Customer, Product, Time) – «кто, что и когда заказал».

public abstract class Aggregate<K,V> extends Function<Collection<K>, V> {}

// select K,V from S group by K
public abstract class Partitioned<S,K,V> 
 extends Function<Collection<S>, Map<K, V>> {}

Function<Order,Product> getProduct() {..}
Function<Product,Price> getPrice() {..}
Filter<Product> categoryEquals(String pat) {..}
Partitioned<Order,Month,T> byMonth(Aggregate<Order, T> inner) {..}

далее

public Partitioned<Order,Month,Customer> mostGenerousCustomerByMonth() 
{
 Function<Order, Price> ORDER_PRICE = getProduct().then(getPrice());
 Aggregate<Order, Order> MOST_EXPENSIVE_ORDER =
 over(getProduct().then(categoryEquals("Cars")), 
 maximizeBy(ORDER_PRICE));
 return byMonth(MOST_EXPENSIVE_ORDER.then(Order.GET_CUSTOMER));
}

{
 ...
 Map<Month, Customer> goodGuysIn2007 = 
 filter(timeBetween(year(2007), year(2008))) // <---
 .then(mostGenerousCustomerByMonth())
 .apply(ourOrders);
 ... 
}

Код mostGenerousCustomerByMonth выглядит довольно плотно, если не сказать громоздко – однако он гораздо компактнее и читаемей, чем реализация без комбинаторов высшего порядка (попытка написать или представить таковую и сравнить ее с приведенной оставляется читателю в качестве упражнения).

Пары и бинарные отношения

Не так-то легко найти проект, в котором нету самописного класса Pair и не используются какие-нибудь List<Pair<Foo,Bar>>. Довольно часто возникают задачи вроде «собрать значения функции foo над левыми частями пар», или «собрать пары из значений foo на левой части и bar на правой», или «оставить только пары, у которых правая часть удовлетворяет предикату qux», и т.п. Было бы возмутительным не учредить для этого комбинаторы и не предоставить классу List<Pair<Foo,Bar>> возможность иметь их в качестве member methods:

class BiRelation<L,R> 
{
 ...
 List<Pair<L,R>> allEntries() {..}
 static BiRelation<L,R> rel(List<Pair<L,R>> pairs) {..}
 BiRelation<R,L> flip() {..}
 BiRelation<L,R> filterL(Predicate<L> p) {..}
 BiRelation<R,L> filterR(Predicate<R> p) {return flip().filterL(p).flip();}
 BiRelation<L,List<R>> compressL() {..}
 BiRelation<List<L>,R> compressR() {return flip().compressL().flip();}
 BiRelation<P,Q> fmap(Function<L,P> f, Function<R,Q> g) 
 {
 return rel(map(pair(f,g)).apply(allEntries()));
 }
 List<T> map(Function<Pair<L,R>, T> fun) {...}
 ...
}

Здесь также иллюстрируется еще один полезный прием – "worker/wrapper" (обертывание во flip : flip()/.../flip())

Теперь можно, например, написать поиск в ширину:

private static BiRelation<Node, Node> bfs(BiRelation<Node, Node> graph, Node root) 
{
 Set<Node> roots = singleton(root);
 Set<Node> reached = new LinkedHashSet<Node>();
 reached.add(root);
 List<Pair<Node,Node>> res = newList();
 while(true) 
 {
 BiRelation<Node,Node> nextLayer = 
 graph.filterL(memberOf(roots)).filterR(not(memberOf(reached))); // <---
 if(nextLayer.isEmpty())
 break;
 res.addAll(nextLayer.allEntries());
 reached.addAll(roots = nextLayer.rightSet());
 }
 return rel(res);
}

или преобразование Шварца (при сортировке массива по значению функции применяется для того, чтобы не вычислять функцию от одного и того же элемента несколько раз; суть – к каждому элементу в списке приклеиваем соответствующее ему значение функции, полученный список пар сортируем по второму элементу (т.е., по значению функции), и затем отбрасываем от каждой пары второй элемент.

public static Function<T, Pair<T,U>> attachR(Function<T,U> fun) {..}
public static Function<Pair<T,U>, T> detachR() {..}
public static Function<Pair<T,U>, U> detachL() {..}

public static <U extends Comparable<? super U>> 
Function<List<T>,List<T>> schwartzSortBy(Function<T,U> fun) 
{
 return map(attachR(fun)).then(sortBy(second())).then(map(detachR())); 
}

Или вот:

BiRelation<Customer, Order> customer_order = ...
BiRelation<Product, Integer> product_buyersCount = 
 customer_order.fmap(ID, GET_PRODUCT).groupByR().fmap(SIZE, ID).flip();

И много чего другого можно написать. Вообще, flip() – очень общая и полезная функция; существует много симметричных структур – пары, бинарные отношения, обратимые функции, Map'ы... А тройные отношения можно поворачивать – TriRelation<A,B,C>.rotate213() и т.п. Но злоупотреблять этим не стоит.

Крупные комбинаторы

Есть такая архитектура процессоров – VLIW, Very Large Instruction Word. В одной инструкции помещается сразу несколько действий – например, перемножить и сложить с аккумулятором; вычислить синус и косинус; и т.п.Там это делается для повышения параллелизма на уровне команд и вообще повышения быстродействия.

В случае ФП на Java (и не только на Java) это можно делать для повышения читаемости и понижения количества скобок. Ну, и для быстродействия тоже – крупный комбинатор можно реализовать не через мелкие, а более эффективно (например, избавляясь от промежуточных результатов – этот прием называется deforestation, или fusion, т.е. «сплавка» нескольких комбинаторов в один).

Вместо:

Aggregate<Customer, Order> everyonesOrdersIn2008 = 
 map(GET_ORDERS)
 .then(concat())
 .then(filter(timeBetween(year(2007), year(2008))))

Делаем так:

public static <K,V> Aggregate<K,V> concatMapOver(Function<K,V> fun, 
 Filter<K> filter) {...}

Aggregate<Customer, Order> everyonesOrdersIn2008 = 
 concatMapOver(GET_ORDERS, timeBetween(year(2007), year(2008)));

Заключение

Язык Java, с учетом возможностей Java 5, позволяет писать в значительно более читаемом стиле, нежели общепринятый, и позволяет даже использовать идеи функционального программирования. Автор надеется, что читатель найдет описанным приемам применение и испытает от их использования такую же радость. :)



Source: http://rsdn.ru/article/java/JavaFP.xml
Category: Java | Added by: tsvetkov (06.03.2009) | Author: Евгений Кирпичев aka jkff
Views: 1321 | Rating: 0.0/0