Functional thinking. Part 5

    In the previous post about currying, we saw how functions with several parameters are split up into smaller functions, with one parameter. This is a mathematically correct solution, but there are other reasons for doing so — it also leads to a very powerful technique called partial application of functions . This style is very widely used in functional programming, and it is very important to understand it.

    Partial application of functions

    The idea of ​​partial application is that if we fix the first N parameters of the function, we get a new function with the remaining parameters. From the discussion of currying it was possible to see how the partial application occurs naturally.
    A few simple examples to illustrate:

    // Создаем "сумматор" с помощью частичного применения к функции + аргумента 42
    let add42 = (+) 42    // само частичное применение
    add42 1
    add42 3
    // создаем новый список через применение функции
    // к каждому элементу исходного списка
    [1;2;3] |> 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

    In each case, we create a partially applied function that can be reused in different situations.

    And of course, partial application makes it easy to fix the function parameters. Here are some examples:

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

    The following, more complex example illustrates how the same approach can be used to transparently create "embedded" behavior.

    • We create a function that adds two numbers, but in addition it accepts a logging function that will log these numbers and the result.
    • The logging function takes two parameters: (string) "name" and (generic) "value", therefore, it has a signature string->'a->unit.
    • Then we create various implementations of the logging function, such as a console logger or a logger based on a popup window.
    • Finally, we partially apply the main function to create a new function, with a closed logger.

    // создаем сумматор который поддерживает встраиваемый логгер-функцию
    let adderWithPluggableLogger logger x y =
        logger "x" x
        logger "y" y
        let result = x + y
        logger "x+y"  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
          |> ignore
    // создаем сумматор с логгер-фукцией во всплывающее окно через частичное применение
    let addWithPopupLogger  = adderWithPluggableLogger popupLogger
    addWithPopupLogger  1 2
    addWithPopupLogger  42 99

    These functions with a closed logger can be used like any other functions. For example, we can create a partial application for appendix 42, and then pass it to the list function, as we did for the simple function " add42".

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

    Partially applied functions are a very useful tool. We can create flexible (albeit complex) library functions, and it is easy to make them reusable by default, so the complexity will be hidden from client code.

    Designing functions for partial applications

    Obviously, the order of the parameters can seriously affect the convenience of partial applications. For example, most functions in Listsuch as List.mapand List.filterhave a similar form, namely:

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

    The list is always the last parameter. A few examples in full form:    (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]

    The same examples using partial applications:

    let eachAdd1 = (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]

    If library functions were implemented with a different order of arguments, partial application would be much less convenient.

    When you write your function with many parameters, you can think about their best order. As with all design issues, there is no “right” answer, but there are a few generally accepted recommendations.

    1. Put in the beginning of the parameters that are likely to be static
    2. Put the latest data structures or collections (or other variable parameters)
    3. For a better perception of operations, such as subtraction, it is desirable to observe the expected order.

    The first tip is simple. The parameters that are most likely to be “fixed” by partial application should go first, as in the examples with the logger above.

    Following the second tip makes it easy to use the pipeline operator and composition. We have already seen this many times in the examples with functions on lists.

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

    Similarly, partially applied functions on lists are easily subject to composition, since list parameter can be omitted:

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

    Wrapping BCL functions for partial applications

    The functions of the base class library (base class library - BCL) .NET are easily accessible from F #, but they are designed for use in functional languages ​​such as F #. For example, most functions require a data parameter at the beginning, while in F #, the data parameter should generally be the last.

    However, it is easy enough to write wrappers to make these functions more idiomatic. In the example below, the string .NET functions are rewritten so that the target string is used last, not first:

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

    After the line has become the last parameter, you can use these functions in pipelines as usual:

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

    or in the composition of functions:

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

    Understanding the pipeline operator

    After you have seen the partial application in business, you can understand how pipeline functions work.

    The pipeline function is defined as:

    let (|>) x f = f x

    All it does is allow the argument to be set before the function, not after.

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

    In the case when the function fhas several parameters, and the xlast parameter of the function will be the input value of the pipeline f. The actually passed function is falready partially applied and expects only one parameter - the input value for pipelining (m e x).

    Here is a similar example rewritten for partial application.

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

    As you've already seen, the pipeline operator is extremely common in F #, and is used whenever you want to preserve the natural flow of data. Some more examples you may have encountered:

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

    Reverse conveyor operator

    From time to time you can meet the inverse pipeline operator "<|".

    let (<|) f x = f x

    It seems that this function does nothing, so why does it exist?

    The reason is that when the inverse pipelined operator is used as a binary operator in the infix style, it reduces the need for brackets, which makes the code cleaner.

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

    You can use pipelines in two directions at once to get pseudoinfix notation.

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

    Additional resources

    For F # there are many tutorials, including materials for those who come with C # or Java experience. The following links may be helpful as you learn more about F #:

    Several other ways to get started with learning F # are also described .

    Finally, the F # community is very friendly to beginners. There is a very active Slack chat, supported by the F # Software Foundation, with rooms for beginners that you can freely join . We strongly recommend that you do this!

    Do not forget to visit the site of the Russian-speaking community F # ! If you have any questions about learning the language, we will be happy to discuss them in chat rooms:

    About authors of translation

    Author of the translation @kleidemos
    Translation and editorial changes are made by the efforts of the Russian-speaking community of F # -developers . We also thank @schvepsss and @shwars for preparing this article for publication.

    Also popular now: