Хабрахабр

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

Это математически корректное решение, однако есть и другие причины так поступать — это также приводит к очень мощной технике, называемой частичное применение функций. В предыдущем посте о каррировании мы увидели, как функции с несколькими параметрами дробятся на функции поменьше, с одним параметром. Такой стиль очень широко используется в функциональном программировании, и очень важно его понимать.

Из обсуждения каррирования можно было увидеть, как частичное применение происходит естественным образом.
Несколько простых примеров для иллюстрации: Идея частичного применения заключается в том, что если зафиксировать первые N параметров функции, получится новая функция с оставшимися параметрами.

// Создаем "сумматор" с помощью частичного применения к функции + аргумента 42
let add42 = (+) 42 // само частичное применение
add42 1
add42 3 // создаем новый список через применение функции
// к каждому элементу исходного списка
[1;2;3] |> List.map add42 // создаем предикатную функцию с помощью частичного применения к функции "меньше"
let twoIsLessThan = (<) 2 // частичное применение
twoIsLessThan 1
twoIsLessThan 3 // отфильтруем каждый элемент с функцией twoIsLessThan
[1;2;3] |> List.filter twoIsLessThan // создаем функцию "печать" с помощью частичного применения к функции printfn
let printer = printfn "printing param=%i" // итерируем список и вызываем функцию printer для каждого элемента
[1;2;3] |> List.iter printer

В каждом случае мы создаем частично примененную функцию, которую можно повторно использовать в разных ситуациях.

Вот несколько примеров: И конечно, частичное применение позволяет так же легко фиксировать параметры-функции.

// пример использования List.map
let add1 = (+) 1
let add1ToEach = List.map add1 // фиксируем функцию "add1" с List.map // тестируем
add1ToEach [1;2;3;4] // пример с использованием List.filter
let filterEvens = List.filter (fun i -> i%2 = 0) // фиксируем фильтр функции // тестируем
filterEvens [1;2;3;4]

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

  • Создаем функцию, которая складывает два числа, но в дополнение она принимает функцию логирования, которая будет логировать эти числа и результат.
  • Функция логирования принимает два параметра: (string) "name" и (generic) "value", поэтому имеет сигнатуру string->'a->unit.
  • Затем мы создаем различные реализации логирующей функции, такие как консольный логгер или логгер на основе всплывающего окна.
  • И наконец, мы частично применяем основную функцию для создания новой функции, с замкнутым логгером.

// создаем сумматор который поддерживает встраиваемый логгер-функцию
let adderWithPluggableLogger logger x y = logger "x" x logger "y" y let result = x + y logger "x+y" result result // создаем логгер-функцию которая выводит лог на консоль
let consoleLogger argName argValue = printfn "%s=%A" argName argValue // создаем сумматор с логером на консоль через частичное применение функции
let addWithConsoleLogger = adderWithPluggableLogger consoleLogger
addWithConsoleLogger 1 2
addWithConsoleLogger 42 99 // создаем логгер-функцию с выводом во всплывающее окно
let popupLogger argName argValue = let message = sprintf "%s=%A" argName argValue System.Windows.Forms.MessageBox.Show( text=message,caption="Logger") |> ignore // создаем сумматор с логгер-фукцией во всплывающее окно через частичное применение
let addWithPopupLogger = adderWithPluggableLogger popupLogger
addWithPopupLogger 1 2
addWithPopupLogger 42 99

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

// создаем еще один сумматор с частично примененным параметром 42
let add42WithConsoleLogger = addWithConsoleLogger 42
[1;2;3] |> List.map add42WithConsoleLogger
[1;2;3] |> List.map add42 //сравниваем с сумматором без логгера

Мы можем создать гибкие (хоть и сложные) библиотечные функции, причем легко сделать их по умолчанию пригодными для повторного использования, так что сложность будет сокрыта от клиентского кода. Частично примененные функции — это очень полезный инструмент.

Проектирование функций для частичного применения

Например, большинство функций в List таких как List.map и List.filter имеют схожую форму, а именно: Очевидно, что порядок параметров может серьезно влиять на удобство частичного применения.

