In F#, unlike VB.NET and C#, Nullable<T> is not a language-supported construct. That is not to say that you can't use nullable types in F#. But in F#, Nullable<T> is just another type in the BCL, it doesn't get any special treatment from the compiler. Concretely, in C# you get:
- Special type syntax: C# has some syntax sugar for nullable types, e.g.
int? x = 4is shorthand for
Nullable<int> x = 4
- Implicit conversions:
int? a = nulland
int? a = 4are both implicit conversions. In the first case because
Nullable<int>is a value type so it can't really be null. In the second case there's an implicit conversion from
- Overloaded operators: arithmetic, comparison, boolean operators are overloaded for nullable types. The null coalescing (??) operator is also overloaded.
Not having this in F# is not that much of a problem actually, since you usually use an Option type instead of Nullable. Option is widely supported in F# and it has the advantage of working with any underlying type, not just value types.
However this lack of support can become annoying when interoping with code that makes extensive use of Nullables. So let's see what we can do in F# to improve this situation.
Special type syntax
Not much we can do here... then again, it's not so bad either. The type parameter of a Nullable constructor is inferred by the F# compiler so instead of:
let x = Nullable<int>(4)
we can just say:
let x = Nullable 4
If we want a null Nullable, we do have to specify the type:
let x = Nullable<int>()
except for those cases where the compiler knows the type a priori, e.g.:
let x: Nullable<int> = Nullable()
we'll see some more of these later.
Also, we could create a shorter type alias:
type N<'a when 'a: (new: unit -> 'a) and 'a: struct and 'a :> ValueType> = Nullable<'a> let x = N<int>()
but the few bytes saved are probably not worth the loss of readability.
F# does not allow implicit conversions. Just as in OCaml, you need to be explicit about the types you want (barring type inference). Sometimes this can be annoying if you're working with an API that assumes implicit conversions are part of the language, such as LINQ to XML. For these particular cases you can define an operator or shorthand to avoid calling op_Implicit constantly.
However, for Nullable types I'd avoid this. Being explicit about types (modulo type inference) is in the nature of F#.
Mapping to Option
So far we haven't done anything but complaining. So let's start writing some code for a change!
I mentioned before that Nullable is similar to Option. Indeed, mapping one to another is quite easy:
module Option = let fromNullable (n: _ Nullable) = if n.HasValue then Some n.Value else None let toNullable = function | None -> Nullable() | Some x -> Nullable(x)
This is not really an isomorphism though, like I said the domain of Nullable is smaller than the domain of Option.
A very useful feature of Options is their ability to be pattern-matched. We can define partial active patterns over Nullable to achieve the same effect:
let (|Null|_|) (x: _ Nullable) = if x.HasValue then None else Some() let (|Value|_|) = Option.fromNullable
Only problem is that the compiler can't statically assert that these partial active patterns cover all possible cases, so every time you use it you get a warning "Incomplete pattern matches on this expression". You can turn this off with a #nowarn "25" at the beginning of the file. EDIT: you can define the active pattern as a choice instead, to make it exhaustive. See kvb's comment below.
Next, we define the comparison operators. Equality already works as expected so we don't need to do anything about it. For the other operators, we'll use a convention of appending '?' as a suffix for all operators. For example, '>' becomes '>?'. We'll also use a little helper function to allow us to express Nullable comparisons in terms of their underlying type's comparison functions:
let mapBoolOp op (a: _ Nullable) (b: _ Nullable) = if a.HasValue && b.HasValue then op a.Value b.Value else false
We can also define this using pattern matching, which allows us to take advantage of type inference and makes the code a bit more concise:
let mapBoolOp op a b = match a,b with | Value x, Value y -> op x y | _ -> false
Now the definition of the operators themselves:
let inline (>?) a b = (mapBoolOp (>)) a b let inline (<?) a b = (mapBoolOp (<)) a b let inline (>=?) a b = a >? b || a = b let inline (<=?) a b = a <? b || a = b
And that's it for comparison operators.
These apply only to
let inline notn (a: bool Nullable) = if a.HasValue then Nullable(not a.Value) else Nullable()
C# doesn't have a && (short-circuit and) operator for
Nullable<bool>, but it does have a & (non-short-circuit and) operator. This is probably because the right part of the expression has to be evaluated anyway if the left part is null, so it's not much of a short-circuit evaluation. VB.NET has AndAlso and OrElse (short-circuit) for Nullable<bool>, but the documentation warns about this.
let inline (&?) a b = let rec and' a b = match a,b with | Null, Value y when not y -> Nullable(false) | Null, Value y when y -> Nullable() | Null, Null -> Nullable() | Value x, Value y -> Nullable(x && y) | _ -> and' b a and' a b
let inline (|?) a b = notn ((notn a) &? (notn b))
To define arithmetic operators, we'll use another helper function similar to mapBoolOp:
let liftNullable op (a: _ Nullable) (b: _ Nullable) = if a.HasValue && b.HasValue then Nullable(op a.Value b.Value) else Nullable()
let inline (+?) a b = (liftNullable (+)) a b let inline (-?) a b = (liftNullable (-)) a b let inline ( *?) a b = (liftNullable ( *)) a b let inline (/?) a b = (liftNullable (/)) a b
I stole the idea of lifting operators from this hubfs thread.
By the way, you might wonder why I didn't call mapBoolOp a lift. Well, at first I did, but then I read this article and found out I was using the term "lifting" the wrong way.
Let's see a basic example of the null coalescing operator applied to a nullable type in C#:
int? c = null; int d = c ?? -1;
This is just like F#'s defaultArg, except it's for Nullable. If we have to express this in F# :
let c = Nullable<int>() let d = if c.HasValue then c.Value else -1
If you find that too verbose you can hide it behind an infix operator:
let inline (|??) (a: 'a Nullable) (b: 'a) = if a.HasValue then a.Value else b let d = c |?? -1
However this same operator can't be applied when chaining multiple '??', e.g.:
int? e = null; int? f = null; int g = e ?? f ?? -1;
It's possible to define another operator for this, and combine it with a Lazy to achieve the same effect of laziness and composability (see this article, which does it for Option types), but the end result looks weird in my opinion and not worth the added complexity. Instead, we can use pattern matching:
let e = Nullable<int>() let f = Nullable<int>() let g = match e,f with Value x,_ -> x | _,Value y -> y | _ -> -1
It's not as concise as C#, but at least it's pretty clear.
Since Nullable is so similar to Option, we can also define some composable functions for Nullable just like the ones in the Option module:
module Nullable = let create x = Nullable x let getOrDefault n v = match n with Value x -> x | _ -> v let getOrElse (n: 'a Nullable) (v: 'a Lazy) = match n with Value x -> x | _ -> v.Force() let get (x: _ Nullable) = x.Value let fromOption = Option.toNullable let toOption = Option.fromNullable let bind f x = match x with | Null -> Nullable() | Value v -> f v let hasValue (x: _ Nullable) = x.HasValue let isNull (x: _ Nullable) = not x.HasValue let count (x: _ Nullable) = if x.HasValue then 1 else 0 ...
You might wonder what value Nullable.create or Nullable.get could have. The reason behind them is that constructors and properties are not really first-class functions. You can't compose or pipe them. For example, to create a nullable you have to do
let x = Nullable 5 , you can't write
let x = 5 |> Nullable.
Even though nullable types are not as nice in F# as in other .NET languages, they're fully usable and the helper functions and operators defined here make it easier to do so. Some times, though, the best bet is to map them to Option types, do your thing, and then map back to Nullables.