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, it just so happened, raises suspicions of futility. In order to show that F # is much more than just an FNP (although just an FNP - this is a lot), I wrote all of the following.
This article, despite its fair length, does not purport to fully describe the full functionality of the language. This is just a brief overview, designed to demonstrate a wide range of features, each of which deserves a separate article, and not even one.
In addition, 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 insignificant things of a basic level. Of course, immediately heading to the pond - this is effective, but any foundation will not hurt.
And next time I will give an example on an exciting topic of F # suitability for ordinary professional programming activities.
And again, under the cut, there are really LOTS of text. And then do not say that I did not warn you. =)
F # functional
Of course, first of all, F # is a functional language, which means that it is the support of the functional paradigm that is most fully implemented in it. 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 have learned from my past posts, however, solely in order to create a complete impression of the language, I will briefly repeat all of them again.
Identifiers, keywords, functions.
So, F #, oddly enough, allows the programmer to define identifiers with which you can subsequently access functions. This is done using the let keyword, followed by the identifier name, a list of parameters, and after the equal sign - an expression that defines the function. Like that:
let k = 3.14
let square x = x**2.0
Unlike imperative programming, the first expression defines not 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 F # keywords can be seen here . Words from the second list provided by reference are not currently in use, but are reserved for the future. They can be used, but the compiler will give a warning.
F # supports curried functions, in which not all parameters can be transferred at once:
let add a b = a + b //'a -> 'a -> 'a
let addFour = add 4 //'a -> 'a
The second identifier defines the function already from one free parameter, the other is defined as 4. This once again demonstrates the thesis that the function is a value. Since the function is a value, not having received a complete set of parameters, it simply returns another function, which is also a value.
However, all functions from .NET do not have the property of curryability, and for their use in F # tuples are used - sets of several values of different types. A tuple can contain many different parameters within itself, however, F # is considered as one parameter, and as a result is applied only in its entirety. 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 a parameter of an inappropriate 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 make them curried if possible, since they obviously have more flexibility in use.
As I suppose, the reader has already noticed that in F # the functions 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, the existence of which many, I think, managed to forget - with the help of spaces. Internal calculations in a function are usually separated by four spaces:
let midValue a b =
let dif = b - a
let mid = dif / 2
mid + a
By the way, if one of those who saw the F # program was surprised by the constant presence of the #light command in the code, then one of its effects is precisely that spaces become important. This avoids the use of many keywords and characters that come from OCaml, such as in, ;;, begin, end.
Each of the identifiers has its own field of application, which starts from the place of its definition (that is, it is impossible to use it higher in code than the place of its definition), and ends at the end of the section where it was defined. For example, the intermediate identifiers dif and mid from the previous example will not act outside the midValue function.
Identifiers defined inside functions have some peculiarity in comparison with those defined externally - they can be redefined using the word let. This is useful because it allows you to not 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 a b =
let k = b - a
let k = k / 2
k + a
Moreover, since this is an override in the full sense, and not a change in the value of a variable, we can very well change not only the value of the identifier, but also its type.
let changingType () =
let k = 1
let k = "string"
F # allows in most cases to do without cycles at all due to the batch functions of processing the sequences map, list, fold, etc., however, in cases where it is necessary, you can use recursion. What is easier to understand, a cycle or recursion is a generally open question, in my opinion both are quite feasible. In order for a function in F # to access itself inside its definition, you must add the rec keyword after 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. In many cases, F # itself infers the type of a function, and it can be ambiguous, for example:
let square x = x*x
has type 'a ->' a, where 'a can be int, float, and generally speaking any, for which the * operator is overloaded.
If necessary, you can set the type of the parameter of the function yourself (for example, when you need to use class methods):
let parent (x:XmlNode) = x.ParentNode
Lambdas and operators
F # supports anonymous functions or lambdas, which are used if there is no need to give the function a name when it is passed as a parameter to another function. Lambda example below:
List.map (fun x -> x**2) [1..10]
This function will produce a list consisting of squares of all numbers from one to ten.
In addition, in F # there is another way to define a lambda using the function keyword. A lambda defined in this way may contain a pattern matching operation inside it, however, it accepts only one parameter. But even in this case, it is possible to save the curryability of the function:
function x -> function y -> x + y
Lambdas in F # support 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 #, you cannot apply the operator to operands of various types, that is, you cannot add lines with numbers (and even integers with real numbers), it is always necessary do a cast.
F # allows you to overload operators, or define your own.
let (+) a b = a - b
printfn "%d" (1 + 1) // "0"
Operators can be any sequence of the following characters! $% & * + _. / <=>? @ ^ | ~:
let (+:*) a b = (a + b) * a * b
printfn "%d" (1 +:* 2) // "6"
List initialization
Another powerful F # technique is to initialize lists, which allows you to create fairly complex lists, arrays, and sequences (the equivalent of IEnumerable) directly, using special syntax. Lists are given in square brackets [], sequences in {}, arrays in [| |].
The simplest way is to determine the gap, which is set using (..), for example:
let lst = [1 .. 10]
let seq = {'a'..'z'}
Also, by adding one more (..), 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 loops (loops 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 enter using the yield statements (adds one element to the sequence) and yield! (adds a lot of elements), and you can also use any logical constructs, loops, comparisons with the template. For example, this is how the creation of a sequence of names of 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 regular conditional statement or switch, but it has much more functionality. In general, the operation syntax looks like this:
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 located higher. The most common template looks like this: _ (underscore), and means that we are not interested in the value of the identifier. In addition, the comparison with the template should be complete (there are no unexamined features) and all calculations should produce the result of the same type.
The simplest type of operation with a template compares the 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 to be checked is assigned to it.
The most commonly used options for comparing with a template are above tuples and lists. Let x be a tuple of the form (string * int), then it is possible to write any similar template:
match x with
| "Пупкин", _ -> "Здравствуй, Вася!"
| _, i when i > 200 -> "Здравствуй, Дункан!"
| name, age -> sprintf "Здравствуйте %s, %d" name age
| _ -> "И вам тоже здрасте"
Note that if there are identifiers in the template, they will be automatically determined by the corresponding values, and you can use the name and age fields separately in the processing.
The list is processed in exactly the same way (which is actually not even a list, but a discriminated union, which will be discussed below). Usually the templates for a list ('a list) look either as [] if it is empty, or head :: tail where head is of type' a and tail is of type 'a list, however other options are possible, for example:
match lst with
|[x;y;z] -> //lst содержит три элемента, причем они присвоятся идентификаторам x y z.
|1::2::3::tail -> // lst начинается с [1,2,3] tail присвоится хвост списка
The ability to pass values to identifiers when compared with a template is so useful that in F # there is the possibility of such an assignment directly, without using template syntax, like this:
let (name, age) = x
or even so:
let (name, _) = x
if we are only interested in the first element of the tuple.
Posts
Records in F # are similar to tuples, with the difference that each field in them has a name. The definition of the entry 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"]}
Record fields are accessed, as usual, through a point. Entries can mimic classes, as will be shown below.
Marked Pool
This type in F # allows you to store data that has a different structure and meaning. For example, here is a 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. Labeled joins are always processed through comparison with a 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 marked-up union. Its informal definition looks like this:
type a' list =
|[]
|:: of a' * List
By the way, as can be seen from the above example, marked-up sets in F # can be parameterized, in the manner of generic ones.
F # imperative
Unit type
The unit type is related to the void type from C #. If the function takes no arguments, then its input type is unit; if it does not return any value, its output type is unit. For functional programming, a function that does not accept or does not return a value does not represent any value, but in the imperative paradigm it has value due to side effects (for example, input-output). The only value of the unit type is (). Such a function does not accept anything and does nothing (unit -> unit).
let doNothingWithNothing () = ()
The brackets after the name mean that it is a function with an empty input, not a value. As we have said, functions are values, but there is a big difference between a functional and a non-functional value - the second is calculated only once, and the first - with each call.
Any function that returns a value can be converted to a function that returns a unit type using the ignore function. Applying it, we kind of inform the compiler that in this function we are only interested in the side effect, and not the return value.
Mutable keyword
As we know, in the general case, 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. For this, the mutable keyword must be written before the variable name, and the value can be changed 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 throw an error:
let mainFunc () =
let mutable i = 0
let subFunc () =
i <- 1
Type ref
In F # there is another way to define variables using the ref type. To do this, just put the keyword ref in front of the calculations that represent the identifier value.
To assign a different value to a variable, the nostalgic operator: = is painfully used: access to the value of the variable is done by adding! before the variable name.
let i = ref 0
i := !i + 1
Perhaps this notation is far not as neat as the previous one, which is only worth using an exclamation mark to get the value (for negation in F # there is the keyword not)
However, unlike mutable, ref type has no restrictions on the scope, so it can be used both in nested functions and in closures. Such 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 a mutable type. The values inside the array can be reassigned, unlike the values in lists. Arrays are given in such brackets [| |], elements in it are listed through a semicolon. The array element is accessed via. [Ind], and the assignment is made using the <- operator, familiar from working with mutables. All functions for processing arrays (almost similar to methods for processing lists) are in the Array class.
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) |]
Also F # allows you to create multidimensional arrays, both “step” (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 considered as a function, which means that it must under any condition return a value of the same type. This also suggests that using else is mandatory. In fact, there is one exception - when the calculations, when the condition is met, return the type unit:
if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Sunday then
printfn "Хороших выходных!"
printfn "Каждый день замечателен!"
Shifts are also used to determine which functions belong to the loop and 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 should produce this type, otherwise the compiler will give 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 all the familiar foreach:
for item in arr
print_any item
The while loop is also quite common and familiar to an imperative programmer, its body is located between the keywords do and done, but the second can optionally be omitted using a shift system.
Calling static methods and objects from .NET libraries
You can use the entire .NET toolkit in F #, however, it is obvious that all methods written not under F # do not have the property of curryability, so they need to be given arguments in the form of a tuple corresponding to the set of input elements. In this case, the call record will not differ one iota from the sisharp one:
#light
open System.IO
if File.Exists("file.txt") then
printf "Есть такой файл!"
However, if you really want the .NET method to have curiosity, you need to import it like this:
let exists file = File.Exists(file)
Using objects is just as simple - they are created using the new keyword (who would have thought?), And using the appropriate tuple of constructor parameters. An object can be assigned an identifier using let. method calls are 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 fields immediately when creating 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 sign up for a timer event:
#light
open System.Timers
let timer = new Timer(Interval=1000, Enabled=true)
timer.Elapsed.Add(fun _ -> printfn "Timer tick!")
Unsubscribing from an event is done using the Remove method.
Operator |>
The forwarding operator |> is defined as follows:
let (|>) f g = g f
It passes the first argument as a parameter to the second argument. The second argument, of course, should 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, we can use an underdetermined function:
[1..10] |> List.iter (fun i -> print_int i)
For example, the iter function has the form ('a list -> unit) ->' a list -> unit, by setting the first parameter with a lambda, we get a function of the type 'a list -> unit, which just takes an argument-defined list as an argument.
Programs often use long chains of forwarding operators, each of which processes the value obtained by the previous one, a sort of pipeline.
F # Object Oriented
I think few people are willing to argue that it is the object-oriented paradigm that is the flagship of programming today, and of course, F # could not ignore the concepts contained in it. Let's see what he offers us.
Typing.
In F # it is possible to explicitly change the static value type. F # uses two different operators to cast up and down. Bringing up, that is, assigning a static type a value of the type of one of its ancestors, is carried out by the operator:>. The strObj value in the bottom example will be of type object.
let strObj = ("Тили-тили, трали-вали" :> obj)
The assignment down, that is, the refinement of the type of value by the type of one of its descendants, is carried out by the operator:?>.
To check the type of value (the analogue of is from C #), use the operator:?, Which can be used not only in logical constructions, but also when comparing 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 using the type inheritor as an argument. For example, such a program does not compile:
let showForm (form:Form) =
form.Show()
let ofd = new OpenFileDialog();
showForm ofd
In principle, you can explicitly cast the type: showForm (ofd:> Form), however, F # provides another way - to add a pound sign # before the type in the function signature.
let showForm (form: #Form) =
form.Show()
Thus, a certain function will take as an argument an object of any class inherited from Form.
Records and joins as objects
You can add methods to records and joins. To do this, after defining the record, you need to add the with keyword, after defining all methods write end, and use the member keyword before the identifier of each method:
type Point ={
mutable x: int;
mutable y: int; }
with
member p.Swap() =
let temp = p.x
p.x <- p.y
p.y <- temp
end
Note that the parameter p specified before the method name is used inside it to access the fields.
Classes and Interfaces
Classes in F # are defined using the type keyword, followed by the class name, equal sign, and class keyword. The class definition ends with the end keyword. In order to specify a constructor, it is necessary to include a member named new in the class definition.
type construct = class
new () = {}
end
let inst = new construct()
Please note that the class definition must contain at least one constructor, otherwise the code will not compile! F # does not provide a default constructor like C #.
To define a field, you must add the keyword val before its name.
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")
As you can see, designers can be overloaded in the usual way. The constructor cannot leave some field uninitialized, otherwise the code will not be compiled. Note that in constructors you can only initialize fields or call other constructors. To specify additional operations in the constructor, it is necessary to add after it then, after which write down all additional calculations:
new (path) as x =
{ path = path;
info = new FileInfo(path) }
then
if not x.info.Exists then printf "Нет файла!"
By default, the fields of a class are immutable; to make a certain field mutable, you must add mutable before its name.
The interface in F # is defined and implemented as follows:
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 = x.i <- y
end
end
F # offers another elegant way to define a class - implicit assignment. Immediately after the class name, the input parameters are listed, which otherwise would be included in the constructor arguments. The construction of a class takes place directly in its body, with the help of the let sequence preceding the definition of methods. All identifiers defined in this way will be private to the class. The fields and methods of the class are set using the member keyword. It’s better to immediately see an example:
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 # like C # supports only single inheritance, and implementation of several interfaces. Inheritance is specified using the inherit keyword, which comes immediately after the class:
type Base = class
val state : int
new () = {state = 0}
end
type Sub = class
inherit Base
val otherState : int
new () = {otherState = 0}
end
There is no need to explicitly call the base empty constructor. When any descendant constructor is called, an empty ancestor constructor is automatically called. If there is no such constructor in the ancestor, you must explicitly call the base constructor in the descendant constructor using the inherit keyword.
Properties in F # are defined as follows:
type PropertySample = class
let mutable field = 0
member x.Property
with get () = field
and set v = field <- rand
end
To define static fields, the static keyword is added before member and the parameter denoting the class instance is removed:
type StaticSample = class
static member TrimString (st:string) = st.Trim()
end
Conclusion
Today we briefly examined most of the basic features of the language, and it is possible to draw some conclusions based on what we saw.
Well, in fact, any operation with classes and variables available in C # can also be performed in F #, so this language is no less object-oriented than its older brother. The syntax in it is perhaps more complicated, but not critical, and it seems to be just a matter of habit. On the other hand, in functional terms, F #, as one would expect (in fact, one should expect even more, since it was described here