F #: What does your code turn into after compilation
The F # language appeared in the standard VisualStudio package quite recently, namely from the 2010 version (currently the most relevant one). Naturally, everyone knows this very well, the language functions on the basis of the CLR - all your code will be compiled into MS IL like any other .NET language.
Let’s look at what the compiler turns your code into, as an example of the often used and useful memorization technique. For clarity, I will write the code in F # itself and decompile it in C #.
So, the original example:
A few clarifications:
Let's now see what the memorialization led to.
The program execution time without memorization is 28.97 s, and with memorization 0.89 s. As the saying goes, the effect is obvious.
We look into what all this compiles. The most interesting point for a C # programmer is of course the moment the function is called
Let's watch.
Function
Hmm ... Function
For starters, we have two class fields
It can be seen that the second is initialized by the object of the standard class into which the closure is passed. The first field does not do anything interesting, it simply returns itself (method
So, the closure looks:
We understand that this is a class that has an interesting method:
Hm. It looks like they came where they were already. But this is not so. This returns the result of calling the function
Here! Finally we got to the function
Here it is our factorial! We found a function that directly calculates the value. Note that a memorialized version is used for recursion, as we wrote in F # code. Comparison with the template was converted to normal
Let's go back one step and see the function
Here, the fun began. As a result of the work, the object is returned to the designer parameters of which our closure (first argument) and the dictionary (second argument) are passed. Now it’s clear why the dictionary is not recreated every time.
We go deep:
The class fields once again show why the cache is one for the entire program. We look at the function
And here is our memorialization function! The key is passed through the argument.
So in F # there is no magic. Well, absolutely. All of these currying (and the function
Let’s look at what the compiler turns your code into, as an example of the often used and useful memorization technique. For clarity, I will write the code in F # itself and decompile it in C #.
So, the original example:
let memoize f =
let cache = System.Collections.Generic.Dictionary()
fun x ->
if cache.ContainsKey(x) then
cache.[x]
else
let res = f x
cache.[x] <- res
res
let rec memoizedFactorial =
let factorial x =
match x with
| a when a = 0I -> 1I
| x -> x * memoizedFactorial (x - 1I)
memoize factorial
let now = System.DateTime.Now
let arguments = [10000I .. 10100I]
let factorials = arguments |> List.map memoizedFactorial
let timeDiff = System.DateTime.Now - now
printfn "Time: %A" timeDiff
A few clarifications:
- We will consider 101 factorial from 10,000 to 10,100. In order to be longer.
- We note the execution time of our calculation.
- Since there is no built-in memorization in F #, we use a home-made one. It takes any function as an argument, initializes the cache and creates a lambda expression that is used to check the existence of the element in the cache, call the real function if the element is absent, and add the element to the cache.
- For clarity, I created a separate memorized function
memoizedFactorial
. The effect is based on the fact that we determine the functionmemoizedFactorial
without arguments, and as a result we return another function, to which we will pass the arguments later. Thus, it turns out that itmemoizedFactorial
exists in a single instance, as an object, and our cache will be the same with multiple function calls.
Let's now see what the memorialization led to.
The program execution time without memorization is 28.97 s, and with memorization 0.89 s. As the saying goes, the effect is obvious.
We look into what all this compiles. The most interesting point for a C # programmer is of course the moment the function is called
memoizedFactorial
. Why is the local function cache not recreated? Let's watch.
Function
main
:public static void main@()
{
memoizedFactorial@11-1 = LazyExtensions.Create>(new Program.clo@11-1());
memoizedFactorial@11 = Program.memoizedFactorial@11.get_Value();
now@18 = DateTime.Now;
arguments@20 = SeqModule.ToList(new Program.arguments@20(0, new BigInteger()));
factorials@21 = ListModule.Map(Program.memoizedFactorial, Program.arguments);
timeDiff@23 = (TimeSpan) (DateTime.Now - Program.now);
fp@1 = new PrintfFormat, TextWriter, Unit, Unit, TimeSpan>("Time: %A");
PrintfModule.PrintFormatLineToTextWriter>(Console.Out, Program.fp@1).Invoke(Program.timeDiff);
}
Hmm ... Function
memoizedFactorial
has become a separate object. In addition, there are no variables at all - they all became class fields internal static class $Program
. In general, up to specific classes, the code should be clear. Going deep into memoizedFactorial
. For starters, we have two class fields
$Program
:internal static FSharpFunc memoizedFactorial@11;
internal static Lazy> memoizedFactorial@11-1;
It can be seen that the second is initialized by the object of the standard class into which the closure is passed. The first field does not do anything interesting, it simply returns itself (method
get_Value
). So, the closure looks:
internal class clo@11-1 : FSharpFunc>
{
// Methods
internal clo@11-1();
public override FSharpFunc Invoke(Unit arg00@);
}
We understand that this is a class that has an interesting method:
public override FSharpFunc Invoke(Unit arg00@)
{
return Program.memoizedFactorial@11-1();
}
Hm. It looks like they came where they were already. But this is not so. This returns the result of calling the function
memoizedFactorial@11-1
. Finally, we look at this function:internal static FSharpFunc memoizedFactorial@11-1()
{
return memoize(new clo@13());
}
Here! Finally we got to the function
memoize
. And to another closure. A closure is an inheritor of the same class, we look at the Invoke method right away:public override BigInteger Invoke(BigInteger x)
{
if (LanguagePrimitives.HashCompare.GenericEqualityIntrinsic(x, BigInteger.Zero))
{
return BigInteger.One;
}
return (x * Program.memoizedFactorial@11.get_Value().Invoke(x - BigInteger.One));
}
Here it is our factorial! We found a function that directly calculates the value. Note that a memorialized version is used for recursion, as we wrote in F # code. Comparison with the template was converted to normal
if
. Also, along the way, we notice that the two are BigInteger
compared through their hashes. Let's go back one step and see the function
memoize
:public static FSharpFunc memoize(FSharpFunc f)
{
return new memoize@3(f, new Dictionary());
}
Here, the fun began. As a result of the work, the object is returned to the designer parameters of which our closure (first argument) and the dictionary (second argument) are passed. Now it’s clear why the dictionary is not recreated every time.
We go deep:
internal class memoize@3 : FSharpFunc
{
// Fields
public Dictionary cache;
public FSharpFunc f;
// Methods
internal memoize@3(FSharpFunc f, Dictionary cache);
public override b Invoke(a x);
}
The class fields once again show why the cache is one for the entire program. We look at the function
Invoke
:public override b Invoke(a x)
{
if (this.cache.ContainsKey(x))
{
return this.cache[x];
}
b res = this.f.Invoke(x);
this.cache[x] = res;
return res;
}
And here is our memorialization function! The key is passed through the argument.
f
We already know closure through object fields. The secret is unraveled. So in F # there is no magic. Well, absolutely. All of these currying (and the function
memoizedFactorial
is what it is), functions like first-class citizens are actually just automatically generated classes and objects. I hope that my article will help someone better understand how F # works inside.