DEV Community

loading...
Cover image for Kentico Xperience Design Patterns: Modeling Missing Data - The Maybe Monad

Kentico Xperience Design Patterns: Modeling Missing Data - The Maybe Monad

Sean G. Wright
Chief Solutions Architect @WiredViews, founding partner @craftbrewingbiz. @KenticoXP MVP. love to learn / teach web dev & software engineering, collecting vinyl records, mowing my lawn, craft ๐Ÿบ
ใƒป14 min read

In two previous posts (Modeling Missing Data with Nullable Reference Types and Modeling Missing Data with The Null Object Pattern) we compared approaches for representing missing data in our code and considered the implications that modeling had for how we could handle the missing data.

As we saw through some example implementations, both options have pros and cons ๐Ÿค”.

Let's quickly review how those approaches work and where they fall short. Then we will dive into my favorite approach for modeling missing data in Kentico Xperience applications - the Maybe monad ๐Ÿคฉ!

๐Ÿ“š What Will We Learn?

๐Ÿƒ A Refresher - The Problems with Our Options

โ” Nullable Reference Types

While I do think we should enable nullable reference types in our Kentico Xperience applications ๐Ÿ‘๐Ÿพ, trying to model our data with this language feature alone can lead to confusion ๐Ÿ˜ต.

Let's look at the example HomeViewModel below:

public class HomeViewModel
{
    public string Title { get; set; }
    public ImageViewModel? Image { get; set; }
}