List-function [function parameter(s)] [list]

Несколько примеров в полной форме: Список всегда является последним параметром.

List.map (fun i -> i+1) [0;1;2;3]
List.filter (fun i -> i>1) [0;1;2;3]
List.sortBy (fun i -> -i ) [0;1;2;3]

Те же самые примеры с использованием частичного применения:

let eachAdd1 = List.map (fun i -> i+1)
eachAdd1 [0;1;2;3] let excludeOneOrLess = List.filter (fun i -> i>1)
excludeOneOrLess [0;1;2;3] let sortDesc = List.sortBy (fun i -> -i)
sortDesc [0;1;2;3]

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

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

  1. Ставьте в начало параметры, которые скорее всего будут статичными
  2. Ставьте последними структуры данных или коллекции (или другие изменяющиеся параметры)
  3. Для лучшего восприятия операций, таких как вычитание, желательно соблюдать ожидаемый порядок

Параметры, которые скорее всего будут "зафиксированы" частичным применением должны идти первыми, как в примерах с логгером выше. Первый совет прост.

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

// использование конвейерной функции со списком и функциями обработки списков
let result = [1..10] |> List.map (fun i -> i+1) |> List.filter (fun i -> i>5)

параметр-список может быть опущен: Аналогично, частичное примененные функции над списками легко подвергаются композиции, т.к.

let compositeOp = List.map (fun i -> i+1) >> List.filter (fun i -> i>5)
let result = compositeOp [1..10]

Оборачивание BCL функций для частичного применения

NET легко доступны из F#, но они спроектированы без расчёта на использование в функциональных языках, таких как F#. Функции библиотеки базовых классов (base class library — BCL) . Например, большинство функций требует параметр данных вначале, в то время как в F# параметр данных в общем случае должен быть последним.

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

// создает обертку вокруг стандартного .NET метода
let replace oldStr newStr (s:string) = s.Replace(oldValue=oldStr, newValue=newStr) let startsWith lookFor (s:string) = s.StartsWith(lookFor)

После того, как строка стала последним параметром, можно использовать эти функции в конвейерах, как обычно:

let result = "hello" |> replace "h" "j" |> startsWith "j" ["the"; "quick"; "brown"; "fox"] |> List.filter (startsWith "f")

или в композиции функций:

let compositeOp = replace "h" "j" >> startsWith "j"
let result = compositeOp "hello"

Понимание конвейерного оператора

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

Функция конвейеризации определена так:

let (|>) x f = f x

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

let doSomething x y z = x+y+z
doSomething 1 2 3 // все параметры после функции

Фактически передаваемая функция f уже частично применена и ожидает лишь один параметр — входное значение для конвейеризации (т е x). В случае, когда функция f имеет несколько параметров, а в качестве входного значения x конвейеризации будет выступать последний параметр функции f.

Вот аналогичный пример, переписанный с целью частичного применения

let doSomething x y = let intermediateFn z = x+y+z intermediateFn // возвращаем intermediateFn let doSomethingPartial = doSomething 1 2
doSomethingPartial 3 // теперь только один параметр после функции
3 |> doSomethingPartial // тоже что и выше, но теперь последний параметр конвейеризован в функцию

Еще несколько примеров, которые вы возможно встречали: Как вы уже видели, конвейерный оператор чрезвычайно распространен в F#, и используется всякий раз, когда требуется сохранить естественный поток данных.

"12" |> int // парсит строку "12" в int
1 |> (+) 2 |> (*) 3 // арифметическая цепочка

Обратный конвейерный оператор

Время от времени можно встретить обратный конвейерный оператор "<|".

let (<|) f x = f x

Кажется, что эта функция ничего не делает, так почему же она существует?

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

printf "%i" 1+2 // ошибка
printf "%i" (1+2) // использование скобок
printf "%i" <| 1+2 // использование обратного конвейера

Можно использовать конвейеры сразу в двух направлениях для получения псевдоинфиксной нотации.

let add x y = x + y
(1+2) add (3+4) // ошибка
1+2 |> add <| 3+4 // псевдоинфиксная запись

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»