📜 ⬆️ ⬇️

Functional thinking. Part 3

I drove up the third part of a series of articles on functional programming. Today we will talk about all types of this paradigm and show them using examples. Learn more about primitive types, generic types, and much more under the cut!




Now that we have some understanding of the functions, we will see how types interact with functions such as domain and range. This article is just a review. For deeper immersion in types there is a series of "understanding F # types" .


First we need to understand the type notation a little better. We have seen the " -> " arrow notation separating domain and range. So the function signature always looks like this:


 val functionName : domain -> range 

Some more examples of functions:


 let intToString x = sprintf "x is %i" x //  int  string let stringToInt x = System.Int32.Parse(x) 

If you execute this code in an interactive window , you can see the following signatures:


 val intToString : int -> string val stringToInt : string -> int 

They mean:



Primitive types


There are expected primitive types: string, int, float, bool, char, byte, etc., as well as many other derivatives of the .NET type system.


A couple more examples of functions with primitive types:


 let intToFloat x = float x // "float" -  int  float let intToBool x = (x = 2) // true  x  2 let stringToString x = x + " world" 

and their signatures:


 val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string 

Type summary


In the previous examples, the F # compiler correctly defined the types of parameters and results. But this is not always the case. If you try to execute the following code, you will get a compilation error:


 let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type 

The compiler does not know the type of the argument "x", and because of this does not know whether the "Length" is a valid method. In most cases, this can be fixed by passing an "type annotation" to the F # compiler. Then he will know which type to use. In the revised version, we indicate that the type "x" is a string.


 let stringLength (x:string) = x.Length 

The brackets around the x:string parameter are important. If they are skipped, the compiler will decide that the string is the return value! That is, an "open" colon is used to indicate the type of the return value, as shown in the following example.


 let stringLengthAsInt (x:string) :int = x.Length 

We indicate that the x parameter is a string, and the return value is an integer.


Types of functions as parameters


A function that accepts other functions as parameters or returns a function is called a higher order function (the higher-order function is sometimes abbreviated to HOF). They are used as an abstraction for setting as general a behavior as possible. This type of function is very common in F #, most standard libraries use them.


Consider the evalWith5ThenAdd2 function, which takes a function as a parameter, and then calculates this function from 5 and adds 2 to the result:


 let evalWith5ThenAdd2 fn = fn 5 + 2 //   ,   fn(5) + 2 

The signature of this function is as follows:


 val evalWith5ThenAdd2 : (int -> int) -> int 

You can see that domain is (int->int) , and range is int . What does it mean? This means that the input parameter is not a simple value, but a function from the set of functions from int to int . The output value is not a function, but simply an int .


Let's try:


 let add1 x = x + 1 //  -  (int -> int) evalWith5ThenAdd2 add1 //   

and get:


 val add1 : int -> int val it : int = 8 

" add1 " is a function that maps an int to an int , as we can see from the signature. It is a valid evalWith5ThenAdd2 parameter, and its result is 8.


By the way, the special word " it " is used to denote the last calculated value, in this case it is the result we were waiting for. This is not a keyword, it is just a naming convention.


Another case:


 let times3 x = x * 3 // -  (int -> int) evalWith5ThenAdd2 times3 //   

gives:


 val times3 : int -> int val it : int = 17 

" times3 " is also a function that maps an int to an int , as seen from the signature. It is also a valid parameter for evalWith5ThenAdd2 . The result of the calculation is 17.


Note that the input data is type sensitive. If the function being passed uses float , not int , then nothing happens. For example, if we have:


 let times3float x = x * 3.0 // -  (float->float) evalWith5ThenAdd2 times3float 

The compiler, when trying to compile, will return an error:


 error FS0001: Type mismatch. Expecting a int -> int but given a float -> float 

indicating that the input function must be a function of type int->int .


Functions as output


Value functions can also be the result of functions. For example, the following function will generate an "adder" function that will add an input value.


 let adderGenerator numberToAdd = (+) numberToAdd 

Its signature is:


 val adderGenerator : int -> (int -> int) 

means that the generator accepts an int and creates a function ("adder") that matches the ints in ints . Let's see how it works:


 let add1 = adderGenerator 1 let add2 = adderGenerator 2 

Two adder functions are created. The first creates a function that adds to input 1, the second adds 2. Note that the signatures are exactly what we expected.


 val add1 : (int -> int) val add2 : (int -> int) 

Now you can use the generated functions as usual, they are no different from functions defined explicitly:


 add1 5 // val it : int = 6 add2 5 // val it : int = 7 

Using type annotations to limit function types


In the first example, we looked at the function:


 let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int 

In this example, F # can infer that " fn " converts an int to an int , so its signature will be int->int .


But what is the signature "fn" in the following case?


 let evalWith5 fn = fn 5 

