After a little excursion into the basic types, we can return to the functions again. In particular, to the previously mentioned riddle: if a mathematical function can take only one parameter, then how in F # can there be a function that accepts more parameters? More under the cut!
The answer is quite simple: a function with several parameters is rewritten as a series of new functions, each of which takes only one parameter. The compiler performs this operation automatically, and it is called currying , in honor of Haskell Curry, a mathematician who greatly influenced the development of functional programming.
To see how currying works in practice, let's use the simplest code example that prints two numbers:
// let printTwoParameters xy = printfn "x=%iy=%i" xy
In fact, the compiler rewrites it in approximately the following form:
// let printTwoParameters x = // let subFunction y = printfn "x=%iy=%i" xy // , subFunction //
Consider this process in more detail:
printTwoParameters
" is printTwoParameters
, but it takes only one parameter: "x".By rewriting functions in this way, the compiler ensures that each function takes only one parameter, as required. Thus, using " printTwoParameters
", you might think that this is a function with two parameters, but in fact a function with only one parameter is used. You can verify this by passing only one argument instead of two:
// printTwoParameters 1 // val it : (int -> unit) = <fun:printTwoParameters@286-3>
If we calculate it with one argument, we will not get an error - the function will be returned.
So, this is what actually happens when printTwoParameters
is called with two arguments:
printTwoParameters
with the first argument (x)printTwoParameters
returns a new function, in which "x" is closed.Here is an example of step-by-step and normal versions:
// let x = 6 let y = 99 let intermediateFn = printTwoParameters x // - // x let result = intermediateFn y // let result = (printTwoParameters x) y // let result = printTwoParameters xy
Here is another example:
// let addTwoParameters xy = x + y // let addTwoParameters x = // ! let subFunction y = x + y // subFunction // // let x = 6 let y = 99 let intermediateFn = addTwoParameters x // - // x let result = intermediateFn y // let result = addTwoParameters xy
Again, a “two-parameter function” is actually a single-parameter function that returns an intermediate function.
But wait, what about the " +
" operator? Is this a binary operation that should take two parameters? No, it is also curried, like other functions. This is a function called " +
" that takes one parameter and returns a new intermediate function, exactly as addTwoParameters
above.
When we write the expression x+y
, the compiler reorders the code in such a way as to convert the infix to (+) xy
, which is a function called +
that takes two parameters. Note that the "+" function needs parentheses to indicate that it is used as a normal function, and not as an infix operator.
Finally, a function with two parameters, called +
, is treated like any other function with two parameters.
// let x = 6 let y = 99 let intermediateFn = (+) x // "" "" let result = intermediateFn y // let result = (+) xy // let result = x + y
And yes, it works for all other operators and built-in functions, such as printf
.
// let result = 3 * 5 // - let intermediateFn = (*) 3 // "" 3 let result = intermediateFn 5 // printfn let result = printfn "x=%iy=%i" 3 5 // printfn - let intermediateFn = printfn "x=%iy=%i" 3 // "3" let result = intermediateFn 5
Now that we know how the curried functions work, it's interesting to know what their signatures will look like.
Returning to the first example, " printTwoParameter
", we saw that the function took one argument and returned an intermediate function. The intermediate function also took one argument and did not return anything (ie, unit
). Therefore, the intermediate function was of type int->unit
. In other words, domain printTwoParameters
is an int
, and range is an int->unit
. Putting it all together we will see the final signature:
val printTwoParameters : int -> (int -> unit)
If you compute an explicitly curried implementation, you can see the parentheses in the signature, but if you compute an ordinary, implicitly curried implementation, the brackets will not be:
val printTwoParameters : int -> int -> unit
Brackets are optional. But they can be represented in the mind in order to simplify the perception of function signatures.
And what is the difference between a function that returns an intermediate function and a regular function with two parameters?
Here is a function with one parameter that returns another function:
let add1Param x = (+) x // signature is = int -> (int -> int)
But a function with two parameters that returns a simple value:
let add2Params xy = (+) xy // signature is = int -> int -> int
Their signatures are slightly different, but in practical terms there is not much difference between them, except for the fact that the second function is automatically curried.
How does currying work for functions with more than two parameters? In the same way: for each parameter, except the last, the function returns an intermediate function that closes the previous parameter.
Consider this difficult example. I have explicitly declared the types of the parameters, but the function does nothing.
let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () // let intermediateFn1 = multiParamFn 42 // multoParamFn int (bool -> string -> float -> unit) // intermediateFn1 bool // (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2 string // (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 float // (unit) let finalResult = intermediateFn3 3.141
The signature of the entire function:
val multiParamFn : int -> bool -> string -> float -> unit
and intermediate function signatures:
val intermediateFn1 : (bool -> string -> float -> unit) val intermediateFn2 : (string -> float -> unit) val intermediateFn3 : (float -> unit) val finalResult : unit = ()
The function signature can indicate how many parameters the function accepts: it’s enough to count the number of arrows outside the brackets. If the function accepts or returns another function, there will be more arrows, but they will be in brackets and they can be ignored. Here are some examples:
int->int->int // 2 int int string->bool->int // string, - bool, // int int->string->bool->unit // (int,string,bool) // (unit) (int->string)->int // , // ( int string) // int (int->string)->(int->bool) // (int string) // (int bool)
Until you understand the logic behind the currying, it will produce some unexpected results. Remember that you will not get an error if you run a function with fewer arguments than expected. Instead, you get a partially applied function. If you then use a partially applied function in a context where a value is expected, you can get a little understood error from the compiler.
Consider a function that is harmless at first sight:
// let printHello() = printfn "hello"
What do you think will happen if you call it as shown below? Will "hello" be displayed on the console? Try to guess before execution. Hint: look at the function signature.
// printHello
Contrary to the expectations of the call will not be. The original function expects a unit
as an argument that was not passed. Therefore, a partially applied function was obtained (in this case without arguments).
What about this case? Will it be compiled?
let addXY xy = printfn "x=%iy=%i" x x + y
If you run it, the compiler will complain about the line with printfn
.
printfn "x=%iy=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, ie is missing //arguments. Its type is ^a -> unit.
If there is no understanding of currying, this message can be very mysterious. The point is that all expressions that are evaluated separately (i.e. are not used as a return value or a binding to something by means of "let") must be calculated in a unit
value. In this case, it is not calculated in the unit
value, but instead returns a function. This is a long way to say that printfn
lacks an argument.
In most cases, errors like this happen when interacting with the library from the .NET world. For example, the Readline
method of the TextReader
class should accept the unit
parameter. You can often forget about this and not put brackets, in this case you cannot get a compiler error at the time of the “call”, but it will appear when you try to interpret the result as a string.
let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // , printfn "The line is %s" line1 // // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() // printfn "The line is %s" line2 //
In the code above, line1
is just a pointer or delegate to the Readline
method, not a line, as one would expect. Using ()
in reader.ReadLine()
will actually call the function.
You can get the same mysterious messages, if you pass too many parameters to the function. Some examples of passing too many parameters to printf
:
printfn "hello" 42 // ==> error FS0001: This expression was expected to have // type 'a -> 'b but here has type unit printfn "hello %i" 42 43 // ==> Error FS0001: Type mismatch. Expecting a 'a -> 'b -> 'c // but given a 'a -> unit printfn "hello %i %i" 42 43 44 // ==> Error FS0001: Type mismatch. Expecting a 'a->'b->'c->'d // but given a 'a -> 'b -> unit
For example, in the latter case, the compiler reports that a formatting line with three parameters is expected (the signature 'a -> 'b -> 'c -> 'd
has three parameters), but instead a line with two is received (at signature 'a -> 'b -> unit
two parameters).
In cases where printf
is not used, the transfer of a large number of parameters often means that at a certain stage of the calculations a simple value was obtained which the parameter is attempted to pass. The compiler will be outraged that a simple value is not a function.
let add1 x = x + 1 let x = add1 2 3 // ==> error FS0003: This value is not a function // and cannot be applied
If we break the general call into a series of explicit intermediate functions, as we did earlier, we can see what exactly is going wrong.
let add1 x = x + 1 let intermediateFn = add1 2 // let x = intermediateFn 3 //intermediateFn ! // ==> error FS0003: This value is not a function // and cannot be applied
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:
#ru_general
in Slack chat F # Software FoundationTranslated 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/430620/
All Articles