📜 ⬆️ ⬇️

Three F # Paradigms

Introduction


Anyone who is somehow connected with .NET programming knows that already in the next version of Visual Studio a new programming language will be built in - F #, which is positioned as functional, which immediately led to suspicions of uselessness. In order to show that F # is much more than just a FNP (although just FNP is a lot), I wrote all of the following.
This article, despite the hefty length, does not pretend to fully describe all the functionality of the language. This is just a brief overview, designed to demonstrate a wide range of possibilities, each of which deserves a separate article, and not even one.
Besides, having written such a lengthy post, I wanted to make a reserve for the future, so that in the future I would not be distracted by minor things of a basic level. Of course, immediately head over to the pond - this is effective, but even some kind of foundation will not interfere.
And the next time I will give an example on the exciting topic of F # suitability for ordinary professional programming.
And once again, under the cut is really a lot of text. And do not say later that I did not warn you. =)

F # functional


Of course, first of all, F # is a functional language, which means it supports the functional paradigm in it most fully. As you know, many keywords and literals in it are borrowed from OCaml, which is not surprising since Don Syme, the main creator of F #, once had a hand in OCaml.
A lot of knowledge about F #, as a pure functional programming language, the reader could already learn from my previous posts, but solely in order to create a complete impression of the language, I will briefly repeat all of them once again.

Identifiers, keywords, functions.


So, F #, oddly enough, allows the programmer to determine the identifiers with which it will be possible to subsequently access the functions. This is done using the let keyword, followed by the name of the identifier, the list of parameters, and after the equal sign, the expression that defines the function. Like that:
let k = 3.14
let square x = x**2.0

Unlike imperative programming, the first expression does not define a variable, but rather a constant, since its value cannot be changed during program execution. Generally speaking, F # does not distinguish between functions and values ​​— any function is a value that can also be freely passed as a parameter.
A list of all the F # keywords can be seen here . The words from the second list given by reference are not used at the moment, but are reserved for the future. They can be used, but the compiler will give a warning.
F # supports curried functions, in which you can transfer not all parameters at once:
let add ab = a + b //'a -> 'a -> 'a
let addFour = add 4 //'a -> 'a

The second identifier sets the function already from one free parameter, the other is defined as 4. This again demonstrates the thesis that the function is a value. Since a function is a value, without having received a complete set of parameters, it simply returns another function, which is also a value.
However, all functions from .NET do not possess the property of curvature, and for their use in F # tuples are used - sets of several different types of values. A tuple can contain many different parameters within itself, however, F # is considered as one parameter, and as a result, it is applied only as a whole. Tuples are written in parentheses, separated by commas.

let add (a,b) = a + b
let addFour = add 4

Such code will not be compiled, since according to F # we are trying to apply the function to the parameter of the wrong type, namely int instead of 'a *' b.
However, it should be remembered that when developing your own functions, especially those that will be used by other programmers, you should, if possible, make them curried, since they obviously have greater flexibility in use.
As I believe, the reader has already noticed that in F # in functions you do not need to explicitly define the return value. However, it is not clear how to calculate the intermediate values ​​inside the function? Here, F # uses a method that many people have, I think, have forgotten - with the help of spaces. Internal calculations in a function are usually separated by four spaces:
let midValue ab =
let dif = b - a
let mid = dif / 2
mid + a

By the way, if someone from those who saw the program on F # was surprised by the constant presence in the code of the #light command , then one of its effects is precisely that the spaces become important. This avoids the use of multiple keywords and characters that come from OCaml, such as in , ;;, begin , end .
Each of the identifiers has its own scope, which starts from the place of its definition (that is, it cannot be applied higher in code than the place of its definition), but ends at the end of the section where it was defined. For example, the intermediate dif and mid identifiers from the previous example will not act outside the midValue function.
Identifiers defined inside functions have some peculiarity in comparison with those defined on the external level — they can be redefined using the word let . This is useful because it allows you not to invent all new, and most often, little meaningful names for holding intermediate values. For example, in the previous example we could write this:
let midValue ab =
let k = b - a
let k = k / 2
k + a