It is clear that " fn " is a kind of function that accepts an int , but what does it return? The compiler cannot answer this question. In such cases, if it is necessary to specify the type of the function, you can add an annotation type for the parameters of the functions, as well as for primitive types.


 let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5 

You can also define a return type.


 let evalWith5AsString fn :string = fn 5 

Since the main function returns a string , the function " fn " is also forced to return a string . Thus, it is not required to explicitly specify the type " fn ".


Type "unit"


During the programming process, we sometimes want the function to do something without returning anything. Consider the " printInt " function. The function does not return anything. It simply prints the string to the console as a side effect of the execution.


 let printInt x = printf "x is %i" x //    

What is its signature?


 val printInt : int -> unit 

What is a " unit "?


Even if the function does not return values, it still needs a range. In the world of mathematics there are no "void" functions. Each function must return something, because the function is a display, and the display must display something!



So, in F #, functions like this return a special type of result called " unit ". It contains only one value, denoted by " () ". You might think that unit and () are something like "void" and "null" from C #, respectively. But unlike them, unit is a real type, and () real value. To verify this, it suffices to perform:


 let whatIsThis = () 

the following signature will be obtained:


 val whatIsThis : unit = () 

Which indicates that the " whatIsThis " label is of type unit and is associated with the value () .


Now, returning to the " printInt " signature, you can understand the meaning of this entry:


 val printInt : int -> unit 

This signature says that printInt has a domain of int , which is converted to something that does not interest us.


Functions without parameters


Now that we understand a unit , can we predict its appearance in a different context? For example, try to create a reusable function "hello world". Since there is no input or output, we can expect the signature unit -> unit . We'll see:


 let printHello = printf "hello world" //    

Result:


 hello world val printHello : unit = () 

Not exactly what we expected. "Hello world" was displayed immediately, and the result was not a function, but a simple value of type unit. We can say that this is a simple value, because, as we saw earlier, it has a signature of the form:


 val aName: type = constant 

In this example, we see that printHello really a simple value () . This is not a function that we can call later.


What is the difference between printInt and printHello ? In the case of printInt value cannot be determined until we know the value of the parameter x , so the definition was a function. In the case of printHello there are no parameters, so the right side can be defined in place. And it was equal to () with a side effect in the form of console output.


You can create a real reusable function without parameters, forcing the definition to have a unit argument to the argument:


 let printHelloFn () = printf "hello world" //    

Now its signature is:


 val printHelloFn : unit -> unit 

and to call it, we must pass () as a parameter:


 printHelloFn () 

Strengthening unit types with the ignore function


In some cases, the compiler requires the unit type and complains. For example, both of the following cases will cause a compiler error:


 do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello" 

To help in these situations, there is a special function ignore , which accepts anything and returns a unit . The correct version of this code could be:


 do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello" 

Generic types


In most cases, if the type of the function parameter can be any type, we need to say something about it. F # uses generics from .NET for such situations.


For example, the following function converts a parameter to a string by adding some text:


 let onAStick x = x.ToString() + " on a stick" 

No matter what type of parameter, all objects are able to ToString() .


Signature:


 val onAStick : 'a -> string 

What kind of 'a ? In F #, this is a method of indicating a generic type that is unknown at the time of compilation. An apostrophe before "a" means that the type is generic. Equivalent to this signature in C #:


 string onAStick<a>(); //   string OnAStick<TObject>(); // F#-   'a    // C#'-   "TObject"   

It should be understood that this F # function still has strong typing, even with generic types. It does not accept an Object parameter. Strong typing is good because it allows you to preserve their type safety when composing functions.


The same function is used for int , float and string .


 onAStick 22 onAStick 3.14159 onAStick "hello" 

If there are two generalized parameters, the compiler will give them two different names: 'a for the first, 'b for the second, and so on. For example:


 let concatString xy = x.ToString() + y.ToString() 

In this signature there will be two generalized types: 'a and 'b :


 val concatString : 'a -> 'b -> string 

On the other hand, the compiler recognizes when only one universal type is required. In the following example, x and y must be of the same type:


 let isEqual xy = (x=y) 

So, the function signature has the same generalized type for both parameters:


 val isEqual : 'a -> 'a -> bool 

Generalized parameters are also very important when it comes to lists and other abstract structures, and we will see quite a lot of them in the following examples.


Other types


So far, only basic types have been discussed. These types can be combined in various ways into more complex types. A complete analysis of them will be later in another series , but in the meantime, and here we briefly analyze them, so that they can be recognized in function signatures.



 string * int // ("hello", 1) 


 int list // List type  [1;2;3] string list // List type  ["a";"b";"c"] seq<int> // Seq type  seq{1..10} int [] // Array type  [|1;2;3|] 


 int option // Some 1 


Test your understanding of types


Here are a few expressions to test your understanding of function signatures. To check, just run them in the interactive window!


 let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // :     x? let testO x:string = x 1 // :    :string ? 

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


Translated 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/422115/


All Articles