We continue our series of articles on functional F # programming. Today we have a very interesting topic: the definition of functions. Including, let's talk about anonymous functions, functions without parameters, recursive functions, combinators and much more. Look under the cat!
We already know how to create ordinary functions using the "let" syntax:
let add xy = x + y
In this article we will look at some other ways to create functions, as well as tips on how to define them.
If you are familiar with lambdas in other languages, the following paragraphs will seem familiar. Anonymous functions (or "lambda expressions") are defined as follows:
fun parameter1 parameter2 etc -> expression
Compared to C # lambdas, there are two differences:
fun
keyword, which is not required in C #->
is used instead of double =>
from C #.Lambda-definition of the addition function:
let add = fun xy -> x + y
The same function in the traditional form:
let add xy = x + y
Lambdas are often used in the form of small expressions or when there is no desire to define a separate function for the expression. As you have seen, this is not uncommon when working with lists.
// let add1 i = i + 1 [1..10] |> List.map add1 // [1..10] |> List.map (fun i -> i + 1)
Note that you need to use brackets around lambdas.
Also lambdas are used when obviously another function is needed. For example, the previously discussed " adderGenerator
", which we discussed earlier, can be rewritten using lambda.
// let adderGenerator x = (+) x // let adderGenerator x = fun y -> x + y
The lambda version is slightly longer, but immediately makes it clear that an intermediate function will be returned.
Lambda can be nested. Another example of the definition of adderGenerator
, this time only in lambda.
let adderGenerator = fun x -> (fun y -> x + y)
Is it clear to you that all three definitions are equivalent?
let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y)
If not, reread the currying chapter . This is very important to understand!
When a function is defined, parameters can be passed to it explicitly, as in the examples above, but it can also be mapped to a template directly in the parameters section. In other words, the parameters section may contain patterns (matching patterns), and not just identifiers!
The following example demonstrates the use of templates in a function definition:
type Name = {first:string; last:string} // let bob = {first="bob"; last="smith"} // // let f1 name = // let {first=f; last=l} = name // printfn "first=%s; last=%s" fl // let f2 {first=f; last=l} = // printfn "first=%s; last=%s" fl // f1 bob f2 bob
This type of matching can occur only when a match is always solvable. For example, it is impossible to match types of association and lists in this way, because some cases cannot be compared.
let f3 (x::xs) = // printfn "first element is=%A" x
The compiler will give a warning about the incompleteness of the match (an empty list will cause an error in runtime at the entrance to this function).
If you come from a C-like language, the tuple used as the only function argument can painfully resemble a multi-parameter function. But this is not the same thing! As I noted earlier, if you see a comma, this is most likely a tuple. Parameters are separated by spaces.
An example of confusion:
// let addTwoParams xy = x + y // - let addTuple aTuple = let (x,y) = aTuple x + y // // let addConfusingTuple (x,y) = x + y
addTwoParams
", takes two parameters, separated by a space.addTuple
", takes one parameter. This parameter binds the "x" and "y" from the tuple and summarizes them.addConfusingTuple
", takes one parameter as well as " addTuple
", but the trick is that this tuple is unpacked (matched with the pattern) and bound as part of the parameter definition using pattern matching. Behind the scenes, everything is exactly the same as in addTuple
.Look at the signatures (always look at them if you are not sure about something).
val addTwoParams : int -> int -> int // val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int
And now here:
// addTwoParams 1 2 // ok -- addTwoParams (1,2) // error - // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b
Here we see an error in the second call.
First, the compiler treats (1,2)
as a generalized tuple of the form ('a * 'b)
, which it tries to pass as the first parameter to " addTwoParams
". After that, it complains that the expected first parameter addTwoParams
not an int
, but an attempt was made to transfer a tuple.
To make a tuple, use a comma!
addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 // , // ! addTuple y // ok addConfusingTuple y // ok
And vice versa, if you pass several arguments to a function waiting tuple, you also get an incomprehensible error.
addConfusingTuple 1 2 // error -- // => error FS0003: This value is not a function and // cannot be applied
This time, the compiler decided that once two arguments were addConfusingTuple
, addConfusingTuple
should be curried. And the " addConfusingTuple 1
" entry is a partial application and should return an intermediate function. An attempt to call this intermediate function with the parameter "2" will generate an error, because There is no intermediate function! We see the same error as in the currying chapter, where we discussed problems with too many parameters.
The discussion of tuples above shows another way to define functions with multiple parameters: instead of transferring them separately, all parameters can be gathered into one structure. In the example below, the function takes a single parameter — a tuple of three elements.
let f (x,y,z) = x + y * z // - int * int * int -> int // f (1,2,3)
Note that the signature is different from the signature of a function with three parameters. There is only one arrow, one parameter and asterisks pointing to the tuple (int*int*int)
.
When do I need to submit arguments with separate parameters, and when with a tuple?
TryParse
methods from the .NET library return the result and a boolean variable in the form of a tuple. But for storing a large amount of related data it is better to define a class or record ( record .When calling .NET libraries, commas are very common!
They all take tuples, and the calls look the same as in C #:
// System.String.Compare("a","b") // System.String.Compare "a" "b"
The reason lies in the fact that classic .NET functions are not curried and cannot be partially applied. All parameters must always be transmitted immediately, and the most obvious way is to use a tuple.
Note that these calls only look like the transfer of tuples, but in fact this is a special case. You cannot transfer real tuples to such functions:
let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error
If there is a desire to partially apply the .NET functions, it is enough to write wrappers over them, as was done earlier , or as shown below:
// let strCompare xy = System.String.Compare(x,y) // let strCompareWithB = strCompare "B" // ["A";"B";"C"] |> List.map strCompareWithB
Discussion of tuples leads to a more general topic: when parameters should be separate, and when grouped?
Attention should be paid to how F # differs from C # in this respect. In C #, all parameters are always transferred, so this question does not even arise there! In F #, due to partial application, only some of the parameters can be represented, so it is necessary to distinguish between the case when the parameters should be combined and the case when they are independent.
General recommendations on how to structure the parameters when designing your own functions.
In other words, when developing a function, ask yourself "Can I provide this option separately?". If the answer is no, then the parameters should be grouped.
Consider a few examples:
// . // , let add xy = x + y // // , let locateOnMap (xCoord,yCoord) = // // // - type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = // let setCustomerName first last = // // // // , let setCustomerName myCredentials aName = //
Finally, make sure that the order of the parameters helps in partial application (see the manual here ). For example, why did I put myCredentials
before aName
in the last function?
Sometimes you may need a function that does not accept any parameters. For example, you need the function "hello world" which can be called multiple times. As shown in the previous section, the naive definition does not work.
let sayHello = printfn "Hello World!" //
But this can be corrected by adding the unit parameter to the function or using lambda.
let sayHello() = printfn "Hello World!" // let sayHello = fun () -> printfn "Hello World!" //
After that, the function must always be called with the unit
argument:
// sayHello()
What happens quite often when interacting with .NET libraries:
Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory()
Remember, call them with unit
parameters!
You can define functions using one or more operator symbols (see the documentation for a list of symbols):
// let (.*%) xy = x + y + 1
You must use brackets around the characters to define a function.
Operators starting with *
require a space between the bracket and *
, since in F # (*
plays the role of the beginning of a comment (like / /*...*/
in C #)
let ( *+* ) xy = x + y + 1
Once defined, a new function can be used in the usual way if it is wrapped in parentheses:
let result = (.*%) 2 3
If the function is used with two parameters, you can use the infix operator record without brackets.
let result = 2 .*% 3
You can also define prefix operators starting with !
or ~
(with some restrictions, see the documentation )
let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello"
In F #, the definition of operators is quite a frequent operation, and many libraries will export operators with names like >=>
and <*>
.
We have already seen many examples of functions that lacked the latest parameters in order to reduce the level of chaos. This style is called point-free style or silent programming (tacit programming) .
Here are some examples:
let add xy = x + y // let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 // let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list // let sum = List.reduce (+) // point free
This style has its pros and cons.
One of the advantages is that the emphasis is on the composition of higher-order functions instead of fussing with low-level objects. For example, " (+) 1 >> (*) 2
" is an explicit addition followed by multiplication. And " List.reduce (+)
" makes it clear that the addition operation is important, irrespective of the information about the list.
Pointless style allows you to focus on the basic algorithm and to identify common features in the code. The " reduce
" function used above is a good example. This topic will be discussed in a scheduled list processing series.
On the other hand, excessive use of this style can make the code obscure. Explicit parameters act as documentation and their names (such as "list") make it easier to understand what the function does.
Like everything in programming, the best recommendation is to prefer the approach that provides the most clarity.
" Combinators " call functions whose result depends only on their parameters. This means that there is no dependence on the outside world, and, in particular, no other functions or global values can affect them.
In practice, this means that combinatorial functions are limited by the combination of their parameters in various ways.
We have already seen several combinators: the "pipe" (pipeline) and the composition operator. If you look at their definitions, it is clear that all they do is reorder the parameters in various ways.
let (|>) xf = fx // pipe let (<|) fx = fx // pipe let (>>) fgx = g (fx) // let (<<) gfx = g (fx) //
On the other hand, functions like "printf", although primitive, are not combinators, because they are dependent on the external world (I / O).
Combinators are the basis of a whole section of logic (naturally called "combinatorial logic"), which was invented many years before computers and programming languages. Combinatorial logic has a very large influence on functional programming.
To learn more about combinators and combinatorial logic, I recommend the book "To Mock a Mockingbird" by Raymond Smullyan. In it, he explains other combinators and fancifully gives them the names of birds . Here are a few examples of standard combinators and their bird names:
let I x = x // , Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling // ... let rec Y fx = f (Y f) x // Y-, Sage bird
Letter names are quite standard, so you can refer to the K-combinator to anyone who is familiar with this terminology.
It turns out that many common programming patterns can be represented through these standard combinators. For example, Kestrel is a regular pattern in the fluent interface where you do something, but return the original object. Thrush is a pipe, Queer is a direct composition, and the Y-combinator does an excellent job with creating recursive functions.
In fact, there is a well-known theorem that any computable function can be constructed using only two basic combinators, Kestrel and Starling.
Combinator libraries are libraries that export a multitude of combinatorial functions that are designed to be shared. The user of such a library can easily combine functions together to get even larger and more complex functions, like cubes easily.
A well-designed combinator library allows you to focus on high-level features and hide low-level “noise”. We have already seen their power in several examples in the "why use F #" series, and the List
module is full of such functions, " fold
" and " map
" are also combinators, if you think about it.
Another advantage of combinators is that they are the safest type of function. Since they do not have dependencies on the outside world; they cannot change when the global environment changes. A function that reads a global value or uses library functions may break or change between calls if the context changes. This will never happen to combinators.
In F #, combinator libraries are available for parsing (FParsec), creating HTML, testing frameworks, etc. We will discuss and use the combinators later in the next series.
Often a function needs to refer to itself from its body. A classic example is the Fibonacci function.
let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
Unfortunately, this function will not be able to compile:
error FS0039: The value or constructor 'fib' is not defined
You must tell the compiler that this is a recursive function using the keyword rec
.
let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
Recursive functions and data structures are very common in functional programming, and I hope to devote a whole series to this topic later.
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/433398/
All Articles