Moreover, since this redefinition is in the full sense, and not a change in the value of a variable, we can easily change not only the value of the identifier, but also its type.
let changingType () =
let k = 1
let k = "string"

F # makes it possible in most cases to do without cycles at all at the expense of the batch functions for processing the sequences map, list, fold, etc., however, in those cases where it is necessary, recursion can be used. What is easier to understand, a cycle or a recursion is a whole open question, in my opinion, both are perfectly possible. In order for a function in F # to be able to refer to itself within its definition, it is necessary to add the keyword rec after the let .

F # is a strongly typed language, that is, you cannot use functions with values ​​of the wrong type. Functions, like any values, have their own type. F # in many cases itself displays the type of the function, while it can be determined ambiguously, for example:
let square x = x*x

has type 'a -> ' a, where 'a can be int, float, and generally speaking anyone for which the * operator is overloaded.
If necessary, the type of the parameter of the function can be set yourself (for example, when you need to use the class methods):
let parent (x:XmlNode) = x.ParentNode

')

Lambda and Operators


F # supports anonymous functions or lambdas, which are used if there is no need to name the function when it is passed as a parameter to another function. Lambda example below:
List.map (fun x -> x**2) [1..10]

This function will display a list consisting of squares of all numbers from one to ten.
In addition, in F # there is another way to define lambda using the function keyword. A lambda defined in this way can contain a pattern matching operation within itself, but it takes only one parameter. But even in this case, it is possible to preserve the curvature of the function:
function x -> function y -> x + y

Lambda in F # supports closure , but this will be discussed in more detail in the second part of the review.
In F #, operators (unary and binary) can be considered as a more aesthetic way to call functions. As in C #, operators are overloaded, so they can be used with different types, however, unlike C #, the operator cannot be applied to operands of different types, that is, strings with numbers cannot be added (and even integers with real ones); do a ghost
F # allows you to overload operators, or define your own.
let (+) ab = a - b
printfn "%d" (1 + 1) // "0"

Operators can be any sequence of the following characters! $% & * + _. / <=>? @ ^ | ~:
let (+:*) ab = (a + b) * a * b
printfn "%d" (1 +:* 2) // "6"


Initializing Lists


Another powerful F # technique is list initialization, which allows you to create fairly complex lists, arrays, and sequences (equivalent to IEnumerable) directly, using special syntax. Lists are specified in square brackets [], sequences are specified in {}, arrays are specified in [| |].
The simplest way is to define a space, which is specified using (..), for example:
let lst = [1 .. 10]
let seq = { 'a' .. 'z' }

Also, by adding another (..) you can set the selection step in the interval:
let lst = [1 .. 2 .. 10] // [1, 3, 5, 7, 9]

In addition, when creating lists, you can use cycles (cycles can be either single or nested to any degree)
let lst = [for i in 1..10 -> i*i] // [1, 4, 9,..]

However, this is not all. When initializing lists, you can explicitly specify which elements to add using the yield (adds one element to the sequence) and yield! (adds many elements), and you can also use any logical constructions, cycles, comparisons with the template. For example, this is how the creation of a sequence of names for all files contained in this folder and in all its subfolders looks like:
let rec xamlFiles dir filter =
seq { yield! Directory.GetFiles(dir, filter)
for subdir in Directory.GetDirectories(dir) do yield! xamlFiles subdir filter}


Pattern comparison


Comparison with the template is a bit like a conventional conditional statement or switch, but it has much more functionality. In general, the syntax of the operation is as follows:
match with
[|]1|2|..|10 -> 1
|11 when 1 -> 2
...

