A Story Of Reliable Code
Imagine you are a C# programmer and you are designing API of a tiny search engine. The engine is really simple — it searches for goods in a database using a query string provided by a user. You sit down, decide not to focus on implementation details yet, and write a simple C# interface:
Fine. You still don’t want to focus on implementation details, you’d like to ask your trainee to implement that interface for you. But before doing that, you ask yourself — what will happen, if your mate writes buggy code and such code passes code review? You decide to analyse potential issues and try to eliminate them. So, what’s wrong with the contract defined above?
Searchmethod accepts a string. C# doesn’t prevent you from passing
nullinstead of a string to that method, but will the search engine implementation handle the
nullvalue correctly? Will it fail fast or return some search results? No answer.
Searchmethod returns a
Task<T>, which can also be
null. Yeah, it is a really bad practice to return
nullfrom methods returning tasks, but bad things happen.
Searchmethod can return a
nullvalue. Is this meant to be correct behavior or not, what that
nullvalue means — again, no answer.
- The search method executes correctly, consumes a query string and returns a sequence of search results. Assuming
SearchResultis a reference type, is there any guarantee, that our program will never fail with
NullReferenceExceptionwhen we try to access a
SearchResultinstance property in our code? No, we have to trust the implementation details.
- Exceptions. Who knows, what exceptions a custom search engine might throw. The exceptions apparently are not documented anywhere yet.
ISearchEngine.Search method is something like this:
No one writes such code! Most time we don’t handle all these cases separately and simply let the program fail at run time if anything goes wrong. This means if we use a bad interface implementation, our software will likely crash.
Make Contracts More Reliable
Willing to make the contract more reliable, you, as a C# developer, download JetBrains.Annotations NuGet package, and annotate the code snippet above.
This is definitely better. You mark your input and output parameters with the
ItemNotNull attributes — so the project won’t compile if someone tries to use
null s anywhere. Additionally, you document all possible exceptions that a search engine implementation might throw — so one can omit the global
try catch block when working with that interface. Now, one can call the
ISearchEngine.Search method safely!
This is the best way of resolving such issues with C#, but it isn’t a silver bullet. The person who implements the
ISearchEngine interface can easily ignore recommendations defined in the signature, or turn ReSharper off. So, what do we have to do to absolutely trust our code base?
Write Unit Tests For Interface Implementations
This is quite obvious, and prevents our software from unexpected errors in most cases, but sometimes requires doing a ton of work each time the API changes. That’s why we move forward and try to learn new tools.
Use F#, Functional Programming and Types
F# is a strongly typed, multi-paradigm programming language that encompasses functional, imperative, and object-oriented programming methods. F# runs everywhere C# does.
Considering our search engine implementation, the first benefit we get when using F# is null safety. This means a reference type can never implicitly be
null in F#. For example, a
string variable can never be null, if we need to store a
null value inside it, we use
Yeah, no more null reference exceptions. Hopefully, null safety is going to be introduced in C#8. But now, if we rewrite our C# interface declared above in F#, we’ll eliminate four of five issues from our list! The only issue left is related to exceptions that are not documented.
Functional programmers don’t like exceptions. Although one can write a pure function that uses
raise statements under the hood, exceptions violate referential transparency. Instead of exceptions, it’s better to use explicitly typed output and discriminated unions.
Assuming that, let’s refactor the C# interface declared above into F# types.
Here we describe all possible values that a function may produce, including errors, using a
SearchResponse discriminated union. Instead of using implicit exceptions and XML documentation, we use types, and our code becomes self-documenting. Instead of using an interface here, we use a functional type. The search engine becomes a function that takes a query string and returns a response asynchronously. “Implementing” and using such function is simple.
We can finally trust the search engine implementation. Our app will never fail with null reference exceptions, and compiler forces us to handle all errors that a function may return, using pattern matching over a discriminated union. If we don’t handle all cases, we’ll receive a warning each time we are attempting to build the project. Far better!
Yeah, sounds tricky at first glance. If you find yourself interested and would like to learn more, visit F# For Fun and Profit website. Additionally, it’s worth reading “Domain Modelling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#” book by Scott Wlaschin for more real-world code examples and best practices. Code snippets in this article are highlighted using carbon.now.sh online service.