Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unchecked.defaultof on a list

Tags:

f#

Consider:

> Unchecked.defaultof<list<int>>;;
val it : int list = null
> 2 :: 1 :: Unchecked.defaultof<list<int>>;;
val it : int list = [2]

What's the reasoning behind this behavior? I would expect the final list to be [2;1] or raise an exception.

like image 290
Jizugu Avatar asked Nov 22 '12 22:11

Jizugu


1 Answers

This is pretty amazing question. I looked how F# lists are compiled and it explains the behaviour. I would not consider this as bug (that's why defaultof is in the Unchecked module, it might as well burn down your computer!)

First of all, the empty list [] is not represented as null in F# 2.0, because then you would not be able to invoke methods on empty list - and this would break many things (like passing empty list to any Seq function). As an aside, the None value is represented as null for efficiency, which means that option<'a> cannot implement the seq<'a> interface even though it would make sense.

So instead, F# uses a special value list.Empty to represent empty lists. Now this value is an instance of the same type as values representing non-empty list (the type is FSharpList<'T>). The only difference is that both head and tail fields of the instance are null.

Now, you can maybe see where this is going - an empty list is essentially compiled to an object where the this.tail field is null (and the this.head field is null too, but this is not that important).

The list.Tail property will throw an exception when the field this.tail is null (because this means you're getting the tail of an empty list - which is an error), but there is also an internal property list.TailOrNull which returns the value without checking and which is used to compile pattern matching.

For example, here is a simple iter function:

let rec iter (xs:int list) = 
  match list with
  | x::xs -> Console.WriteLine(x); iter xs
  | [] -> ()

This is compiled to the following C# code:

if (list.TailOrNull != null) {
  Console.WriteLine(list.HeadOrDefault);
  iter(list.TailOrNull);
}

This checks whether list is an empty list by checking whether its list.TailOrNull is null. If it is then it knows that the current list value is the list.Empty value representing an empty list (and it does not print its HeadOrDefault because it assumes that this is null too).

If you use defaultof to create null value explicitly then 42::null gives you an object where TailOrNull is null but HeadOrDefault is 42. So that's why the value is not printed.

TL;DR - Do not use Unchecked.defaultof with F# types. You never know what will happen.

like image 76
Tomas Petricek Avatar answered Oct 07 '22 17:10

Tomas Petricek