Визначення функціонального програмування
Вступ до «Радості Клоджура» говорить про наступне:
Функціональне програмування - один з тих обчислювальних термінів, що має аморфне визначення. Якщо ви запитаєте 100 програмістів про їх визначення, ви, ймовірно, отримаєте 100 різних відповідей ...
Функціональне програмування стосується та полегшує застосування та склад функцій ... Щоб мова вважалася функціональною, її поняття функції повинно бути першокласним. Функції першого класу можна зберігати, передавати та повертати, як і будь-який інший фрагмент даних. Поза межами цієї основної концепції [визначення FP може включати] чистоту, незмінність, рекурсію, лінь та референтну прозорість.
Програмування в Scala 2nd Edition p. 10 має таке визначення:
Функціональне програмування керується двома основними ідеями. Перша ідея полягає в тому, що функції - це першокласні значення ... Ви можете передавати функції як аргументи іншим функціям, повертати їх як результати функцій або зберігати їх у змінних ...
Друга основна ідея функціонального програмування полягає в тому, що операції програми повинні відображати вхідні значення на вихідні значення, а не змінювати дані на місці.
Якщо ми приймаємо перше визначення, то єдине, що вам потрібно зробити, щоб зробити ваш код «функціональним» - це перетворити петлі зсередини. Друге визначення включає незмінність.
Функції першого класу
Уявіть, що ви зараз отримуєте Список пасажирів від об'єкта Автобус, і ви повторюєте його, зменшуючи банківський рахунок кожного пасажира на суму тарифу на автобус. Функціональним способом виконання цієї самої дії було б мати метод на Bus, який може бути названий forEachPassenger, який приймає функцію одного аргументу. Тоді Автобус повторить своїх пасажирів, але найкраще це зробити, і код вашого клієнта, який стягує вартість проїзду, буде введений у функцію та переданий ForEachPassenger. Вуаля! Ви використовуєте функціональне програмування.
Обов’язкові:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Функціональний (використовуючи анонімну функцію або "лямбда" в Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Більш цукриста версія Scala:
myBus = myBus.forEachPassenger(_.debit(fare))
Не першокласні функції
Якщо ваша мова не підтримує першокласні функції, це може стати дуже потворним. У Java 7 або новіших версіях ви повинні надати інтерфейс "Функціональний об'єкт", наприклад такий:
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
Тоді клас Bus забезпечує внутрішній ітератор:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Нарешті, ви передаєте об'єкт анонімної функції на Шину:
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
Java 8 дозволяє захоплювати локальні змінні сферою дії анонімної функції, але в більш ранніх версіях будь-які такі варіації повинні бути оголошені остаточними. Щоб обійти це, можливо, вам доведеться скласти обгортковий клас MutableReference. Ось цілочисельний клас, який дозволяє додавати лічильник циклу до вищевказаного коду:
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
Навіть при цій потворності іноді вигідно усунути складну та повторювану логіку із циклів, що поширюються у вашій програмі, надавши внутрішній ітератор.
Ця потворність була зафіксована в Java 8, але обробка перевірених винятків усередині функції першого класу все ще по-справжньому потворна, і Java все ще несе припущення про незмінність у всіх своїх колекціях. Що підводить нас до інших цілей, часто пов'язаних з ПП:
Незмінюваність
Пункт 13 Джоша Блоха - "Віддавайте перевагу незмінності". Незважаючи на поширені розмови про сміття, навпаки, OOP можна робити з незмінними предметами, і це робить це набагато краще. Наприклад, рядок у Java незмінний. StringBuffer, OTOH потрібно змінювати, щоб створити незмінний рядок. Деякі завдання, як-от робота з буферами, по суті потребують змін.
Чистота
Кожна функція повинна бути принаймні запам'ятовуваною - якщо ви даєте їй однакові вхідні параметри (і вона не повинна мати жодного вводу, окрім фактичних аргументів), вона повинна створювати один і той же вихід, не викликаючи "побічних ефектів", таких як зміна глобального стану, виконуючи I / O або викидання виключень.
Кажуть, що у функціональному програмуванні зазвичай потрібне деяке зло для того, щоб завершити роботу. 100% чистота, як правило, не є метою. Мінімізація побічних ефектів є.
Висновок
Дійсно, з усіх вищезазначених ідей незмінність стала найбільшою єдиною виграшею з точки зору практичних застосувань для спрощення мого коду - будь то OOP чи FP. Передача функцій ітераторам - це другий за величиною виграш. Документація Java 8 Lambdas має найкраще пояснення того, чому. Рекурсія чудово підходить для обробки дерев. Лінь дозволяє працювати з нескінченними колекціями.
Якщо вам подобається JVM, я рекомендую вам поглянути на Scala та Clojure. Обидва є проникливими інтерпретаціями функціонального програмування. Scala є безпечним для типу з дещо схожим на C синтаксисом, хоча він справді має стільки ж синтаксису спільного з Haskell, як і з C. Clojure не є безпечним для типу, і це Lisp. Нещодавно я опублікував порівняння Java, Scala та Clojure щодо однієї конкретної проблеми рефакторингу. Порівняння Логана Кемпбелла, використовуючи гру «Життя життя», включає також Haskell і також набрав Clojure.
PS
Джиммі Хоффа зазначив, що мій клас "Автобус" є змінним. Замість того, щоб виправити оригінал, я думаю, що саме це буде демонструвати саме те, яке рефакторинг стосується цього питання. Це можна виправити, зробивши кожен метод на Автобусі заводом для виробництва нової Автобус, кожен метод на Пасажирі - фабрикою для виробництва нового Пасажира. Таким чином, я додав тип повернення до всього, що означає, що я скопіюю java.util.function.Function Java 8 замість споживчого інтерфейсу:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Потім у автобусі:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
Нарешті, об’єкт анонімної функції повертає модифікований стан речей (нова шина з новими пасажирами). Це передбачає, що p.debit () тепер повертає нового незмінного Пасажира з меншими грошима, ніж оригінал:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
Сподіваємось, ви тепер можете самостійно прийняти рішення про те, наскільки функціональним ви хочете зробити свою імперативну мову, і вирішити, чи було б краще переробити проект за допомогою функціональної мови. У Scala або Clojure колекції та інші API розроблені для спрощення функціонального програмування. Обидва мають дуже хороший інтероп Java, тож ви можете змішувати та співставляти мови. Насправді, для інтероперабельності Java Scala збирає свої функції першого класу в анонімні класи, майже сумісні з функціональними інтерфейсами Java 8. Про подробиці ви можете прочитати в Scala в глибині секти. 1.3.2 .