Functional thinking. Part 8
- Transfer
Hi, Habr! We are a little late return from the New Year holidays with the continuation of our series of articles on functional programming. Today we will tell about understanding of functions through signatures and definition of own types for signatures of functions. Details under the cut!
Not obvious, but in F # there are two syntaxes: for ordinary (meaningful) expressions and for defining types. For example:
[1;2;3] // обычное выражение
int list // выражение типов
Some 1 // обычное выражение
int option // выражение типов
(1,"a") // обычное выражение
int * string // выражение типов
Expressions for types have a special syntax that differs from the syntax of ordinary expressions. You may have noticed many examples of this syntax while working with FSI (FSharp Interactive), since The types of each expression are displayed along with the results of its execution.
As you know, F # uses the type inference algorithm, so often you don’t need to explicitly type in the code, especially in functions. But in order to work effectively with F #, you need to understand the type syntax so that you can define your own types, debug type-casting errors, and read function signatures. In this article, I will focus on using types in function signatures.
Here are some examples of type syntax signatures:
// синтаксис выражений // синтаксис типов
let add1 x = x + 1 // int -> int
let add x y = x + y // int -> int -> int
let print x = printf "%A" x // 'a -> unit
System.Console.ReadLine // unit -> string
List.sum // 'a list -> 'a
List.filter // ('a -> bool) -> 'a list -> 'a list
List.map // ('a -> 'b) -> 'a list -> 'b list
Understanding Signature Functions
Often, even just by examining the signature of a function, you can get some idea of what it does. Consider a few examples and analyze them in turn.
int -> int -> int
This function takes two int
parameters and returns one more int
. Most likely, this is a kind of mathematical functions, such as addition, subtraction, multiplication or exponentiation.
int -> unit
This function accepts int
and returns unit
, which means that the function does something important in the form of a side effect. Since it does not return a useful value, the side effect is most likely to write to the IO, such as logging, writing to the database, or something similar.
unit -> string
This function takes nothing, but returns string
, which may mean that the function gets a string from the air. Since there is no explicit input, the function probably does something with reading (say from a file) or generation (for example, a random string).
int -> (unit -> string)
This function accepts int
and returns another function that, when called, will return a string. Again, the function is likely to perform a read or generate operation. Input, most likely, somehow initializes the returned function. For example, input may be a file identifier, and the return function is similar to readline()
. Alternatively, the input may be the initial value for the random string generator. We cannot say for sure, but we can draw some conclusions.
'a list -> 'a
The function accepts a list of any type, but returns only one value of this type. This may indicate that the function aggregates the list or selects one of its elements. Such signatures are List.sum
, List.max
, List.head
and so on
('a -> bool) -> 'a list -> 'a list
This function takes two parameters: the first is a function that converts something into bool
(predicate), the second is a list. The return value is a list of the same type. Predicates are used to determine if an object meets a certain criterion, and it looks like this function selects items from the list according to the predicate — true or false. After that, it returns a subset of the source list. An example of a function with such a signature is List.filter
.
('a -> 'b) -> 'a list -> 'b list
The function takes two parameters: conversion from type 'a
to type 'b
and type list 'a
. The return value is a type list 'b
. It is reasonable to assume that the function takes each element from the list 'a
, and converts it to 'b
, using the function passed as the first parameter, and then returns the list 'b
. Indeed, it List.map
is a prototype of a function with such a signature.
Finding library methods using signatures
Function signatures are very important in finding library functions. The F # libraries contain hundreds of functions, which at first can be confusing. Unlike object-oriented languages, you cannot simply “enter an object” through a dot to find all the related methods. But if you know the signature of the desired function, you can quickly narrow down the search.
For example, you have two lists, and you want to find a function combining them into one. What signature would the desired function have? It would take two lists as parameters and return a third one, all of the same type:
'a list -> 'a list -> 'a list
Now let's go to the MSDN documentation site for the List module , and look for a similar function. It turns out that there is only one function with this signature:
append : 'T list -> 'T list -> 'T list
Exactly what is needed!
Defining custom types for function signatures
Someday you will want to define your own types for the desired function. This can be done using the "type" keyword:
type Adder = int -> int
type AdderGenerator = int -> Adder
In the future, you can use these types to limit the values of the parameters of functions.
For example, the second declaration due to the constraint imposed will fall with a type conversion error. If we remove it (as in the third ad), the error will disappear.
let a:AdderGenerator = fun x -> (fun y -> x + y)
let b:AdderGenerator = fun (x:float) -> (fun y -> x + y)
let c = fun (x:float) -> (fun y -> x + y)
Verify Understanding Function Signatures
Do you understand function signatures well? Check yourself if you can create simple functions with the signatures below. Avoid explicit types!
val testA = int -> int
val testB = int -> int -> int
val testC = int -> (int -> int)
val testD = (int -> int) -> int
val testE = int -> int -> int -> int
val testF = (int -> int) -> (int -> int)
val testG = int -> (int -> int) -> int
val testH = (int -> int -> int) -> int
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:
- room
#ru_general
in Slack chat F # Software Foundation - chat in Telegram
- chat in gitter
- Room #ru_general in Slack chat F # Software Foundation
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.