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.
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] |> 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
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:
// 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]
The following, more complex example illustrates how the same approach can be used to transparently create "embedded" behavior.
string->'a->unit
. // - let adderWithPluggableLogger logger xy = 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
These functions with a closed logger can be used like any other functions. For example, we can create partial applications 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] |> List.map add42WithConsoleLogger [1;2;3] |> List.map 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.
Obviously, the order of the parameters can seriously affect the convenience of partial applications. For example, most functions in List
such as List.map
and List.filter
have a similar form, namely:
List-function [function parameter(s)] [list]
The list is always the last parameter. A few examples in full form:
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]
The same examples using partial applications:
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]
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.
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 = [1..10] |> List.map (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 = List.map (fun i -> i+1) >> List.filter (fun i -> i>5) let result = compositeOp [1..10]
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) = s.StartsWith(lookFor)
After the line has become the last parameter, you can use these functions in pipelines as usual:
let result = "hello" |> 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"
After you have seen the partial application in business, you can understand how pipeline functions work.
The pipeline function is defined as:
let (|>) xf = fx
All it does is allow the argument to be set before the function, not after.
let doSomething xyz = x+y+z doSomething 1 2 3 //
In the case when the function f
has several parameters, and the last parameter of the function f
will act as the input value x
pipeline. The actually passed function is already partially applied and expects only one parameter - the input value for pipelining (T e x
).
Here is a similar example rewritten for partial application.
let doSomething xy = 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 //
From time to time you can meet the inverse pipeline operator "<|".
let (<|) fx = fx
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 xy = x + y (1+2) add (3+4) // 1+2 |> add <| 3+4 //
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:
#ru_general
in Slack chat F # Software FoundationTranslated by @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.
Source: https://habr.com/ru/post/430622/
All Articles