F# nullable preview

After poking the F# team (thanks Vlæd Zá !), I was able to test the nullable feature support RFC 1060.

Why is this a big deal for F# ? After all, we already have different tools to express this. For me, it’s all about interop and alignment with the .net platform: C# and BCL. BCL has been annotated and it’s incredibly useful in C#. It’s also really great to help modeling and ensure your code is doing great.

Productivity

Personnaly, this is something I was eargely waiting for. It’s something that has been promised for years and unfortunately never completed. F# was lagging behind platform improvements to the point it was ridiculous to invest on - even decided F# was not worth the game anymore (with few exceptions for algorithmic stuffs). Having used C# and nullable extensively, it’s a real productivity improvement and a real safety net for developers.

Productivity comes from:

  • data models are cleaner and safer (null is now explicit)
  • functions are explicit about null values
  • return values are explicit and null as specified
  • compiler nullability mismatches are caught at compile time (it’s even better with <TreatWarningsAsErrors> on 😋)

Note this does not remove null references, you can probably easily inject null references on non-nullable values with little effort (deserialization, Reflection, …). This only reduces cognitive load and focus design on what’s matter.

Now it’s baked in F# - or will be baked as this is a preview, it’s probably not completely ready but great step forward ! Let’s try to compare implementation and usage against C#.

Declaration

// C#
string? name = "toto";

// F#
let name: string | null = "toto"

Despite behind nearly the same, there is a strong difference in philosophy imho.

In F#, nullability is supported through the type (string | null) whereas in C# nullability is supported through the binding site (field, variable, argument). Probably this is why you can define a nullable type in F#:

type NullableString = string | null
let name: NullableString = "toto"

This is something you can’t do in C#. Nullability is always defined at the binding site (sorry, I do not have a better word to describe this) - not when declaring the type itself. My view on C# can probably better expressed as (if that was valid C#):

string (?name) = "toto";

F# is type oriented - this does make sense nullability is baked at type declaration level and not at binding level. I think this sucks a little bit despite this can be seen as a value as well. Looks like reason is F# designers are trying to bring syntax closer to RFC 1092 - but hell yeah, I feel it’s weird to mix storage and type definition (I’ve always the feeling nullable is an access problem, not a type declaration one). Moreover, this is definitively confusing with distriminated union declarations. I would have probably gone for anonymous nullable type only (with <type>? syntax - and less verbose as well). Time for eyes to adjust probably, but you have my point of view.

Test drive

I have a bunch of projects where NRT would be interesting:

  • PresqueYaml: a Yaml deserializer, with lot of Reflection usage
  • Terrabuild: a build tool for monorepo. It does use F# Compiler.Service and Reflection for scripts and FsLexYacc

But before, I’ve setup a test project to play with this feature. Check the Makefile.

PresqueYaml

First build: a lot of errors but it’s ok as I’ve not checked null for Reflection operation. This can be easily fixed. It’s a nice recall operations can lead to null and this must be handled. Fixed using nonNull to assert and remove nullability:

let private readMethod (ty: Type) = ty.GetMethod("Read") |> nonNull

Also I’ve assumed reference type can be nullable, again this can be changed easily:

[<Sealed>]
type ListConverter<'T>() =
    inherit YamlConverter<List<'T> | null>()

Note there are strange error reporting. Here error is reported on converterType line 3 but definitively shall be on line 6:

override _.CreateConverter (typeToConvert: Type, options:YamlSerializerOptions) =
    let converterType = typedefof<YamlNodeConverter<_>>
    converterType
        .MakeGenericType([| typeToConvert.GetGenericArguments().[0] |])
        .GetConstructor([| |])
        .Invoke([| |])
    :?> YamlConverter

This can be rewritten as:

    override _.CreateConverter (typeToConvert: Type, options:YamlSerializerOptions) =
        let converterType = typedefof<YamlNodeConverter<_>>
        let ctor =
            converterType
                .MakeGenericType([| typeToConvert.GetGenericArguments().[0] |])
                .GetConstructor([| |])
                |> nonNull
        ctor.Invoke([| |]) :?> YamlConverter

This is probably explained by this remark in the RFC.

All in all, pretty easy to enable NRT on this project. Note I choose the ugly path (i.e. use nonNull) and probably this ought be handled better than throwing NRE.

Changes Diff PR

Terrabuild

Terrabuild does use Reflection to bind parameters to script arguments - otherwise it’s a full fledge F# project and I do not expect nullable errors to pop up but Reflection ones.

Obviously, some obj have to be converted to objnull but nothing dramatic.

An error is annoying tho, there is an exception type in Terrabuild defined like this:

type TerrabuildException(msg, ?innerException: Exception) =
    inherit Exception(msg, innerException |> Option.defaultValue null)

    static member Raise(msg, ?innerException) =
        TerrabuildException(msg, ?innerException=innerException) |> raise

