Ответ 1
<<
является оператором композиции функций, определенным в базовой библиотеке Basics
. Все функции из Basics импортируются в проекты Elm неквалифицированными.
Система типа Elm
Вспомним основы системы типа Elm.
Elm статически напечатано. Это означает, что в Elm каждая переменная или функция имеет тип, и этот тип никогда не изменяется. Примеры типов в Elm:
-
Int
-
String
-
Maybe Bool
-
{ name : String, age : Int }
-
Int -> Int
-
Int -> String -> Maybe Char
.
Статическая типизация означает, что компилятор обеспечивает правильность типов всех функций и переменных во время компиляции, поэтому у вас нет ошибок типа времени выполнения. Другими словами, у вас никогда не будет функции типа String -> String
получения или возврата Int
, кода, который позволяет это даже не компилировать.
Вы также можете сделать свои функции полиморфными, заменив конкретный тип, такой как String
или Maybe Int
на переменную типа, которая является произвольной строчной строкой, например a
. Многие основные функции Elm являются полиморфными типами, например List.isEmpty
имеет тип List a -> Bool
. Он принимает List
некоторого типа и возвращает значение типа Bool
.
Если вы снова видите одну и ту же переменную типа, то экземпляры этой переменной типа должны быть одного типа. Например List.reverse
имеет тип List a -> List a
. Поэтому, если вы примените List.reverse
к списку целых чисел (например, к типу List Int
), он вернет список целых чисел назад. Ни в коем случае такая функция не может принимать список целых чисел, но возвращает список строк. Это гарантируется компилятором.
По умолчанию все функции в Elm curries. Это означает, что если у вас есть функция из 2 аргументов, она преобразуется в функцию из 1 аргумента, которая возвращает функцию из 1 аргумента. Вот почему вы используете синтаксис приложения Elm настолько отличается от функционального приложения на других языках, таких как Java, С++, С#, Python и т.д. Нет причин писать someFunction(arg1, arg2)
, когда вы можете написать someFunction arg1 arg2
. Зачем? Потому что на самом деле someFunction arg1 arg2
на самом деле ((someFunction arg1) arg2)
.
Currying делает частичное приложение возможным. Предположим, вы хотите частично применить List.member
. List.member
имеет тип a -> List a -> Bool
. Мы можем прочитать тип, поскольку "List.member
принимает 2 аргумента типа a
и тип List a
". Но мы также можем прочитать тип, поскольку "List.member
принимает 1 аргумент типа a
. Он возвращает функцию типа List a -> Bool
". Поэтому мы можем создать функцию isOneMemberOf = List.member 1
, которая будет иметь тип List Int -> Bool
.
Это означает, что ->
в аннотации типов функций является право-ассоциативным. Другими словами, a -> List a -> Bool
совпадает с a -> (List a -> Bool)
.
Инфиксная и префиксная нотация
Но вы все равно можете поместить двоичный оператор, например +
, перед двумя аргументами, заключая его в круглые скобки, поэтому приведенные ниже 2 приложения функций эквивалентны:
2 + 3 -- returns 5
(+) 2 3 -- returns 5, just like the previous one
Операторы Infix - это просто обычные функции. В них нет ничего особенного. Вы можете частично применить их так же, как и любую другую функцию:
addTwo : Int -> Int
addTwo = (+) 2
addTwo 3 -- returns 5
Состав функций
(<<)
является оператором композиции функций, определенным в основной библиотеке Basics
. Все функции из основ импортируются в проекты Elm неквалифицированными, то есть вам не нужно писать import Basics exposing (..)
, это уже сделано по умолчанию.
Так же, как и любой другой оператор, (<<)
- это просто функция, как и любая другая. Каков его тип?
(<<) : (b -> c) -> (a -> b) -> a -> c
Поскольку ->
является право-ассоциативным, это эквивалентно:
(<<) : (b -> c) -> (a -> b) -> (a -> c)
Другими словами, (<<)
выполняет 2 функции типов b -> c
и a -> b
соответственно и возвращает функцию типа a -> c
. Он объединяет 2 функции в одну. Как это работает? Давайте посмотрим на надуманный пример для простоты. Предположим, что у нас есть две простые функции:
addOne = (+) 1
multTwo = (*) 2
Предположим, что у нас нет (+)
, только addOne
, как бы мы создали функцию, которая добавляет 3, а не 1? Очень просто, мы будем составлять addOne
вместе 3 раза:
addThree : Int -> Int
addThree = addOne << addOne << addOne
Что делать, если мы хотим создать функцию, добавляющую 2, а затем умножить на 4?
ourFunction : Int -> Int
ourFunction = multTwo << multTwo << addOne << addOne
(<<)
содержит функции справа налево. Но приведенный выше пример прост, потому что все типы одинаковы. Как мы найдем сумму всех четных кубов списка?
isEven : Int -> Bool
isEven n = n % 2 == 0
cube : Int -> Int
cube n = n * n * n
ourFunction2 : List Int -> Int
ourFunction2 = List.sum << filter isEven << map cube
(>>)
- это одна и та же функция, но с аргументами перевернуты, поэтому мы можем написать одну и ту же композицию слева направо:
ourFunction2 = map cube >> filter isEven >> List.sum
Резюме
Когда вы видите что-то вроде h << g << f
, вы знаете, что f
, g
, h
являются функциями. Когда эта конструкция h << g << f
применяется к значению x
, вы знаете:
- Сначала Elm применяет
f
кx
- затем применяет
g
к результату предыдущего шага - затем применяет
h
к результату предыдущего шага
Поэтому (negate << (*) 10 << sqrt) 25
равно -50.0
, потому что сначала вы берете квадратный корень из 25 и получаете 5, затем умножаете 5 на 10 и получаете 50, затем вы отрицаете 50 и получаете -50.
Почему < < и не.
До Elm 0.13 (см. объявление) оператор композиции функции был (.)
, и его поведение было идентично текущему (<<)
. (<<)
был принят в Elm 0.13 с языка F # (см. вопрос Github). Elm 0.13 также добавил (>>)
как эквивалент flip (<<)
и (<|)
в качестве замены для оператора приложения функций ($)
и (|>)
как эквивалент flip (<|)
.
вызов функции Infix
Вам может показаться, что вы можете превратить обычное алфавитно-цифровое имя функции в бинарный оператор infix. До Elm 0.18 вы использовали бы обратные элементы для создания функции infix, поэтому ниже 2 будет эквивалентно:
max 1 2 -- returns 2
1 `max` 2 -- returns 2
Elm 0.18 удалила эту функцию. Вы больше не можете делать это в Elm, но такие языки, как Haskell и PureScript все еще есть.