Функциональное мышление. Часть 6

в 7:00, , рубрики: .net, F#, fsharp, fsharplangru, microsoft, Блог компании Microsoft, математика, Программирование, функциональное программирование

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

Функциональное мышление. Часть 6 - 1

Ассоциативность и композиция функций

Ассоциативность функций

Пусть, есть цепочка функций написанных в ряд. В каком порядке они будут скомбинированы?

Например, что значит эта функция?

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

  1. Верен второй вариант.
  2. Применение функций имеет левую ассоциативность.
  3. x y z значит тоже самое что и (x y) z.
  4. А w x y z равно ((w x) y) z.
  5. Это не должно выглядеть удивительным.
  6. Мы уже видели как работает частичное применение.
  7. Если рассуждать об 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".

Функциональное мышление. Часть 6 - 2

Например:

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#! Если у вас возникнут вопросы по изучению языка, мы будем рады обсудить их в чатах:

Об авторах перевода

Автор перевода @kleidemos
Функциональное мышление. Часть 6 - 3 Перевод и редакторские правки сделаны усилиями русскоязычного сообщества F#-разработчиков. Мы также благодарим @schvepsss и @shwars за подготовку данной статьи к публикации.

Автор: shwars

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js