Рубрики
Без рубрики

Практическое функциональное программирование

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

Автор оригинала: Steven Heidel.

Ниже переформатировано из презентации, которую я дал в LinkedIn в прошлом году. Презентация попыталась объяснить функциональное программирование без использования концепций, таких как «монады» или «неизменность» или «побочные эффекты». Вместо этого фокусируется на том, как думать о Состав Может сделать вас лучшим программистом, независимо от того, на каком языке вы используете.

40 лет назад, 17 октября 1977 года, премия «Тьюринг» была представлена Джонерузам в своем вкладе в проект систем программирования высокого уровня, в частности, язык программирования Fortran. Все лауреаты Turing Award предоставляются возможность представить лекцию на тему их выбора в течение года, в которой они получают награду. В качестве создателя языка программирования Fortran, который можно ожидать лекции по преимуществам Фортран и будущих событий на языке. Вместо этого он дал лекцию под названием Может ли программировать быть освобожденным от стиля фон Неймана? В котором он критиковал некоторые из основных языков дня, включая Фортран, за их недостатки. Он также предложил альтернативу: A Функциональный стиль программирования Отказ

Лекция контрастирует обычные программы и их «неспособность эффективно использовать мощные объединять формы» с функциональными программами, которые «основаны на использовании сочетание формы». Функциональное программирование получило возобновленный интерес за последние несколько лет из-за роста высоко масштабируемых и параллельных вычислений. Но основным преимуществом функционального программирования является то, что может быть реализовано независимо от того, если ваша программа будет распараллелизована или нет: функциональное программирование лучше в Состав Отказ

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

Давайте начнем с поиска функции, которая очень хорошо составляет:

String addFooter(String message) {
  return message.concat(" - Sent from LinkedIn");
}

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

boolean validMessage(String message) {
  return characterCount(addFooter(message)) <= 140;
}

Это здорово, мы взяли небольшой кусочек функциональности и составили его вместе, чтобы сделать что-то большее. Пользователи VaidMessage функция даже не должна знать о том, что эта функция была построена из меньшего; Это абстрагировано как деталь реализации.

Теперь давайте посмотрим на функцию, которая не составляется так хорошо:

String firstWord(String message) {
  String[] words = message.split(' ');
  if (words.length > 0) {
    return words[0];
  } else {
    return null;
  }
}

А затем попробуйте составить его в другой функции:

// "Hello world" -> "HelloHello"
duplicate(firstWord(message));

Хотя просто на первый взгляд, если мы провели вышеуказанный код с пустым сообщением, то мы ударили страшные Nullpointerexception Отказ Один вариант будет изменять функцию дублирования, чтобы справиться с тем, что его вход иногда может быть null :

String duplicateBad(String word) {
  if (word == null) {
    return null;
  } else {
    return word.concat(word);
  }
}

Теперь мы можем использовать эту функцию с Firstword Функция из ранее и просто пропустите null значение. Но это против точки состава и абстракции. Если вам постоянно следует войти и изменить компонентные детали каждый раз, когда вы хотите сделать что-то большее, то он не является композимами. В идеале вы хотите функции быть как черные коробки, где точные детали реализации не имеют значения.

Нулевые объекты не составляют хорошо.

Давайте посмотрим на альтернативную реализацию, которая использует Java 8 Необязательно Тип (также называется Опция или Может быть, на других языках):

Optional firstWord(String message) {
  String[] words = message.split(' ');
  if (words.length > 0) {
    return Optional.of(words[0]);
  } else {
    return Optional.empty();
  }
}

Теперь мы пытаемся составить его с немодифицированным Дубликат Функция из ранее:

// "Hello World" -> Optional.of("HelloHello")
firstWord(input).map(this::duplicate)

Оно работает! Необязательно заботится о том, что Firstword иногда не возвращает значение. Если Дополнительный. Пустота () возвращается из Firstword Тогда .map Функция просто пропустит работу Дубликат функция. Мы смогли легко комбинировать функции без необходимости изменения внутренних органов Дубликат Отказ Контрастное это с нулевым случаем, где мы должны были создать дубликатбад функция. Другими словами: нулевые объекты не составляют хорошо, но необязатся.

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

Асинхронный код общеизвестно сложно составить. Асинхронные функции обычно принимают «обратные вызовы», которые выполняются, когда асинхронная часть вызова завершена. Например, функция getdata Может сделать HTTP Call на веб-сервис, а затем запустить функцию в возвращенных данных. Но что, если вы хотите сделать еще один HTTP-звонок сразу после этого? А потом другой? Делая это быстро приводит вас к ситуации, нежно известной как обратный ад.

getData(function(a) {  
    getMoreData(a, function(b) {
        getMoreData(b, function(c) { 
            getMoreData(c, function(d) { 
                getMoreData(d, function(e) { 
                    // ...
                });
            });
        });
    });
});

