Functional thinking. Part 7

Original author: Scott Wlaschin
  • Transfer

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!

Definition of functions

We already know how to create ordinary functions using the "let" syntax:

let add x y = x + y

In this article we will look at some other ways to create functions, as well as tips on how to define them.

Anonymous functions (lambda)

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:

  • lambdas should start with a keyword funthat is not required in C #
  • use a single arrow ->instead of a double =>from C #.

Lambda-definition of the addition function:

let add = fun x y -> x + y

The same function in the traditional form:

let add x y = 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] |> add1
// лямбда функция переданная без описания отдельной функции
[1..10] |> (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 adderGenerator, this time only in lambdas.

let adderGenerator = fun x -> (fun y -> x + y)

Is it clear to you that all three definitions are equivalent?

let adderGenerator1 x y = 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!

Pattern matching with pattern

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" f l
// использование шаблона
let f2 {first=f; last=l} =          // сопоставление с образцом прямо в описании функции
   printfn "first=%s; last=%s" f l
// тест
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).

Common error: tuples vs. many parameters

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 x y = x + y
// функция которая принимает один параметр - кортеж
let addTuple aTuple =
   let (x,y) = aTuple
   x + y
// другая функция которая принимает один кортеж как параметр
// но выглядит так будто принимает два параметра
let addConfusingTuple (x,y) = x + y

  • The first definition, " addTwoParams", takes two parameters, separated by a space.
  • The second definition, " addTuple", takes one parameter. This parameter binds the "x" and "y" from the tuple and summarizes them.
  • The third definition, " addConfusingTuple", takes one parameter 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 happens 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". Then he complains that the expected first parameter is addTwoParamsnot int, and an attempt was made to transfer the 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 are passed, it addConfusingTuplemust 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.

Why not use tuples as 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 indicating a tuple (int*int*int).

When do I need to submit arguments with separate parameters, and when with a tuple?

  • When tuples are significant in their own right. For example, for operations in three-dimensional space, triple tuples will be more convenient than three coordinates separately.
  • Sometimes tuples are used to combine data that must be stored together into a single structure. For example, TryParsemethods from .NET libraries return a 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 .

Special case: tuples and functions of the .NET library

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"

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 x y = System.String.Compare(x,y)
// частично применяем ее
let strCompareWithB = strCompare "B"
// используем с функцией высшего порядка
|> strCompareWithB

Selection Guide for Individual and Grouped Parameters

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 general, it is always better to use separate parameters instead of passing one structure, be it a tuple or a record. This allows for more flexible behavior, such as partial application.
  • But, when a group of parameters has to be transferred at once, some grouping mechanism should be used.

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 x y = 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 myCredentialsin front of aNamethe last function?

Functions without parameters

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 should always be called with an unitargument:

// вызов

What happens quite often when interacting with .NET libraries:


Remember, call them with unitparameters!

Definition of new operators

You can define functions using one or more operator symbols (see the documentation for a list of symbols):

// описываем
let (.*%) x y = 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 # it (*plays the role of the beginning of a comment (as /*...*/in C #):

let ( *+* ) x y = 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 type names >=>and <*>.

Point-free style

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 x y = 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. A " List.reduce (+)" makes it clear that the addition operation is important, regardless of the information on 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 (|>) x f = f x             // прямой pipe
let (<|) f x = f x             // обратный pipe
let (>>) f g x = g (f x)       // прямая композиция
let (<<) g f x = g (f x)       // обратная композиция

On the other hand, functions like "printf", although primitive, are not combinators, because they are dependent on the external world (I / O).

Combinatorial birds

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 x y = x              // the Kestrel
let M x = x >> x           // the Mockingbird
let T x y = y x            // the Thrush (выглядит знакомо!)
let Q x y z = y (x z)      // the Queer bird (тоже знакомо!)
let S x y z = x z (y z)    // The Starling
// и печально известный...
let rec Y f x = 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

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 module Listis 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.

Recursive functions

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 a 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.

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: