31

When interacting with C# libraries, I find myself wanting C#'s null coalescing operator both for Nullable structs and reference types.

Is it possible to approximate this in F# with a single overloaded operator that inlines the appropriate if case?

jbtule
  • 31,383
  • 12
  • 95
  • 128
  • Nice article that also includes option coalescing: http://troykershaw.com/blog/null-coalescing-operator-in-fsharp-but-for-options/ – Giles Jan 09 '15 at 10:28
  • @Giles that blog post is mostly outdated, it **only** includes option coalescing, and it's behavior is more succinctly expressed in f# as `let (|?) = defaultArg` – jbtule Jan 09 '15 at 17:51
  • I haven't tested it, but the article does suggest a null coalescing replacement in the form of `let inline (|??) (a: 'a Nullable) b = if a.HasValue then a.Value else b`. I'm new to F# so I may be wrong, but wouldn't your suggestion (presumably with parameters?) result in the default always being used? – Giles Jan 12 '15 at 10:47
  • Nope, `defaultArg` is a builtin function that works exactly how |? was described in the blog post. The nullable version you've described has the severe limitation of only working with the Nullable<> struct rather than any type that could have a null value. But this question is for a single operator that works for Options or Nullables or other variants, rather than having |?, |??, |??? or adding another ? for each slightly different monad. Just having one coalescing operator for all. – jbtule Jan 12 '15 at 15:02
  • Ah, I see (http://msdn.microsoft.com/en-us/library/ee340463.aspx). Thanks for the clarification. – Giles Jan 12 '15 at 15:57

3 Answers3

31

Yes, using some minor hackery found in this SO answer "Overload operator in F#".

At compiled time the correct overload for an usage of either ('a Nullable, 'a) ->'a or ('a when 'a:null, 'a) -> 'a for a single operator can be inlined. Even ('a option, 'a) -> 'a can be thrown in for more flexibility.

To provide closer behavior to c# operator, I've made default parameter 'a Lazy so that it's source isn't called unless the original value is null.

Example:

let value = Something.PossiblyNullReturned()
            |?? lazy new SameType()

Implementation:

NullCoalesce.fs [Gist]:

//https://gist.github.com/jbtule/8477768#file-nullcoalesce-fs
type NullCoalesce =  

    static member Coalesce(a: 'a option, b: 'a Lazy) = 
        match a with 
        | Some a -> a 
        | _ -> b.Value

    static member Coalesce(a: 'a Nullable, b: 'a Lazy) = 
        if a.HasValue then a.Value
        else b.Value

    static member Coalesce(a: 'a when 'a:null, b: 'a Lazy) = 
        match a with 
        | null -> b.Value 
        | _ -> a

let inline nullCoalesceHelper< ^t, ^a, ^b, ^c when (^t or ^a) : (static member Coalesce : ^a * ^b -> ^c)> a b = 
        // calling the statically inferred member
        ((^t or ^a) : (static member Coalesce : ^a * ^b -> ^c) (a, b))

let inline (|??) a b = nullCoalesceHelper<NullCoalesce, _, _, _> a b

Alternatively I made a library that utilizes this technique as well as computation expression for dealing with Null/Option/Nullables, called FSharp.Interop.NullOptAble

It uses the operator |?-> instead.

jbtule
  • 31,383
  • 12
  • 95
  • 128
  • 1
    Wow, this is extendable: `static member Coalesce(a: 'a option ref, b: unit -> 'a) = match a.Value with Some a -> a | _ -> b()`. BTW You can loose the equality constraint by matching against null – kaefer Jan 17 '14 at 23:58
  • 1
    You could also call the operator |? if you wanted. But I really wish F# would allow us to define a ?? operator. ;-) – luksan Feb 03 '14 at 13:47
  • @luksan You can make feature requests via the F# User Voice site https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30935-languages-f- or directly to fsbugs at microsoft dot com – Phillip Trelford Feb 03 '14 at 13:51
  • I'm an F# newbie. Does it work with Nullable ? I'm getting a compile error when I try to apply it: None of the types 'Nullable, int' support the operator '|??' let foo = nullableOfInt |?? 6 – dzendras Oct 08 '14 at 20:16
  • Just thinking out loud, instead of lazy is it possible to maybe put a Computation Expression i.e.: `let foo = nullableOfInt??? <@ 10 @>` – Damian May 11 '16 at 15:58
5

modified the accepted answer by jbtule to support DBNull:

//https://gist.github.com/tallpeak/7b8beacc8c273acecb5e
open System

let inline isNull value = obj.ReferenceEquals(value, null)
let inline isDBNull value = obj.ReferenceEquals(value, DBNull.Value)

type NullCoalesce =
    static member Coalesce(a: 'a option, b: 'a Lazy) = match a with Some a -> a | _ -> b.Value
    static member Coalesce(a: 'a Nullable, b: 'a Lazy) = if a.HasValue then a.Value else b.Value
    //static member Coalesce(a: 'a when 'a:null, b: 'a Lazy) = match a with null -> b.Value | _ -> a // overridden, so removed
    static member Coalesce(a: DBNull, b: 'b Lazy) = b.Value //added to support DBNull
    // The following line overrides the definition for "'a when 'a:null"
    static member Coalesce(a: obj, b: 'b Lazy) = if isDBNull a || isNull a then b.Value else a // support box DBNull
let inline nullCoalesceHelper< ^t, ^a, ^b, ^c when (^t or ^a) : (static member Coalesce : ^a * ^b -> ^c)> a b = 
                                            ((^t or ^a) : (static member Coalesce : ^a * ^b -> ^c) (a, b))

Usage:

let inline (|??) a b = nullCoalesceHelper<NullCoalesce, _, _, _> a b
let o = box null
let x = o |?? lazy (box 2)
let y = (DBNull.Value) |?? lazy (box 3)
let z = box (DBNull.Value) |?? lazy (box 4)
let a = None |?? lazy (box 5)
let b = box None |?? lazy (box 6)
let c = (Nullable<int>() ) |?? lazy (7)
let d = box (Nullable<int>() ) |?? lazy (box 8)
Community
  • 1
  • 1
Aaron West
  • 187
  • 2
  • 5
  • I wanted this coalesce operator to support DBNull.Value and box (DBNull.Value), which this code seems to do, and I think other F# users must have encountered the same requirement and might have comments or modifications to make. – Aaron West Oct 21 '15 at 17:39
0

I usually use defaultArg for this purpose as it is built-in to the language.

  • The downsides being that it only works with option and the expression that provides the default value is always evaluated. – jbtule Sep 17 '20 at 11:59