В более крупном веб-приложении это приводит к высоко вложенным коде спагетти. Это также не очень композибе. Представьте, что пытается отделить один из GetMordata функционирует в свой собственный метод. Или представьте, что пытается добавить ошибку обрабатывать эту вложенную функцию. Причина, по которой она не составлена, заключается в том, что существует так много контекстных требований к каждому блоку кода: самый внутренний блок нуждается в доступе к результатам А , B , C и т. Д. И т. Д.

Значения легче составлять вместе, чем функции

Давайте заглянем внутрь функционального программиста для панели инструментов для альтернативы: Обещание (иногда называют будущее на других языках). Вот что сейчас выглядит код:

getData()
  .then(getMoreData)
  .then(getMoreData)
  .then(getMoreData)
.catch(errorHandler)

getdata Функции теперь возвращают Обещание ценность вместо того, чтобы принимать функцию обратного вызова. Значения легче составлять вместе, чем функции, потому что у них нет те же предпосылки, которые будут иметь обратный вызов. Теперь тривиально добавить обрабатывать ошибку на весь блок из-за функциональности, которые Обещание Объект дает нам.

Еще один пример некомпостабельного кода, который говорил о более чем асинхронном коде, – это петли, или в целом, функции, которые возвращают несколько значений, такие как списки. Давайте посмотрим на пример:

// ["hello", "world"] -> ["hello!", "world!"]
List addExcitement(List words) {
  List output = new LinkedList<>();
  for (int i = 0; i < words.size(); i++) {
    output.add(words.get(i) + "!");
  }
  return output;
}

// ["hello", "world"] -> ["hello!!", "world!!"]
List addMoreExcitement(List words) {
  return addExcitement(addExcitement(words));
}

Мы составили функцию, которая добавляет одну восклицающую точку в то, что добавляет два. Это работает, но это неэффективно, потому что он петли через петлю дважды, а не только один раз. Мы могли бы вернуться и изменить оригинальную функцию, но, как до того, как это нарушает абстракцию.

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

С императивным программированием вы можете получить только одну из модулей или производительности. С функциональным программированием вы можете иметь оба.

Ответ функционального программиста на это (в Java 8 по крайней мере) является Поток Отказ А Поток По умолчанию ленивый, что означает, что он только с помощью данных, когда это абсолютно необходимо. Другими словами, функция «Lazy»: она начинает делать работу только при просьбе результата (функциональный язык программирования Haskell построен вокруг концепции лени). Давайте переписать приведенный выше пример, используя Поток вместо:

String addExcitement(String word) {
  return word + "!";
}

list.toStream()
  .map(this::addExcitement)
  .map(this::addExcitement)
.collect(Collectors.toList())

Это будет только один раз в списке один раз и позвонить в AddExcitement функционируйте дважды на каждом элементе. Опять же нам нужно представить наш код, работающий на том же произведении данных по нескольким частям приложения. Без ленивой структуры, как Поток , пытаясь увеличить производительность путем консолидации всех перечисленных обходов в одно место, означало бы разбитые существующие функции. С ленивым объектом вы можете достичь как модульности, так и производительности, потому что обходы отложены до конца.

Теперь, когда мы видели некоторые примеры, давайте вернемся к задаче выяснения, какие свойства облегчают состав некоторых функций, чем другие. Мы видели, что такие вещи, как нулевые объекты, обратные вызовы, а петли не состоят хорошо. С другой стороны, дополнительные, обещания и потоки хорошо составляют хорошо. Это почему?

Ответ заключается в том, что композируемые примеры имеют чистое разделение между что Вы хотите сделать и Как Вы на самом деле делаете это.

Во всех этих предыдущих примерах есть одна общая вещь. Функциональный способ выполнения вещей сосредоточен на том, что вы хотите, чтобы результат был. Итеративный способ выполнения вещей сосредоточен на том, как вы на самом деле попадаете туда, детали реализации. Оказывается, сшивание совместных итеративных инструкций о том, как делать вещи не составляют, а также описания высокого уровня того, что нужно сделать.

Например, в случае обещаний: что В этом случае выполняется один HTTP-вызов, за которым следует другой. Как не имеет значения и абстрагировано: возможно, он использует резьбовые бассейны, замки Mutex и т. Д., Но не имеет значения.

Функциональное программирование разделение что Вы хотите, чтобы результат был от Как этот результат достигается.

Действительно, это мое практическое определение функционального программирования. Мы хотим иметь четкое разделение проблем в наших программах. Часть «То, что вы хотите» – это хорошая и композимана и позволяет легко построить большие вещи от меньших. «Как вы это делаете», необходима в какой-то момент, но, отделяя его, мы сохраняем вещи, которые не так совокупятся на пути к способу вещей, которые являются более композиционными.

Мы видим это в реальных примерах мира:

  • API Apache Spark’s API для выполнения вычислений на больших наборах данных тезисывает детали того, какие машины будут работать на нем, и где живет данные
  • React.js описывает вид и оставляет различие от DOM до эффективного алгоритма

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