4

I am writing a library in F#, with some interfaces and base classes that are publicly visible. Generally, I avoid specifying [<AllowNullLiteral>] on my custom types as this complicates the validation logic in my F# code (see this nice post for goods and bads of null handing in F# to get a picture ), and also, F# does not initially allow null for F# types. So, I validate for nulls only for types that accept the null value as valid.

However, an issues arises when my library is used from another .NET language, such as C#. More particularly, I worry how should I implement methods that accept F#-declared interfaces, when called by C# code. The interface types are nullable in C#, and I suspect that there will not be an issue for the C# code to pass a null to my F# method.

I fear that the caller would crash and burn with a NPE, and the problem is that I am not even allowed to properly handle that in the F# code -- say throw an ArgumentNullException -- because the respective interface lack the AllowNullLiteral attribute. I fear that I would have to use the attribute and add the related null-checking logic in my F# code to eventually prevent such a disaster.

Are my fears reasonable? I am a little confused as I initially attempt to stick to the good F# practices and avoid null as much as possible. How does this change if among my goals is to allow C# code to subclass and implement the interfaces I created in F#? Do I have to allow nulls for all non-value types coming from my F# code if they are public and can be accessed from any CLR language? Is there a best practice or a good advice to follow?

Ivaylo Slavov
  • 8,839
  • 12
  • 65
  • 108

2 Answers2

5

There are two basic approaches you can take:

  1. Document in your API design that passing null to your library is not allowed, and that the calling code is responsible for ensuring that your library never receives a null. Then ignore the problem, and when your code throws NullReferenceExceptions and users complain about it, point them to the documentation.

  2. Assume that the input your library receives from "outside" cannot be trusted, and put a validation layer around the "outside-facing" edge of your library. That validation layer would be responsible for checking for null and throwing ArgumentNullExceptions. (And pointing to the documentation that says "No nulls allowed" in the exception message).

As you can probably guess, I favor approach #2, even though it takes more time. But you can usually make a single function, used everywhere, to do that for you:

let nullArg name message =
    raise new System.ArgumentNullException(name, message)

let guardAgainstNull value name =
    if isNull value then nullArg name "Nulls not allowed in Foo library functions"

let libraryFunc a b c =
    guardAgainstNull a nameof(a)
    guardAgainstNull b nameof(b)
    guardAgainstNull c nameof(c)
    // Do your function's work here

Or, if you have a more complicated data structure that you have to inspect for internal nulls, then treat it like a validation problem in HTML forms. Your validation functions will either throw an exception, or else they will return valid data structures. So the rest of your library can ignore nulls completely, and be written in a nice, simple, idiomatic-F# way. And your validation functions can handle the interface between your domain functions and the untrusted "outside world", just as you would with user input in an HTML form.

Update: See also the advice given near the bottom of https://fsharpforfunandprofit.com/posts/the-option-type/ (in the "F# and null" section), where Scott Wlaschin writes, "As a general rule, nulls are never created in "pure" F#, but only by interacting with the .NET libraries or other external systems. [...] In these cases, it is good practice to immediately check for nulls and convert them into an option type!" Your library code, which expects to get data from other .NET libraries, would be in a similar situation. If you want to allow nulls, you'd convert them to the None value of an Option type. If you want to disallow them and throw ArgumentNullExceptions when you get passed a null, you'd also do that at the boundaries of your library.

rmunn
  • 34,942
  • 10
  • 74
  • 105
  • In favor of #2 which I like better, I assume that the respecting types that could result in a NPE would be annotated with `[]` as well in order to prevent warnings/errors from the F# compiler – Ivaylo Slavov May 29 '17 at 12:48
  • 2
    The `isNull` function doesn't require you to use the `AllowNullLiteral` attribute, so I'd recommend *against* using that attribute. C# ignores it anyway, so *not* having it doesn't protect you from getting nulls from **C#** code that calls your functions. But if you don't use `AllowNullLiteral`, then at least any **F#** code that calls your library's functions will be forbidden from passing them nulls. Unless you have a specific *need* to use nulls, it's better to avoid the `AllowNullLiteral` attribute, and call the `isNull` function if you have to protect yourself from C# code. – rmunn May 29 '17 at 12:57
  • 1
    If you need to express the "this may not be present" aspect from F# code, you should (as you already know) use `Option`s rather than nulls. And since the C# code that needs to call your library won't understand the `Option` type, write thin C# wrappers that use `Option.ofObj` on their input. That will turn any nulls into `None`, and any non-null objects into `Some (whatever)`, in a way that the F# compiler can type-check for you. – rmunn May 29 '17 at 13:00
  • 1
    By "thin C# wrappers" above, I mean "thin functions written in **F#** that are only intended to be called from C# code". Their only job is to call `Option.ofObj` on their input, then pass the resulting `Option`s on to your main F# functions that do all the work. For a good example of what I mean, look at [this bit of code from FsCheck](https://github.com/fscheck/FsCheck/blob/master/src/FsCheck/Commands.fs#L36-L57). There's one function intended to be called from F# (and hidden from C# IDEs), and two functions intended to be called from C# (and that give warnings if called from F#). – rmunn May 29 '17 at 13:02
  • Excellent! It was the kind of solution that I was actually looking for, but unfortunately I am still learning the F# stuff to really see this trough. Now that you explained it a little I feel better. I dislike the `AllowNullLiteral` as well, because it makes me feel uncomfortable calling my own code from F# internal methods/functions. It seems I would be able to overcome this trough using the `isNull` function in the public methods only. – Ivaylo Slavov May 29 '17 at 13:02
2

Based on @rmunn's advice I ended up creating a simple null2option function:

let null2option arg = if obj.ReferenceEquals(arg, null) then None else Some arg

It solved most of my cases alone. If I expect a null argument to be coming for the calling code I would simply use this idioms:

match null2option arg with | None -> nullArg "arg" "Message" | _ -> ()
Ivaylo Slavov
  • 8,839
  • 12
  • 65
  • 108
  • 2
    The [`Option.ofObj`](https://github.com/fsharp/fsharp/blob/ebf311505d8035e7ba53bcc7493067f8fe8ba5f9/src/fsharp/FSharp.Core/option.fs#L88) function in the standard `Option` module would also do that, BTW, and according to https://stackoverflow.com/a/6818329/2314532 it's equally fast as the `ReferenceEquals`-based function you implemented. (Both clocked in at exactly 202 ms of CPU time for 10 million calls). Since there's a just-as-fast function in the standard F# library, I'd recommend using it rather than your own implementation, if only for the sake of readability and simplicity. – rmunn May 29 '17 at 15:57
  • 1
    The only drawback is that `Option.ofObj` requires the argument to be of type that allows `null` as a proper value, which implies usage of `[]` or putting unnecessary constraints to generic arguments if the check is against a generic type, while this one avoids both. At least I think so unless I am missing something – Ivaylo Slavov May 29 '17 at 18:03
  • No, it seems that you're right. I thought that `Option.ofObj` would work on any type, but a bit of testing proves that you can't pass plain record types to it. Whereas the `obj.ReferenceEquals` approach that you used in your `null2option` function works for both C#-originated types *and* F#-originated types. – rmunn May 30 '17 at 00:13