Продолжаем нашу серию статей о функциональном программировании на F#. Сегодня расскажем об ассоциативности и композиции функций, а также сравним композицию и конвейер. Заглядывайте под кат!

Ассоциативность и композиция функций
Ассоциативность функций
Пусть, есть цепочка функций написанных в ряд. В каком порядке они будут скомбинированы?
Например, что значит эта функция?
let F x y z = x y zЗначит ли это, что функция y должна быть применена к аргументу z, а затем полученный результат должен быть передан в x? Т.е.:
let F x y z = x (y z)Или функция x применяется к аргументу y, после чего функция, полученная в результате, будет вычислена с аргументом z? Т.е.:
let F x y z = (x y) z- Верен второй вариант.
- Применение функций имеет левую ассоциативность.
x y zзначит тоже самое что и(x y) z.- А
w x y zравно((w x) y) z. - Это не должно выглядеть удивительным.
- Мы уже видели как работает частичное применение.
- Если рассуждать об
xкак о функции с двумя параметрами, то(x y) z— это результат частичного применения первого параметра, за которым следует передача аргументаzк промежуточной функции.
Если нужна правая ассоциативность, можно использовать скобки или pipe. Следующие три записи эквивалентны:
let F x y z = x (y z)
let F x y z = y z |> x // использование прямого конвейера
let F x y z = x <| y z // использование обратного конвейераВ качестве упражнения, попробуйте вывести сигнатуры этих функций без реального вычисления.
Композиция функций
Мы упоминали композицию функций несколько раз, но что в действительности означает данный термин? Он кажется устрашающим на первый взгляд, но на самом деле все довольно просто.
Скажем, у нас есть функция "f", которая сопоставляет тип "T1" к типу "T2". Также у нас есть функция "g", которая преобразует тип "T2" в тип "T3". Тогда мы можем соединить вывод "f" и ввод "g", создав новую функцию, которая преобразует тип "T1" к типу "T3".

