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

Функциональное программирование для вашего повседневного JavaScript: Методы композиции

Введение в общие закономерности, используемые в функциональном программировании. Tagged с JavaScript, функциональным, начинающим.

Puedes leer la ersión en español aquí.

Сегодня мы поговорим о композиции функции. Искусство создания больших вещей с «простыми» произведениями. Будет еще лучше, если вы ничего не знаете о функциональном программировании, это будет введение в общие концепции и паттерны, используемые в этой парадигме, которые могут быть реализованы в JavaScript. То, что я собираюсь показать вам, это не волшебная формула, чтобы сделать ваш код более читаемым или свободным от ошибок, это не так, как это работает. Я действительно считаю, что это может помочь решить некоторые проблемы, но для того, чтобы сделать это наилучшим образом, чтобы помнить о нескольких вещах. Итак, прежде чем я покажу вам какую -либо реализацию, мы поговорим о некоторых концепциях и немного о философии.

Что тебе нужно знать

Что такое функциональная композиция?

Это механизм, который позволяет нам объединить две или более функции в новую функцию.

Это похоже на простую идею, разве мы все в какой -то момент нашей жизни объединили пару функций? Но действительно ли мы думаем о композиции, когда мы их создаем? Что поможет нам сделать функции, уже предназначенные для объединения?

Философия

Функциональный состав более эффективен, если вы следовали определенным принципам.

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

Вы, наверное, слышали это раньше, это фрагмент Unix Philosophy Анкет Когда -либо задумывался, как пришло Bash Несмотря на странный синтаксис и много ограничений, так популярен? Эти два принципа – большая часть. Многое программное обеспечение, предназначенное для этой среды, специально предназначено для того, чтобы быть многоразовым компонентом, и когда вы «подключаете» два или более, результатом является еще одна программа, которая может быть связана с другими неизвестными программами.

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

Я постараюсь настроить ситуацию, когда мы можем на практике эти принципы.

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

Скажите, что мы хотим извлечь значение переменной с именем ХОЗЯИН Это внутри .env файл. Давайте попробуем сделать это в избиение .

Это файл.

ENV=development
HOST=http://locahost:5000

Чтобы показать содержимое файла на экране, мы используем кошка Анкет

cat .env

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

cat .env | grep "HOST=.*"

Чтобы получить значение, которое мы используем резать , это приведет к результату, предоставленному грип И он разделит его, используя разделитель, а затем даст нам раздел строки, которую мы говорим.

cat .env | grep "HOST=.*" | cut --delimiter=--fields=2

Это должно дать нам.

http://locahost:5000

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

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

Функции – это вещи

Давайте повернемся и обратим внимание на JavaScript. Вы когда-нибудь слышали фразу «первоклассная функция»? Это означает, что функции могут рассматриваться так же, как и любое другое значение. Давайте сравним с массивами.

  • Вы можете назначить их переменным
const numbers = ['99', '104'];
const repeat_twice = function(str) {
  return str.repeat(2);
};
  • Передать их в качестве аргументов функции
function map(fn, array) {
  return array.map(fn);
}

map(repeat_twice, numbers);
  • Вернуть их из других функций
function unary(fn) {
  return function(arg) {
    return fn(arg);
  }
}

const safer_parseint = unary(parseInt);

map(safer_parseint, numbers);

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

Композиция на практике

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

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

  • Получите содержание файла.
const fs = require('fs');

function get_env() {
  return fs.readFileSync('.env', 'utf-8');
}
  • Отфильтруйте контент на основе шаблона.
function search_host(content) {
  const exp = new RegExp('^HOST=');
  const lines = content.split('\n');

  return lines.find(line => exp.test(line));
}
  • Получите значение.
function get_value(str) {
  return str.split('=')[1];
}

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

Естественный состав

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

get_value(search_host(get_env()));

Это идеальная настройка для композиции функции, выход функции становится входом следующего, что является тем же, что | Символ делает в избиение . Но в отличие от Bash , здесь поток данных идет справа налево.

Теперь давайте представим, что у нас есть еще две функции, которые делают что -то со значением Хост Анкет

test(ping(get_value(search_host(get_env()))));

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

Автоматическая композиция

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

function compose(...fns) {
  return function _composed(...args) {
    // Index of the last function
    let last = fns.length - 1;

    // Call the last function
    // with arguments of `_composed`
    let current_value = fns[last--](...args);

    // loop through the rest in the opposite direction
    for (let i = last; i >= 0; i--) {
      current_value = fns[i](current_value);
    }

    return current_value;
  };
}