public class ImageViewModel
{
    public string Path { get; set; }
    public string AltText { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

We can see that HomeViewModel.Image is nullable, so the C# compiler (and our IDE) can alert us to places in our code where we don't first check to see if its value is null before accessing its Path or AltText properties...

If we are unfamiliar with this application, we might look at the code above and ask "But, why is it nullable?" ๐Ÿคท๐Ÿฝโ€โ™€๏ธ

The LINQ method IEnumerable<T>.FirstOrDefault() can also return null, but does a null value returned here, have the same meaning as a null value on one of our View Model's properties? LINQ was not designed with our data model in mind, but the HomeViewModel class is specific to our content model - it feels like these two uses of null should have very different meanings.

I find that null is great for fixing the "All reference types are a union of null and those types" problem, but I don't find it to be the most descriptive technique to represent the data specific to our applications.

As we start to annotate our code with nullable reference types, we'll also discover that null is kinda unpleasant ๐Ÿ˜ฃ to work with.

If we try to work with a reference type variable or property that is nullable, we have to constantly add checks in our code to tell the C# compiler that within a block of code, we know the value isn't null:

HomeViewModel vm = ...

// vm.Image might be null

if (vm.Image is null)
{
    // vm.Image is definitely null

    return;
}

// vm.Image is definitely not null

string altText = vm.Image;
Enter fullscreen mode Exit fullscreen mode

These checks add complexity to our apps for the sake of protecting ourselves from the dreaded NullReferenceException ๐Ÿ˜ฑ.

It would be great if we could work with our 'empty' values in the same way as our 'not empty' ones and not have to have all these guards! It would also be nice if we could represent their 'emptiness' in a way that felt closer to our data model - not using a low level language feature.

๐Ÿงฉ Null Object Pattern

The Null Object Pattern lets us treat an 'empty' value of a type as a 'special case' of its type.

From the previous post in this series, we came up with the following example:

public record ImageViewModel(string Path, string AltText)
{
    public static ImageViewModel NullImage { get; } = 
        new ImageViewModel("", "");

    public bool IsEmpy => this == NullImage;
    public bool IsNotEmpty => this != NullImage;
}
Enter fullscreen mode Exit fullscreen mode

We've move the representation of 'empty' or 'missing data' into the type itself, which means all of our APIs, properties, and variables can avoid adding the null reference type annotation when using this type:

public class HomeViewModel
{
    public string Title { get; set; }

    // notice no '?' on ImageViewModel
    public ImageViewModel Image { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Instead we'll use NullImage property:

var home = contextRetriever.Retrieve().Page;

var cta = retriever.Retrieve<CallToAction>(
    q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
    .FirstOrDefault();

var viewModel = new HomeViewModel
{
    Title = home.Fields.Title,
    Image = cta.HasImage
        ? new ImageViewModel(cta.ImagePath, cta.AltText)
        : ImageViewModel.NullImage // takes the place of null
};
Enter fullscreen mode Exit fullscreen mode

Now, we don't have to guard against viewModel.Image being null to interact with it ๐Ÿ‘๐Ÿป, and if we want to know if it is our 'Null Object' (empty) we can check the value of viewModel.Image.IsEmpty.

Despite these benefits, we've unfortunately swung in the complete opposite direction of null reference types and brought the 'empty' value logic into our type, making it more complex.

Even worse, we need to duplicate this logic for every type that represents data that might be missing in our application.

๐Ÿงฉ What is a Monad?

We would really like something outside of our ImageViewModel class that lets us represent missing data like null reference types, but in an unambiguous way. This approach also should allow us to work with those 'empty' data scenarios without doing gymnastics ๐Ÿคธ๐Ÿฟโ€โ™‚๏ธ to check if the data is there or not.

The answer to our requirements is the Maybe monad, a container for our data that lets us operate on it as though it exists (no conditionals) while expressing 'emptiness' (without putting it into our model).

So what is a Monad ๐Ÿ˜จ?

A Monad is a concept from functional programming that sounds complex, and can take a little effort to reason about if you've never seen it before, but don't get scared, we'll take it slow ๐Ÿค—.

I really like this description of a Monad:

a monad is a design pattern that allows structuring programs generically while automating away boilerplate code needed by the program logic.

This sounds nice, right?

  • Developers love design patterns ๐Ÿคฉ!
  • C# developers know the benefits of using generics ๐Ÿง 
  • And we probably feel like half of our working lives involves automating away boilerplate ๐Ÿค–

Let's get a little more formal with this minimalist definition:

A Monad is a container (Container<Type>) of something that defines two functions:

Return: a function that takes a value of type Type and gives us a Container<Type> where Container is our monad.

Bind: a function that takes a Container<Type> and a function from Type to Container<OtherType> and returns a Container<OtherType>.

So, a Monad is a 'container' type, which, in C#, means it is generic on some type T and it has 2 methods, Bind and Return:

public class Monad<T>
{
    public T Value { get; }

    public Monad<T>(T value)
    {
        this.Value = value;
    }

    public static Monad<T> Return<T>(T value)
    {
        return new Monad<T>(value);
    }

    public static Monad<R> Bind<T, R>(
        Monad<T> source, 
        Func<T, Monad<R> operation)
    {
        return operation(source.Value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Return<T> takes a normal T value and puts it in our Monad container type. It's like a constructor:

Monad<int> fiveMonad = Monad<int>.Return(5);

Console.Write(fiveMonad.Value); // 5
Enter fullscreen mode Exit fullscreen mode

Bind<T, R> takes 2 parameters, a Monad<T> and a function that accepts a T value and returns a Monad<R> (R and T can be the same type). It's a way of unwrapping an existing Monad to convert it to a Monad of a different type:

public Monad<string> ConvertToString(int number)
{
    return Monad<string>.Return(number.ToString());
}

Monad<string> fiveStringMonad = Monad<int>.Bind(
    fiveMonad, ConvertToString);

Console.Write(numberAsString.Value); // "5"
Enter fullscreen mode Exit fullscreen mode

The simplicity of Monads is what makes them so useful ๐Ÿ“. Once you start to work with them, you'll start to see them everywhere - both in existing code and all the ways you can use them in your applications.

๐Ÿฆธโ€โ™€๏ธ Friendly Neighborhood C# Monads

If we look ๐Ÿ‘€ at some C# code we are used to writing, we can recognize 2 of the types as Monads!

Our favorite C# representation of asynchronous work, Task<T> is a Monad - its Task.FromResult() method is the same as Monad.Return().

Also, the always present enumerable type, IEnumerable<T> is a Monad, with IEnumerable<T>.SelectMany() being the same as Monad.Bind().

Specific monads can have a lot more features, and methods/functions to make them more useful (think of all the extension methods that IEnumerable<T> has to make LINQ as awesome as it is ๐Ÿ’ช๐Ÿผ!)


๐Ÿ” The Maybe Monad!

Now, let's get to the Maybe Monad and see how it helps model missing data! We can think of the Maybe Monad as a magic box ๐Ÿง™๐Ÿพโ€โ™‚๏ธ...

Small cardboard box

Photo by Christopher Bill

This magic box might or might not contain something.

It accepts commands from us and can change its contents into whatever we want. If there is something in the box, it changes its contents to what we command. If the box is empty, it ignores the command and nothing happens.

We could also command the box to fill itself, with something specific, if it happens to be empty. If it's not empty, it will ignore the command.

The most interesting aspect of the box that it does all this without us needing to open it up to check if there is actually anything inside.

However, once we are finally done changing or populating the contents of the box, we can open it up and look inside...

Cat in a cardboard box

Picture by DNK.PHOTO

The benefit of keeping the box closed is that we can give it unlimited instructions without any checks on its contents (unlike accessing data with null reference types).

Also, if we instead wanted to change items from one thing to another, like an apple ๐ŸŽ into a rocket ๐Ÿš€, without the box, that apple would need to be magical (normal apples can't change into rockets ๐Ÿ˜ฟ). With this box, the items don't need to have special qualities (unlike the Null Object Pattern classes we create).

With this understanding of what Monads are and how the Maybe Monad gives us the power of the magic box we described above, let's look at how we'd use it with our Kentico Xperience sample code.

๐Ÿ‘ฉโ€๐Ÿ’ป Using Maybe with Kentico Xperience

Maybe + ImageViewModel

My favorite implementation of the Maybe Monad in C# comes from the CSharpFunctionalExtensions library. It's well designed and includes lots of extension methods to make working with the Maybe type easy ๐Ÿ˜Ž.

Let's look at the ImageViewModel example again:

public class HomeViewModel
{
    public string Title { get; set; }

    public Maybe<ImageViewModel> Image { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

We now type the HomeViewModel.Image property as Maybe<ImageViewModel> which means it might or might not have a value.

Moving to the example Controller action method, we create a new Maybe<ImageViewModel> based on the existence of the CallToAction page:

var home = contextRetriever.Retrieve().Page;

Maybe<CallToAction> cta = retriever.Retrieve<CallToAction>(
    q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
    .TryFirst();

HomeViewModel homeModel = new() 
{
    Title = home.Fields.Title,
    Image = cta.Bind(c => c.HasImage
        ? new ImageViewModel(c.ImagePath, c.AltText)
        : Maybe<ImageViewModel>.None);
};
Enter fullscreen mode Exit fullscreen mode

You might also notice the TryFirst() call ๐Ÿคจ. It's and extension method in CSharpFunctionalExtensions and a nice way of integrating Maybe into Kentico Xperience's APIs to help avoid the null checks we might have if we instead used .FirstOrDefault(). It tries to get the first item out of a collection - if there is one, it populates a Maybe<T> with that value, if the collection is empty, it creates a Maybe<T> that is empty.

The .Bind() call on Maybe<CallToAction> cta is saying 'if we have a Call To Action and that Call To Action has an Image, create a Maybe<ImageViewModel> with some values, otherwise create an empty one'.

When creating the HomeViewModel.Image, we can see there's an implicit conversion from T to Maybe<T>, so we don't need to create a new Maybe<ImageViewModel> ... C# does it for us.

When the CallToAction doesn't have an image, we assign Maybe<ImageViewModel>.None, which is our representation of 'missing' data. It's a Maybe<ImageViewModel> that is empty.

So far this doesn't look too different from our null reference type implementation, but the real value ๐Ÿ’ฐ comes with the way we work with our Maybe values ๐Ÿ˜ฎ.

Let's say we wanted to separate out our ImageViewModel.Path and ImageViewModel.AltText into separate variables. With Maybe we don't have to do any checks to see if HomeViewModel.Image is null:

HomeViewModel homeModel = // ...

Maybe<string> path = homeModel.Image.Map(i => i.AltText);
Maybe<string> altText = homeModel.Image.Map(i => i.Path);
Enter fullscreen mode Exit fullscreen mode

We magically changed our ImageViewModel properties to string values without taking them out of the Maybe box.

If we wanted to create an HTML image element by combining the path and altText variables, how could we do that while keeping them in their Maybe boxes?

Maybe<string> htmlImage = path
    .Bind(p => altText
        .Map(a => $"<img src='{p}' alt='{a}'>"));
Enter fullscreen mode Exit fullscreen mode

No conditional, no guards, no checks. We can stay in the happy world of Maybe as long as we want, blissfully ๐Ÿ˜Š ignorant of whether or not there are values to work with.

The Maybe<T> container always exists, and exposes many methods to do transformations on the data inside (like .Map() and .Bind()). If there's no data, the transformations (magic box commands) never happen - but we always end up with another Maybe<T>, ready to perform more transformations.

If we ever want to get the value out of the Maybe and supply a fallback value if its empty, we can use the UnWrap() method:

string imagePath = image
    .Map(i => i.Path)
    .UnWrap("/placeholder.jpg");
Enter fullscreen mode Exit fullscreen mode

UnWrap() is a lot like Kentico Xperience's ValidationHelper type, with calls like ValidationHelper.GetString(someVariable, "ourDefaultValue");.

Maybe Some Best Practices

We can mix and match these extension methods however we want, creating a complex pipeline of operations for our data.

We only open the Maybe box when we need to turn it into a traditional C# value - in Kentico Xperience applications this will often be in a Razor View where we have to convert/render the value to HTML or JSON.

'What's in the box' clip from the movie Seven


Because we don't need to know about the existence of our value until we are ready to render, we should attempt to keep the value in the Maybe Monad for as long as possible. Similar to Task<T>, it's common for a Maybe<T> to bubble up and down the layers of our application code, since we defer unwrapping until the last possible moment.

The CSharpFunctionalExtensions library does support the pattern below (and it's ok to start with when exploring how Maybe works), but I advise against it โš  when seriously integrating Maybe in an application:

Maybe<string> name = // comes from somewhere else

string greeting = "";

if (name.HasValue)
{
    greeting = $"Hello, {name.Value}";
}
else
{
    greeting = "I don't know your name";
}

return greeting;
Enter fullscreen mode Exit fullscreen mode

The Maybe Monad is meant to reduce the number of conditional checks we need to make, however getting the value out of the container can sometimes be a little un-ergonomic. We will have the best developer experiences with them when we go all-in, using expressions instead of statements, and thinking about declarative data transformations instead of procedural data manipulations.

Try this instead:

Maybe<string> name = // comes from somewhere else

string greeting = name
    .Map(n => $"Hello, {n}")
    .UnWrap("I don't know your name");
Enter fullscreen mode Exit fullscreen mode

Here's some real, production Kentico Xperience Page Builder Section code where I use Maybe and some of its extensions to both represent missing data and avoid conditional statements by staying in the Maybe Monad box as long as possible:

TreeNode page = vm.Page;
SectionProperties props = vm.Properties;

Maybe<ImageViewModel> imageModel = vm.Page
    .HeroImage() // could be empty
    .Bind(attachment =>
    {
        var url = retriever.Retrieve(attachment);

        if (url is null) 
        {
            return Maybe<ImageContent>.None;
        }

        return new ImageContent(
            attachment.AttachmentGUID, 
            url.RelativePath, 
            vm.Page.PageTitle().Unwrap(""), 
            a.AttachmentImageWidth, 
            a.AttachmentImageHeight);
    })
    .Map(content => new ImageViewModel(content, props.SizeConstraint));

return View(new SectionViewModel(imageModel));
Enter fullscreen mode Exit fullscreen mode

Page.HeroImage() returns Maybe<DocumentAttachment>. My data access and transformation code never needs to check for missing data - it's a set of instructions I give to the magic ๐Ÿง๐Ÿฝโ€โ™€๏ธ Maybe Monad box.

If there is no Hero Image, the code to fetch the Attachment data will never be executed and at the end of my method, my SectionViewModel will have an empty Maybe<ImageViewModel> ๐Ÿ‘๐Ÿป.

Maybe Some Rendering

Even in our Razor View, we can continue to be ignorant about the status of our Maybe<ImageViewModel> by using the extension method Maybe<T>.Execute() which is only 'executed' when the Maybe<T> has a value:

@model SectionViewModel

<p> ... </p>

<!-- Maybe<T>.Execute() the Image method defined below -->
@{ Model.Image.Execute(Image); }

<p> ... </p>

<!-- We create a helper method to render the image HTML -->
@{
    void Image(ImageViewModel image)
    {
        <img src="@image.Path" alt="@image.AltText" 
             width="@image.Width" ...>
    }
}
Enter fullscreen mode Exit fullscreen mode

The first approach is great if we want to embrace ๐Ÿค— the Monad!

Here's another way we can render the Maybe value:

@model SectionViewModel

<p> ... </p>

@if (Model.Image.HasValue)
{
    var image = Model.Image.Value;

    <img src="@image.Path" alt="@image.AltText" 
         width="@image.Width" ...>
}

<p> ... </p>
Enter fullscreen mode Exit fullscreen mode

This one is better if we want to keep our conditional HTML inline with the rest of our markup and have traditional looking Razor code.

Another technique is to use the Partial Tag Helper and pass the Maybe<T> type to it, handling the conditional rendering and unwrapping logic outside of our primary View:

@model SectionViewModel

<p> ... </p>

<partial name="_Image" model="Model.Image" />

<p> ... </p>
Enter fullscreen mode Exit fullscreen mode

All three are perfectly valid choices and each one has pros and cons, so pick the one that fits your use-case!

๐Ÿคฏ Conclusion!!

We finally maybe'd... I mean made it ๐Ÿ˜…!

Reviewing our previous two attempts at modeling missing data (null reference types, and the Null Object Pattern), we saw how they can be helpful but both either fail to model accurately or are painful to work with.

Null reference types impose a null check every time we want to access some potentially null data, which breaks the logic and flow our of code ๐Ÿ˜‘.

The Null Object Pattern alleviates us from having to make those checks, but requires enhancing, potentially many, models with properties that express their 'is this model's data missing?' nature. This puts a burden on developers and clutters up our model types ๐Ÿ˜–.

Monads might seem like a scary ๐Ÿ‘ป functional programming concept at first, but they're actually pretty simple!

The Maybe Monad combines both techniques from our previous approaches into a single container type that lets us operate on our data without having to know whether or not it's missing ๐Ÿ˜„.

By putting all the state that keeps track of whether or not a value exists, in the Maybe<T>, we are able to keep our model types simple. Also, by being able to transform and access data independent of whether or not its there, we code that is clearer and reads more like a series of instructions without numerous checks.

The Maybe type is most effective in our apps when we leverage it to the fullest, letting it flow through our data access, business logic, and presentation code. Once we get to the Razor View we have several options available for rendering that data.

Of the three options I've covered in these posts, which do you use to model missing data in your Kentico Xperience applications?

Do you have any other approaches you recommend?

Let us know in the comments below ๐Ÿ˜€.

...

As always, thanks for reading ๐Ÿ™!

References


Photo by Jordan Madrid on Unsplash

We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

Or my Kentico Xperience blog series, like:

Discussion (0)