Например:
let f (x:int) = float x * 3.0 // f это ф-ция типа int->float
let g (x:float) = x > 4.0 // g это ф-ция типа float->boolМы можем создать новую функцию "h", которая берет вывод "f" и использует его в качестве ввода для "g".
let h (x:int) =
let y = f(x)
g(y) // возвращаем результат вызова gЧуть более компактно:
let h (x:int) = g ( f(x) ) // h это функция типа int->bool
//тест
h 1
h 2Так далеко, так просто. Это интересно, мы можем определить новую функцию "compose", которая принимает функции "f" и "g" и комбинирует их даже не зная их сигнатуры.
let compose f g x = g ( f(x) )После выполнения можно увидеть, что компилятор правильно решил, что "f" — это функция обобщенного типа 'a к обобщенному типу 'b, а "g" ограничена вводом типа 'b:
val compose : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c(Обратите внимание, что обобщенная композиция операций возможна только благодаря тому, что каждая функция имеет ровно один входной параметр и один вывод. Данный подход невозможен в нефункциональных языках.)
Как мы видим, данное определение используется для оператора ">>".
let (>>) f g x = g ( f(x) )Благодаря данному определению можно строить новые функции на основе существующих при помощи композиции.
let add1 x = x + 1
let times2 x = x * 2
let add1Times2 x = (>>) add1 times2 x
//тест
add1Times2 3Явная запись весьма громоздка. Но можно сделать ее использование более простым для понимания.
Во первых, можно избавиться от параметра x, и композиция вернет частичное применение.
let add1Times2 = (>>) add1 times2Во вторых, т.к. >> является бинарным оператором, можно поместить его в центре.
let add1Times2 = add1 >> times2Применение композиции делает код чище и понятнее.
let add1 x = x + 1
let times2 x = x * 2
// по старому
let add1Times2 x = times2(add1 x)
// по новому
let add1Times2 = add1 >> times2Использование оператора композиции на практике
Оператор композиции (как и все инфиксные операторы) имеет более низкий приоритет, чем обычные функции. Это означает, что функции использованные в композиции могут иметь аргументы без использования ск��бок.
Например, если у функций "add" и "times" есть параметры, они могут быть переданы во время композиции.
let add n x = x + n
let times n x = x * n
let add1Times2 = add 1 >> times 2
let add5Times3 = add 5 >> times 3
//тест
add5Times3 1Пока соответствующие входы и выходы функций совпадают, функции могут использовать любые значения. Например, рассмотрим следующий код, который выполняет функцию дважды:
let twice f = f >> f //сигнатура ('a -> 'a) -> ('a -> 'a)Обратите внимание, что компилятор вывел, что "f" принимает и возвращает значения одного типа.
Теперь рассмотрим функцию "+". Как мы видели ранее, ввод является int-ом, но вывод в действительности — (int->int). Таким образом "+" может быть использована в "twice". Поэтому можно написать:
let add1 = (+) 1 // сигнатура (int -> int)
let add1Twice = twice add1 // сигнатура так же (int -> int)
//тест
add1Twice 9С другой стороны нельзя написать:
let addThenMultiply = (+) >> (*)Потому что ввод "*" должен быть int, а не int->int функцией (который является выходом сложения).
Но если подправить первую функцию так, чтобы она возвращала только int, все заработает:
let add1ThenMultiply = (+) 1 >> (*)
// (+) 1 с сигнатурой (int -> int) и результатом 'int'
//тест
add1ThenMultiply 2 7Композиция также может быть выполнена в обратном порядке посредством "<<", если это необходимо:
let times2Add1 = add 1 << times 2
times2Add1 3Обратная композиция в основном используется для того, чтобы сделать код более похожим на английский язык ("English-like"). Например:
let myList = []
myList |> List.isEmpty |> not // прямой конвейер
myList |> (not << List.isEmpty) // использование обратной композицииКомпозиция vs. конвейер
Вы можете быть сбиты с толку небольшой разницей между композицией и конвейером, т.к. они могут выглядеть очень похожими.
Во первых, посмотрите на определение конвейера:
let (|>) x f = f xВсе это позволяет поставить аргументы функций перед ней, а не после. Вот и все. Если у функции есть несколько параметров, то ввод должен быть последним параметром (в текущем наборе параметров, а не вообще). Пример, встречаемый ранее:
let doSomething x y z = x+y+z
doSomething 1 2 3 // все параметры указаны после функции
3 |> doSomething 1 2 // последний параметр конвейеризирован в функциюКомпозиция не тоже самое и не может быть заменой пайпу. В следующем примере даже число 3 не функция, поэтому "вывод" не может быть передан в doSomething:
3 >> doSomething 1 2 // ошибка
// f >> g то же самое что и g(f(x)) так что можем переписать это:
doSomething 1 2 ( 3(x) ) // подразумевается что 3 должно быть функцией!
// error FS0001: This expression was expected to have type 'a->'b
// but here has type intКомпилятор жалуется, что значение "3" должно быть разновидностью функций 'a->'b.
Сравните это с определением композиции, которая берет 3 аргумента, где первые два должны быть функциями.
let (>>) f g x = g ( f(x) )
let add n x = x + n
let times n x = x * n
let add1Times2 = add 1 >> times 2Попытки использовать конвейер вместо композиции обернутся ошибкой компиляции. В следующем примере "add 1" — это (частичная) функция int->int, которая не может быть использована в качестве второго параметра для "times 2".
let add1Times2 = add 1 |> times 2 // ошибка
// x |> f то же самое что и f(x) так что можем переписать это:
let add1Times2 = times 2 (add 1) // add1 должно быть 'int'
// error FS0001: Type mismatch. 'int -> int' does not match 'int'Компилятор пожалуется, что "times 2" необходимо принимать параметр int->int, т.е. быть функцией (int->int)->'a.
Дополнительные ресурсы
Для F# существует множество самоучителей, включая материалы для тех, кто пришел с опытом C# или Java. Следующие ссылки могут быть полезными по мере того, как вы будете глубже изучать F#:
Также описаны еще несколько способов, как начать изучение F#.
И наконец, сообщество F# очень дружелюбно к начинающим. Есть очень активный чат в Slack, поддерживаемый F# Software Foundation, с комнатами для начинающих, к которым вы можете свободно присоединиться. Мы настоятельно рекомендуем вам это сделать!
Не забудьте посетить сайт русскоязычного сообщества F#! Если у вас возникнут вопросы по изучению языка, мы будем рады обсудить их в чатах:
- комната
#ru_generalв Slack-чате F# Software Foundation - чат в Telegram
- чат в Gitter
- комната #ru_general в Slack-чате F# Software Foundation
Об авторах перевода
Автор перевода @kleidemos
Перевод и редакторские правки сделаны усилиями русскоязычного сообщества F#-разработчиков. Мы также благодарим @schvepsss и @shwars за подготовку данной статьи к публикации.
