tl; dr: Определяющим отличием является то, что ->
направляет канал к первому аргументу, а |>
- к последнему.То есть:
x -> f(y, z) <=> f(x, y, z)
x |> f(y, z) <=> f(y, z, x)
К сожалению, есть некоторые тонкости и последствия, которые делают это немного более сложным и запутанным на практике.Пожалуйста, потерпите меня, когда я пытаюсь объяснить историю, стоящую за этим.
До эпохи конвейера
До появления операторов каналов большинство функциональных программистов создавали большинство функций с "объектом", которыйфункция действует как последний аргумент.Это связано с тем, что при частичном применении функции составление функций стало намного проще, а частичное применение функций стало значительно проще в языках с карри, если неиспользуемые аргументы находятся в конце.
Curring
Вязык карри, каждая функция принимает ровно один аргумент.Функция, которая принимает два аргумента, на самом деле является функцией, которая принимает один аргумент, но затем возвращает другую функцию, которая принимает другой аргумент и, в свою очередь, возвращает фактический результат.Следовательно, они эквивалентны:
let add = (x, y) => x + y
let add = x => y => x + y
Вернее, первая форма является просто синтаксическим сахаром для второй формы.
Приложение с частичной функцией
Это также означает, что мы можем легкочастично применить функцию, просто предоставив первый аргумент, который вернет ей функцию, которая принимает второй аргумент перед выдачей результата:
let add3 = add(3)
let result = add3(4) /* result == 7 */
Без каррирования нам пришлось бы вместо этого обернуть его вфункция, которая гораздо более громоздка:
let add3 = y => add(3, y)
Умный дизайн функции
Теперь оказывается, что большинство функций работают с аргументом "main", который мы можем назвать "объектом"функция.List
функции обычно работают с определенным списком, например, не сразу с несколькими (хотя, конечно, это тоже происходит).И поэтому, поставив главный аргумент последним, вы сможете создавать функции гораздо проще.Например, с парой хорошо разработанных функций определение функции для преобразования списка необязательных значений в список фактических значений со значениями по умолчанию так же просто, как:
let values = default => List.map(Option.defaultValue(default)))
В то время как функции разработаны с "объект "first" потребовал бы, чтобы вы написали:
let values = (list, default) =>
List.map(list, value => Option.defaultValue(value, default)))
рассвет эры трубы (которая, по иронии судьбы, не была первой)
Из того, что я понимаю, кто-то играетв F # обнаружил часто встречающийся конвейерный паттерн и подумал, что было бы сложно придумать именованные привязки для промежуточных значений или вложить вызовы функций в обратном порядке, используя слишком много чертовых скобок.Таким образом, он изобрел оператора pipe-forward, |>
.При этом конвейер может быть записан как
let result = list |> List.map(...) |> List.filter(...)
вместо
let result = List.filter(..., List.map(..., list))
или
let mappedList = List.map(..., list)
let result = List.filter(..., mapped)
Но это работает, только если главный аргумент последний,потому что он опирается на частичное применение функции через каррирование.
А потом ... BuckleScript
Затем приходит Боб, который первым создал BuckleScript для компиляции кода OCaml в JavaScript.BuckleScript был принят Reason, а затем Боб создал стандартную библиотеку для BuckleScript под названием Belt
.Belt
игнорирует почти все, что я объяснил выше, поставив основной аргумент первым .Зачем?Это еще не объяснено, но из того, что я могу понять, это прежде всего потому, что оно более знакомо разработчикам JavaScript 1 .
Боб действительно осознал важность оператора канала, поэтому онсоздал свой собственный оператор pipe-first, |.
, который работает только с BuckleScript 2 .А потом разработчики Reason подумали, что это выглядит немного уродливо и не хватает направления, поэтому они придумали оператор ->
, который переводит в |.
и работает точно так же, как он ... за исключением того, что он имеет другой приоритет и поэтому не 'играть хорошо с чем-либо еще. 3
Заключение
Оператор с конвейером не является плохой идеей сам по себе. Но то, как это было реализовано и выполнено в BuckleScript и Reason, вызывает много путаницы. Он имеет непредсказуемое поведение, поощряет плохой дизайн функций и, если не пойти ва-банк, 4 , накладывает большой когнитивный налог при переключении между различными операторами канала в зависимости от того, какую функцию вы вызываете.
Поэтому я бы рекомендовал избегать оператора pipe-first (->
или |.
) и вместо этого использовать pipe-forward (|>
) с аргументом placeholder (также исключающим Reason), если вам нужно направить к первой объектной функции, например list |> List.map(...) |> Belt.List.keep(_, ...)
.
1 Есть также некоторые тонкие различия в том, как это взаимодействует с выводом типа, потому что типы выводятся слева направо, но это не является очевидным преимуществом для любого стиля IMO.
2 Поскольку требуется синтаксическое преобразование. Он не может быть реализован как обычный оператор, в отличие от pipe-forward.
3 Например, list |> List.map(...) -> Belt.List.keep(...)
не работает так, как вы ожидаете
4 Это означает, что невозможно использовать почти все библиотеки, созданные до того, как существовал оператор pipe-first, потому что они, конечно, были созданы с учетом оригинального оператора pipe-forward. Это эффективно разделяет экосистему на две части.