Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I null-protect my F# code from C# calls

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?

like image 839
Ivaylo Slavov Avatar asked Mar 08 '23 23:03

Ivaylo Slavov


1 Answers

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.

like image 69
rmunn Avatar answered Mar 16 '23 11:03

rmunn