Comparison with templates goes from top to bottom, so you should not forget that narrower templates should be placed higher. The most common pattern is: _ (underscore), and means that we are not interested in the value of the identifier. In addition, the comparison with the template must be complete (there are no unexamined capabilities) and all calculations must produce a result of the same type.
The simplest type of template operation compares an identifier with a certain value (numeric, string). Using the when keyword, you can add a condition to the template, so that calculations will be performed
If another identifier is substituted for the value, then the value of the identifier being checked is assigned to it.
The most commonly used pattern comparison options are over tuples and lists. Let x be a tuple of the form (string * int), then it is possible to write any similar pattern:
match x with
| "" , _ -> ", !"
| _, i when i > 200 -> ", !"
| name, age -> sprintf " %s, %d" name age
| _ -> " "

Note that if there are identifiers in the template, they are automatically determined by the corresponding values, and the name and age fields can be used separately in processing.
The list is processed in exactly the same way (which in fact is not even a list, but a marked-up union (discriminated union), which are discussed below). Usually, templates for a list ('a list) appear either as [] if it is empty, or head :: tail where head is of the type' a, and tail is' a of the list, however other options are possible, for example:
match lst with
|[x;y;z] -> //lst , xy z.
|1::2::3::tail -> // lst [1,2,3] tail

When comparing with a template, the ability to pass in identifiers of values ​​is so useful that in F # there is the possibility of such an assignment directly, without using a template syntax, like this:
let (name, age) = x

or even like this:
let (name, _) = x

if we are only interested in the first element of the tuple.

Records


Records (record) in F # are similar to tuples, with the difference that each field has a name in them. The entry definition is enclosed in braces and separated by a semicolon.
type org = { boss : string; tops :string list }
let Microsoft = { boss = "Bill Gates" ; tops = [ "Steve Balmer" , "Paul Allen" ]}

The entry fields are accessed, as usual, through a dot. Entries can mimic classes, as shown below.

Marked union


This type in F # allows you to store data that has a different structure and meaning. For example, this is the type:
type Distance =
|Meter of float
|Feet of float
|Mile of float

let d1 = Meter 10
let d2 = Feet 65.5

Although all three types of data are of the same type (which is optional), they are obviously different in meaning. Processing of tagged associations is always done through comparison with the template.
match x with
|Meter x -> x
|Feet x -> x*3.28
|Mile x -> x*0.00062

As already mentioned, such a common data type as a list is a markup union. Its informal definition looks like this:
type a' list =
|[]
|:: of a' * List

By the way, as is noticeable from the above example, marked sets in F # can be parameterized in the manner of generics.

F # imperative



Type of unit


The unit type is related to the void type of C #. If the function takes no arguments, then its input type is unit; if it returns no value, its output type is unit. For functional programming, a function that does not accept or return a value is of no value, but in the imperative paradigm it has value due to side effects (for example, input-output). The only value of the type unit is (). Such a function takes nothing and does nothing (unit -> unit).
let doNothingWithNothing () = ()

The parentheses after the name mean that this is a function with an empty input, and not a value. As we have said, functions are values, but there is a big difference between functional and non-functional values ​​- the second is calculated only once, and the first is with each call.
Any function that returns a value can be converted to a function that returns the type unit using the ignore function. Using it, we kind of tell the compiler that in this function we are only interested in the side effect, not the return value.

Keyword mutable


As we know, in general, identifiers in F # can be defined by some value, but this value cannot be changed. However, good old imperative variables are still useful, so F # provides a mechanism for creating and using variables. To do this, you must write the mutable keyword in front of the variable name, and you can change the value using the <- operator.
let mutable i = 0
i <- i + 1

However, the use of such variables is limited, for example, they cannot be used in internal functions, as well as for closure in lambdas. This code will generate an error:
let mainFunc () =
let mutable i = 0
let subFunc () =
i <- 1


Ref type


In F #, there is another way to define variables using the type ref . To do this, you just need to put the ref keyword before the calculations, which represent the value of the identifier.
In order to assign a different value to a variable, the nostalgic operator is painfully used: =, the variable is referenced by adding! before the variable name.
let i = ref 0
i := !i + 1

Perhaps this notation is not as neat as the previous one, which is only the use of an exclamation mark to get the value (for negation in F # there is the keyword not)
However, unlike the mutable , the ref type has no restrictions on the scope, so it can be used both in nested functions and in closures. This code will work:
let i = ref 2
let lst = [1..10]
List.map (fun x -> x * !i) lst


Arrays


In F #, there are arrays that are mutable. Values ​​inside an array can be reassigned, unlike values ​​in lists. Arrays are specified in brackets [| | |], items in it are listed separated by a semicolon. The access to the array element is done through. [Ind], and the assignment is done by the <<- operator familiar with working with mutables. All functions for processing arrays (almost similar to the methods for processing lists) are in the class Array.
let arr = [|1; 2; 3|]
arr.[0] <- 10 // [|10,2,3|]

Arrays can be initialized in exactly the same way as lists, using .., yield, etc.
let squares = [| for x in 1..9 -> x,x*x |] // [| (1,1);(2,4);...;(9,81) |]

F # also allows you to create multi-dimensional arrays, both “stepped” (with subarrays of different lengths) and “monolithic”.

Control logic


In F #, you can use the usual imperative control logic - the conditional operator if ... then ... else , as well as for and while loops.
It should be remembered that the if statement can also be viewed as a function, which means it must, under any condition, produce a value of the same type. This also implies that the use of else is mandatory. In fact, there is one exception - when the calculations with the performed condition return the type unit:
if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Sunday then
printfn " !"
printfn " !"

Shifts are also used to determine which functions are related to the cycle, which are not. For example, in the upper example, the second sentence will be displayed regardless of the day of the week.
The for loop in F # is of type unit, so calculations in the body of the loop must produce this type, otherwise the compiler will generate a warning.
let arr = [|1..10|]
for i = 0 to Array.length arr - 1 do
printfn arr.[i]

If you want to go in the opposite direction, then to is replaced by downto , as in the good old days.
You can also use another form of the for loop, similar to everyone you know foreach:
for item in arr
print_any item

The while loop is also quite common and familiar to the imperative programmer, its body is located between the keywords do and done , but the second can be optionally omitted using the shift system.

Calling static methods and objects from .NET libraries


In F #, you can use the entire set of .NET tools, however, it is obvious that all methods not written under F # do not have the property of curvature, so they need to be given arguments in the form of a tuple corresponding in type to the set of input elements. At the same time, the call record will not be a jot different from sisharp:
#light
open System.IO
if File.Exists( "file.txt" ) then
printf " !"

However, if you really want the .NET method to have curvature, you need to import it, like this:
let exists file = File.Exists(file)

Using objects is just as easy - they are created using the new keyword (who would have thought?), And using the appropriate tuple of constructor parameters. An object can be assigned to an identifier using let . method call is similar to static, fields are changed using <- .
#light
let file = new FileInfo( "file.txt" )
if not file.Exists then
using (file.CreateText()) (fun stream ->
stream.WriteLine( "Hello world" ))
file.Attributes <- FileAttributes.ReadOnly

F # allows you to initialize the fields immediately when you create an object, like this:
let file = new FileInfo( "file.txt" , Attributes = FileAttributes.ReadOnly)


Using events in F #


Each event in F # has an Add method that adds a handler function to the event. The handler function must be of type 'a -> unit. Here's how to subscribe to a timer event:
#light
open System.Timers
let timer = new Timer(Interval=1000, Enabled=true)
timer.Elapsed.Add(fun _ -> printfn "Timer tick!" )

Cancellation of an event is done using the Remove method.

Operator |>


The forwarding operator |> is defined as follows:
let (|>) fg = gf

It passes the first argument as a parameter to the second argument. The second argument, of course, must be a function that takes a value of type f as the only parameter. By the way, precisely because of the possibility of using the forwarding operator, all functions on lists (iter, map, fold) take the list itself last. Then, as g, you can use an undefined function:
[1..10] |> List.iter (fun i -> print_int i)

For example, the iter function has the form ('a list -> unit) -> ' a list -> unit, setting the first parameter to lambda, we get a function of the type 'a list -> unit, which just takes as an argument the list defined before the statement.
The programs often use long chains of forwarding operators, each of which processes the value obtained by the previous one, a sort of conveyor.

F # object oriented


I think very few people are willing to argue with the fact that the object-oriented paradigm is currently the flagship of programming, and of course, F # could not ignore the concepts inherent in it. Let's see what he offers us.

Typification.


In F #, it is possible to explicitly change the static value type. For F #, two different operators are used to bring up and down. The reduction upwards, i.e. the assignment to a static type of a value of the type of one of its ancestors, is done by the operator:>. The value of strObj in the lower example will be of type object.

let strObj = ( "-, -" :> obj)

The assignment down, i.e., the specification of the type of a value by the type of one of its descendants, is carried out by the operator:?>
To check the value type (analogue is from C #), use the operator:?, Which can be used not only in logical constructions, but also when compared with a template.
match x with
|:? string -> printf " !"
|:? int -> printf " !"
|:? obj -> printf " !"

Usually, F # does not take into account the hierarchy of type inheritance when calculating functions, that is, it does not allow to use a type-successor as an argument. For example, such a program will not compile:
let showForm (form:Form) =
form.Show()
let ofd = new OpenFileDialog();
showForm ofd

In principle, you can explicitly give the type: showForm (ofd:> Form), but F # provides another way - to add a # sign in front of the type in the function signature.
let showForm (form: #Form) =
form.Show()

Thus, a certain function will take as its argument an object of any class inherited from the Form.

Records and Connections as Objects


Methods can be added to the records and associations. To do this, after defining the entry, you must add the keyword with , after defining all the methods write end , and before the identifier of each method use the member keyword:
type Point ={
mutable x: int;
mutable y: int; }
with
member p.Swap() =
let temp = px
px <- py
py <- temp
end

Notice that the p parameter specified before the method name is used inside it to access the fields.

Classes and Interfaces


F# type , , class . end . , , new .
type construct = class
new () = {}
end
let inst = new construct()

, , ! F# , C#.
, val .
type File = class
val path: string
val info : FileInfo
new () = new File( "default.txt" )
new (path) =
{ path = path;
info = new FileInfo(path) }
end
let file1 = new File( "sample.txt" )

, . , . , . , then , :
new (path) as x =
{ path = path;
info = new FileInfo(path) }
then
if not x.info.Exists then printf " !"

, , mutable .
F# :
let ISampleInterface = interface
abstract Change : newVal : int -> unit
end

type SampleClass = class
val mutable i : int
new () = { i = 0}
interface ISampleInterface with
member x.Change y = xi <- y
end
end

F# — . , . , let, . . member . :
type Counter (start, inc, length) = class
let finish = start + length
let mutable current = start
member c.Current = current
member c.Inc () =
if current > finish then failwith "-!"
current <- current + inc
end

let count = new Counter(0, 5, 100)
count.Inc()

F# C# , . inherit class :

type Base = class
val state : int
new () = {state = 0}
end

type Sub = class
inherit Base
val otherState : int
new () = {otherState = 0}
end

. . , inherit .
F# :
type PropertySample = class
let mutable field = 0
member x.Property
with get () = field
and set v = field <- rand
end

member static , :
type StaticSample = class
static member TrimString (st:string) = st.Trim()
end


Conclusion



, - .
, , , C#, F#, -, . , , , . , F#, ( ,

Source: https://habr.com/ru/post/55272/


All Articles