Теперь мы можем сделать это.

const get_host = compose(get_value, search_host, get_env);

// get_host is `_composed`
get_host();

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

const get_host = compose(
  test,
  ping,
  get_value,
  search_host,
  get_env
);

get_host();

Как и в нашей первой попытке, здесь данные текут справа налево. Если вы хотите перевернуть заказ, вы бы сделали это так.

function pipe(...fns) {
  return function _piped(...args) {
    // call the first function
    // with the arguments of `_piped`
    let current_value = fns[0](...args);

    // loop through the rest in the original order
    for (let i = 1; i < fns.length; i++) {
      current_value = fns[i](current_value);
    }

    return current_value;
  };
}

Вот.

const get_host = pipe(get_env, search_host, get_value);

get_host();

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

Это не всегда легко

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

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

const fs = require('fs');

function cat(filepath) {
  return fs.readFileSync(filepath, 'utf-8');
}

function grep(pattern, content) {
  const exp = new RegExp(pattern);
  const lines = content.split('\n');

  return lines.find(line => exp.test(line));
}

function cut({ delimiter, fields }, str) {
  return str.split(delimiter)[fields - 1];
}

Они не совсем такие же, как их Bash коллеги, но они делают работу. Но теперь, если мы хотим собрать их вместе, это должно быть так.

cut({delimiter: '=', fields: 2}, grep('^HOST=', cat('.env')));

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

Функции с несколькими входами

Решение об этом Частичное приложение И к счастью для нас JavaScript отлично поддерживает то, что мы хотим сделать. Наша цель проста, мы собираемся передать некоторые параметры, которые необходимо функционировать, но не вызывая ее. Мы хотим иметь возможность сделать это.

const get_host = pipe(
  cat,
  grep('^HOST='), 
  cut({ delimiter: '=', fields: 2 })
);

get_host('.env');

Чтобы сделать это возможным, мы будем полагаться на технику под названием Карринг , это состоит в том, чтобы превратить функцию множественного параметра в несколько функций параметров. Мы делаем это, принимая по одному параметру за раз, просто продолжайте возвращать функции, пока не получим все, что нам нужно. Мы сделаем это Греп и вырезать Анкет

