В чем разница между - ›и |› в reasonml?

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


person Crysknight    schedule 02.04.2019    source источник
comment
Между ними есть существенные различия, которые на первый взгляд не очевидны. Хавьер Чаварри дал исчерпывающее сравнение: javierchavarri.com/data- первое и последнее сравнение данных   -  person javinor    schedule 10.12.2019


Ответы (2)


tl; dr: Определяющее различие состоит в том, что -> направляет по конвейеру первый аргумент, а |> - по конвейеру до последнего. Это:

x -> f(y, z) <=> f(x, y, z)
x |> f(y, z) <=> f(y, z, x)

К сожалению, есть некоторые тонкости и последствия, которые на практике делают это немного более сложным и запутанным. Пожалуйста, терпите меня, пока я пытаюсь объяснить историю, стоящую за этим.

До эпохи трубы

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

Каррирование

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

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)

Умный функциональный дизайн

Теперь выясняется, что большинство функций оперируют «главным» аргументом, который мы могли бы назвать «объектом» функции. List функции обычно работают с определенным списком, например, а не с несколькими одновременно (хотя, конечно, такое тоже бывает). И, следовательно, размещение главного аргумента последним позволяет вам гораздо проще составлять функции. Например, с парой хорошо спроектированных функций определение функции для преобразования списка необязательных значений в список фактических значений со значениями по умолчанию так же просто, как:

let values = default => List.map(Option.defaultValue(default)))

Хотя функции, разработанные с использованием «объекта», сначала потребуют от вас написать:

let values = (list, default) =>
  List.map(list, value => Option.defaultValue(value, default)))

Начало эры трубки (которая, по иронии судьбы, не была первой трубкой)

Насколько я понимаю, кто-то, игравший с F #, обнаружил часто встречающийся паттерн конвейера и подумал, что было сложно либо придумать именованные привязки для промежуточных значений, либо вложить вызовы функций в обратном порядке, используя слишком много чертовых скобок. Поэтому он изобрел оператор прямой передачи |>. Таким образом, конвейер можно записать как

let result = list |> List.map(...) |> List.filter(...)

вместо

let result = List.filter(..., List.map(..., list))

or

let mappedList = List.map(..., list)
let result = List.filter(..., mapped)

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

А потом ... BuckleScript

Затем появляется Боб, который первым создал BuckleScript для компиляции кода OCaml в JavaScript. BuckleScript был принят Reason, а затем Боб создал стандартную библиотеку для BuckleScript под названием Belt. Belt игнорирует почти все, что я объяснил выше, помещая главный аргумент первым. Почему? Это еще предстоит объяснить, но насколько я понимаю, это в первую очередь потому, что это более знакомо разработчикам JavaScript 1.

Однако Боб осознал важность оператора канала, поэтому он создал свой собственный оператор канала, |., который работает только с BuckleScript 2. А затем разработчики Reason подумали, что это выглядит немного некрасиво и не имеет направления, поэтому они придумали оператор ->, который переводится как |. и работает точно так же ... за исключением того, что он имеет другой приоритет и поэтому не очень хорошо работает с что-нибудь еще. 3

Вывод

Сам по себе оператор pipe-first - неплохая идея. Но то, как это было реализовано и выполнено в BuckleScript и Reason, вызывает большую путаницу. Он имеет неожиданное поведение, поощряет плохой дизайн функций и, если никто не использует его 4, накладывает тяжелый когнитивный налог при переключении между различными операторами конвейера в зависимости от того, какую функцию вы вызываете.

Поэтому я бы рекомендовал избегать оператора pipe-first (-> или |.) и вместо этого использовать pipe-forward (|>) с аргумент-заполнитель (также эксклюзивно для Reason), если вам нужно передать по конвейеру функцию" объект "-first, например list |> List.map(...) |> Belt.List.keep(_, ...).


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

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

3 Например, list |> List.map(...) -> Belt.List.keep(...) работает не так, как вы ожидали

4 Это означает невозможность использования почти всех библиотек, созданных до того, как существовал оператор pipe-first, потому что они, конечно, были созданы с учетом исходного оператора pipe-forward. Это эффективно разделяет экосистему на две части.

person glennsl    schedule 02.04.2019
comment
Отдельного оператора для BuckleScript можно было бы избежать, если бы они просто использовали аргументы с меткой < / a>, поскольку помеченные аргументы могут применяться в любом порядке, в том числе до или после немаркированных аргументов. Это позволило бы им сохранить первым t для вывода типа, но по-прежнему использовать стандартный оператор |>. Base очень эффективно использует эту парадигму (например, см. List, где функция для map помечена ~f). - person kevinji; 06.04.2019
comment
@kevinji В самом деле, это замечательный момент, и на самом деле он поднимался как рано, так и часто во время этого процесса. К сожалению, Боб отвергает это просто потому, что лично ему это не нравится. - person glennsl; 06.04.2019
comment
Другой аргумент против -> состоит в том, что он, кажется, ломает любую версию refmt, которая у меня есть. Когда он встречает ->, это говорит о синтаксической ошибке. - person MCH; 21.11.2019
comment
Я лично предпочел бы |> ->, но, очевидно, re-script устарел |> pipe. предполагая, что повторный скрипт станет будущим bucklescript / reasonml, я полагаю, что любому, кто хочет работать с bs / rescript, нужно будет привыкнуть -> pipe - person masoodahm; 28.12.2020
comment
Я сомневаюсь, что он действительно будет удален, поскольку это нарушит совместимость OCaml, а также обратную совместимость с большим количеством библиотек. Но даже если это так, добавить обратно в пользовательское пространство - тривиально. - person glennsl; 28.12.2020

|> обычно называют «прямой конвейер». Это вспомогательная функция, которая используется в более широком сообществе OCaml, а не только в ReasonML. Он «вставляет» аргумент слева как последний аргумент в функцию справа:

0 |> f       == f(0)
0 |> g(1)    == g(1, 0)
0 |> h(1, 2) == h(1, 2, 0)
// and so on

-> называется 'pipe-first', и это новый синтаксический сахар, который вставляет аргумент слева в позицию аргумента first функции или конструктора данных справа :

0 -> f       == f(0)
0 -> g(1)    == g(0, 1)
0 -> h(1, 2) == h(0, 1, 2)
0 -> Some    == Some(0)

Обратите внимание, что -> относится только к BuckleScript, т.е. при компиляции в JavaScript. Он недоступен при компиляции в родной и, следовательно, не переносится. Подробнее здесь: https://reasonml.github.io/docs/en/pipe-first

person Yawar    schedule 02.04.2019