For which compilation ends with:

Terrabuild.Common/Errors.fs(6,66): warning FS3261: Nullness warning: The type 'Exception' does not support 'null'.

Error is not really obvious but here is the reason:
innerException is of type Exception option - hence defaultValue null is still an Exception option - not an Exception as expected for innerException so the warning.

I fixed it by removing optional argument (moving from option to nullable):

type YamlParserException(msg:string, innerEx: Exception | null) =
    inherit Exception(msg, innerEx)

    static member Raise(msg, ?innerEx: Exception) =
        let innerException: Exception | null = 
            match innerEx with
            | None -> null
            | Some ex -> ex
        YamlParserException(msg, innerException)
        |> raise

FsLexYacc does not seem ready for nullable. Lots of errors there. Something to be contributed to probably.

Nullability Info Context

I’ve not completely migrated PresqueYaml so not able to tell about this (PresqueYaml does support nullable context and is able to enforce it force C# define types). I will try to complete the migration and report my finding here for F# reference types.

nonNull / if / match

nonNull is pretty brutal and throws and NRE if null. That’s pretty quick and dirty and probably enough for some situation. Not really useful in practice (as the consequence are same) - but better from a type system point of view. This also can act as a marker for unsafe null removal.

Anyway, the correct way is to correctly handle null case obviously. For this, you can use if but F# is not C#. C# decided to go with the analyzer route and remove null on else branch. This is not perfect but works well in practice - despite I won’t trust it with mutable instances probably. I feel this is nuked by var default nullability, strange decision. F# decided against that - binding does always carry the type information. I more confident with F# philosophy and feels more robust (nothing more than guts feeling).

But there is another way to remove nullability: use match. Beware of order and check for null first (otherwise you will get error FS0026: This rule will never be matched)

let s: string | null = "toto"
let s =
    match s with
    | null -> "null"
    | s -> s
// s is now non nullable string

C# has a ?. operator but F# has not. Probably not a problem considering F# pipelining capabilities. Null-coalescing would be welcome but probably can be solved with a custom function.

The end

I’ve just played a little bit with this new F# feature - I’ve barely scratched the surface. All in all, it’s great and will enhance dramatically interop with C# and BCL. Regarding the last error I had, I clearly think days of option types are counted (and that’s great, there are too many ways to express null state). This is why I think | null is not great and declaration site would have been better and easier to read (and more in line with C# practice). But ok, this brings F# in line with C# and BCL which is great. Again, eyes will adjust, take my words with a pinch of salt.

For the future, I’d rather go with a big transparent unification of Nullable<>, Option<> and | null with implicit null support (let say <type>?). I do not care about struct and reference difference when matching - I only care when designing types. I’m probably just tired of the schism between structs and references as this ought be handled gracefully by compilers. Time will tell.

But hell yeah, congrats to F# team for bringing this long awaited feature !

Update on full-build successor

It’s been a while I’ve been discussing about monorepo.
Few things have changed: I’ve started to work on the successor of full-build.

Why successor?

Well full-build was probably a step forward for source management but it’s really a step backward in terms of compatibility: modifying projects or dev workflows is bad:

  • Devs always forget to update those files
  • Most of the time, this is different workflows on local and CI

In order to build a monorepo efficiently, it’s important to take into account how devs are working:

  • They use most of the time an IDE
  • They use standard tools (make, Terraform, …)
  • They do not want to bother with stuff that interfer with their workflows

So OK, let’s not change those habits and just:

  • Use standard tools to build artifacts
  • Never modify projects files
  • Just provide tooling to build current branch and check if everything is ok

Terrabuild is then the successor of full-build. It’s still being baked.
Only few components are available at the Magnus Opera GitHub Organization.

It will allow building a monorepo using standard tools without any modifications to source. It will also allow to isolate builds and easily build with same toolchains as on CI. Terrabuild will use extensions to deal with various languages or tools and will support build caching for fast builds both local or on CI.

Stay tuned !

About monorepo

It’s been something like a year and half I’ve been CTO at Tessan. When I joined, I’ve found the same problems that plague engineering teams:

  • lack of delivery practices
  • ship branches instead of versions (using kind of gitflow - that was soooo scary)
  • no testing but manual testing
  • no metrics in production
  • no cadence to deliver feature
  • no way to enable a feature in production once mature
  • manual deployments

As most company, Tessan is using a multi-repositories strategy. Like a vast majority of companies, this is just the situation that has been reached without much thoughts: start with a project, start a second, discover builds are complicated - split into 2nd repository,… rince again That’s where most companies are. No strategy, no thinking about what can be done to improve things or improve delivery.

I must confess, we are still using multiple-repositories. But things have been largely improved to gear towards a mono-repository.

What has been implemented is a gitops strategy:

  • repositories have dedicated build and generates their own artifacts: Docker images or zip archive (we have part of our infrastructure that’s running on-premise in the field). Artifacts are archived and tracked using a git tag or branch name
  • everything is deployed using Terraform with strict versionning. Targets are Kubernetes and on-premise infrastructure
  • all deployments happens using GitHub actions within protected environments hence we are using the super expensive GitHub Enterprise just to only use protection rules (this badly hurts)
  • we have more deployment pieces since splitting the big server-side monolith was a requirement to move faster across teams (yes, we went for micro-services using fbus)

Basically, this means we have more repositories than before 🤪. But we are in good shape to switch to mono-repository now. Everything is running quite smooth. So why move away from that model?

Well, feature development is a pain in the neck. Most of our development implies modifying several applications from front-end to back-end while changing database models from time to time. This is difficult for most devs to grasp:

  • isolate from main branch (especially when several repositories are impacted) the time feature stabilizes
  • understand impacts for testing
  • understand impacts for a release to ensure smooth communication with support team
  • do not miss something when deploying to test environment (we have micro-services again)
  • get the correct merge period to lower impacts

From an operational point of view, there are also several things hard to track:

  • reviews are complicated: usually this spans several repositories and it’s a hell to understand what’s going on
  • tests are underestimated (due to incremental impacts)
  • communication is impaired has several repositories have to be tracked

And from a dev perspective, it’s rather not good:

  • no motivation for changes/refactoring across repositories
  • lack of understanding how things keep working despite partially released (nullables, feature flags…)
  • no opportunity to learn by reading more code

For at least one thing, I’m a strong advocate for mono-repository: atomic feature implementation.

But if you think going mono-repository is easy, you are totally wrong. Single app per repository (aka multiple-repositories) is easy to do: setup sources, build and generate artifact on changes. Done.

When using mono-repository, you will hit the wall for sure: time to build the applications and noise:

  • Most of the time, there is no need to rebuild everything. We only need to rebuild and release what has changed. Shipping the whole platform is a non-sense.
  • Noise is a clear problem has looking at history is not so funny. Hopefully, Meta has release a tool to understand what’s going on Sapling. I’ve not tested it, but maybe this can help inthe future. To be investigated.

So what to do ? Well, your mono-repo must have tools to ensure it’s fast, optimize the build and provide auditing features:

  • identify what has changed (a new commit, a new branch)
  • build only what has changed - and think about changes in libraries up to deployment
  • delivery what has changed - generate a release note for changes

A mono-repository requires much more work to setup than multi-repositories. But benefits are tremendous.

I learned about mono-repository (or at least unified view of mult-repositories) at Criteo: that was the MOAB (Mother Of All Builds). When I left, I decided to create full-build to help D-Edge move faster engineering side. full-build is not much maintained (publicly speaking) but it lives under various names today (no more public). I’ve considered doing a public v2 as I’m not really satisfied with current state of affair.

Anyway, there are several tools on the market:

  • Bazel (Google) / Buckbuild (Meta): probably the top offer to consider but requires dedicated teams - does not really fit the startup/mid-company. Lack reuse of existing project metadata
  • Turborepo: javascript only
  • NX Build: nice plugin support but javascript builds are so messy. Also, does not provide proper isolation, this leaks everywhere. It works ok with lots of sweats

But as I’m saying, I’m not really satisfied. What I’m looking for:

  • be explicit with projects declaration: no magic
  • use most of metadata from projects (npm, .net projects, maven…) to get dependencies
  • leverage eco-systems instead of relying on plugins
  • ensure strong projects isolation - all paths must be relative to project, not workspace
  • support for explicit tasks

All in all, I think I will go for full-build v2 😃 I just need a tool that is no brainer and definitively open-source.

Restart

So here I am… restarting this blog again (must be the 4th or 5th I guess). Hope it will be better than past years…

Anyway, I’m planning to talk a little bit about what I will do in the future as I will manage IoT projects starting next week. I will try to share my experience discovering this world, implementing and managing sensors in the wild. It will be Azure related and as I do not know much about IoT on Azure, this will be for sure super interesting - at least for me ;-)

On personal projects, I will restart full-build and complete .net core support. I’m planning to implement following features:

  • .net core project style only
  • Paket removal
  • NuGet as build artifacts

Just asking myself if I should restart this project using Golang or continue with F#… But a full rewrite could be worthful as several data structures and graph algorithms are required for this kind of tool.

For 2020, I also plan to complete implementation of a new 3d engine for Amiga and hopefully release a demo with this engine. I started this project 2 years ago with a friend but I stopped working on this due to lack of motivation and time.

Let’s see how this will turn ;-)