- function grep(pattern, content) {
+ function grep(pattern) {
+   return function(content) {
      const exp = new RegExp(pattern);
      const lines = content.split('\n');

      return lines.find(line => exp.test(line));
+   }
  }
-
- function cut({ delimiter, fields }, str) {
+ function cut({ delimiter, fields }) {
+   return function(str) {
      return str.split(delimiter)[fields - 1];
+   }
  }

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

const get_host = pipe(
  cat,
  grep.bind(null, '^HOST='), 
  cut.bind(null, { delimiter: '=', fields: 2 })
);

Наконец, если все остальное выглядит слишком сложным, у вас всегда есть возможность создать встроенную функцию стрелки.

const get_host = pipe(
  cat,
  content => grep('^HOST=', content), 
  str => cut({ delimiter: '=', fields: 2 }, str)
);

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

Функции с несколькими выходами

Несколько выходов? Я имею в виду функции, возвращаемое значение которого может иметь более одного типа. Это происходит, когда у нас есть функции, которые реагируют по -разному в зависимости от того, как мы их используем или в каком контексте. У нас есть такие функции в нашем примере. Давайте посмотрим на кошка Анкет

function cat(filepath) {
  return fs.readFileSync(filepath, 'utf-8');
}

Внутри кошка У нас есть readfilesync , это тот, который читает файл в нашей системе, действие, которое может потерпеть неудачу по многим причинам. Это означает, что кошка может вернуть Строка Если все идет хорошо, но также может бросить ошибку, если что -то пойдет не так. Нам нужно обрабатывать оба случая.

К сожалению, для нас исключения не единственное, о чем нам нужно беспокоиться, нам также необходимо справиться с отсутствием ценностей. В Греп У нас есть эта линия.

lines.find(line => exp.test(line));

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

Как мы справляемся со всем этим?

Функторы && Monads (Извините за громкие слова). Предоставление соответствующего объяснения этих двух займет слишком много времени, поэтому мы просто сосредоточимся на практических аспектах. В настоящее время вы можете думать о них как о типах данных, которые должны подчиняться некоторым законам (вы можете найти некоторые из них здесь: Fantasy Land ).

С чего начать? С функторами.

  • Функторы

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

const add_one = num => num + 1;
const number = [41];
const empty = [];

number.map(add_one); // => [42]
empty.map(add_one);  // => []

Видеть? карта называется добавить одну только один раз, на номер множество. Он ничего не сделал на пусто Массив, не остановил выполнение скрипта, бросив ошибку, он просто вернул массив. Это поведение, которое мы хотим.

Мы сделаем это самостоятельно. Давайте создадим тип данных с именем Результат , это будет представлять действие, которое может быть или не быть успешным. У него будет карта Метод, который выполнит предоставленный обратный вызов только тогда, когда действие имел ожидаемый результат.

const Result = {};

Result.Ok = function(value) {
  return {
    map: fn => Result.Ok(fn(value)),
  };
}

Result.Err = function(value) {
  return {
    map: () => Result.Err(value),
  };
}

У нас есть наш фанкор Но теперь вам может быть интересно это? Как это помогает? Мы делаем это один шаг за раз. Давайте использовать его с Кот .

function cat(filepath) {
  try {
    return Result.Ok(fs.readFileSync(filepath, 'utf-8'));
  } catch(e) {
    return Result.Err(e);
  }
}

Что мы получаем с этим? Дай этому шанс.

cat('.env').map(console.log);

У вас все еще есть тот же вопрос на уме, я вижу это. Теперь попробуйте добавить другие функции.

Примечание: я собираюсь предположить, что вы можете использовать карри для достижения частичного применения.

cat('.env')
  .map(grep('^HOST='))
  .map(cut({ delimiter: '=', fields: 2 }))
  .map(console.log);

Видеть, что? Эта цепь карта S очень похож на составить или трубка . Мы сделали это, мы вернули нашу композицию, а теперь с обработкой ошибок (вроде).

Я хочу что -то сделать. Этот шаблон, с попробуй/поймать Я хочу поместить это в функцию.

 Result.make_safe = function(fn) {
  return function(...args) {
    try {
      return Result.Ok(fn(...args));
    } catch(e) {
      return Result.Err(e);
    }
  }
 }

Теперь мы можем преобразовать кошка даже не касаясь его кода.

const safer_cat = Result.make_safe(cat);

safer_cat('.env')
  .map(grep('^HOST='))
  .map(cut({ delimiter: '=', fields: 2 }))
  .map(console.log);

Вы можете что -то сделать, если что -то пойдет не так, верно? Давайте сделаем это возможным.

  const Result = {};

  Result.Ok = function(value) {
    return {
      map: fn => Result.Ok(fn(value)),
+     catchMap: () => Result.Ok(value),
    };
  }

  Result.Err = function(value) {
    return {
      map: () => Result.Err(value),
+     catchMap: fn => Result.Err(fn(value)),
    };
  }

Теперь мы можем совершать ошибки и быть уверенными, что что -то с этим делаем.

const safer_cat = Result.make_safe(cat);
const show_error = e => console.error(`Whoops:\n${e.message}`);

safer_cat('what?')
  .map(grep('^HOST='))
  .map(cut({ delimiter: '=', fields: 2 }))
  .map(console.log)
  .catchMap(show_error);

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

  const Result = {};

  Result.Ok = function(value) {
    return {
      map: fn => Result.Ok(fn(value)),
      catchMap: () => Result.Ok(value),
+     cata: (error, success) => success(value)
    };
  }

  Result.Err = function(value) {
    return {
      map: () => Result.Err(value),
      catchMap: fn => Result.Err(fn(value)),
+     cata: (error, success) => error(value)
    };
  }

При этом мы можем выбрать, что делать в конце каждого действия.

const constant = arg => () => arg;
const identity = arg => arg;

const host = safer_cat('what?')
  .map(grep('^HOST='))
  .map(cut({ delimiter: '=', fields: 2 }))
  .cata(constant("This ain't right"), identity)

// ....

Примечание: если вы спрашиваете, почему ката , это происходит от Катаморфизм , еще один из тех терминов, которые люди используют в функциональном программировании.

Теперь давайте создадим тип данных, который может решить проблему, которую мы столкнулись с Греп Анкет В этом случае мы хотим справиться с отсутствием значения.

const Maybe = function(value) {
  if(value == null) {
    return Maybe.Nothing();
  }

  return Maybe.Just(value);
}

Maybe.Just = function(value) {
  return {
    map: fn => Maybe.Just(fn(value)),
    catchMap: () => Maybe.Just(value),
    cata: (nothing, just) => just(value)
  };
}

Maybe.Nothing = function() {
  return {
    map: () => Maybe.Nothing(),
    catchMap: fn => fn(),
    cata: (nothing, just) => nothing()
  };
}

Maybe.wrap_fun = function(fn) {
  return function(...args) {
    return Maybe(fn(...args));
  }
}

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

const maybe_host = Maybe.wrap_fun(grep('^HOST='));

maybe_host(cat('.env'))
  .map(console.log)
  .catchMap(() => console.log('Nothing()'));

Это должно показать http://localhost: 5000 . И если мы изменим шаблон ^Host = он должен показать Ничего () Анкет

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

safer_cat('.env')
  .map(maybe_host)
  .map(res => console.log({ res }));
  .catchMap(() => console.log('what?'))

Вы получаете это.

{
  res: {
    map: [Function: map],
    catchMap: [Function: catchMap],
    cata: [Function: cata]
  }
}

Подожди, что происходит? Ну, у нас есть Может В ловушке внутри Результат Анкет Может быть, вы не видели этого, но другие люди сделали, и у них есть решение.

  • Монадс

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

  Result.Ok = function(value) {
    return {
      map: fn => Result.Ok(fn(value)),
      catchMap: () => Result.Ok(value),
+     flatMap: fn => fn(value),
      cata: (error, success) => success(value)
    };
  }

  Result.Err = function(value) {
    return {
      map: () => Result.Err(value),
      catchMap: fn => Result.Err(fn(value)),
+     flatMap: () => Result.Err(value),
      cata: (error, success) => error(value)
    };
  }
  Maybe.Just = function(value) {
    return {
      map: fn => Maybe.Just(fn(value)),
      catchMap: () => Maybe.Just(value),
+     flatMap: fn => fn(value),
      cata: (nothing, just) => just(value),
    };
  }

  Maybe.Nothing = function() {
    return {
      map: () => Maybe.Nothing(),
      catchMap: fn => fn(),
+     flatMap: () => Maybe.Nothing(),
      cata: (nothing, just) => nothing(),
    };
  }

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

Примечание: да, массивы также Монады. У них есть карта метод и плоская карта метод И они подчиняются всем законам.

Давайте проверить может быть_хост очередной раз.

 safer_cat('.env')
  .flatMap(maybe_host)
  .map(res => console.log({ res }));
  .catchMap(() => console.log('what?'))

Это должно дать нам.

{ res: 'HOST=http://localhost:5000' }

Мы готовы составить все вместе.

const safer_cat = Result.make_safe(cat);
const maybe_host = Maybe.wrap_fun(grep('^HOST='));
const get_value = Maybe.wrap_fun(cut({delimiter: '=', fields: 2}));

const host = safer_cat('.env')
  .flatMap(maybe_host)
  .flatMap(get_value)
  .cata(
    () => 'http://127.0.0.1:3000',
    host => host
  );

// ....

И если мы хотим использовать трубка или составить ?

const chain = fn => m => m.flatMap(fn);
const unwrap_or = fallback => fm => 
  fm.cata(() => fallback, value => value);


const safer_cat = Result.make_safe(cat);
const maybe_host = Maybe.wrap_fun(grep('^HOST='));
const get_value = Maybe.wrap_fun(cut({delimiter: '=', fields: 2}));

const get_host = pipe(
  safer_cat,
  chain(maybe_host),
  chain(get_value),
  unwrap_or('http://127.0.0.1:3000')
);

get_host('.env');

Вы можете проверить весь код здесь: Ссылка Анкет

Все еще хотите узнать больше?

Есть много вещей, о которых я не упомянул, потому что это займет слишком много времени Но если вы хотите узнать больше об этом, я готовлю немного материала.

  • Частичное применение
  • О функторах
  • Используя, возможно
  • Чистые функции и побочные эффекты

Вывод

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

Источники

Спасибо за чтение. Если вы найдете эту статью полезной и хотите поддержать мои усилия, купить мне кофе ☕ .

Оригинал: “https://dev.to/vonheikemen/functional-programming-for-your-everyday-javascript-composition-techniques-4663”