λ Fun With Functions

Maybe Versus Nullable

Posted on November 21, 2017

Select All The Things

Recently I was faced with the task of updating our product to support multiple selection, so that for example a user can move multiple elements together with one mouse drag. As the codebase is written in TypeScript it meant taking this field:

selectedThing: ThingTarget | null;

and changing it to this:

selectedThings: Array<ThingTarget>;

On the surface this isn’t a huge change, but the code operating on these values has to change quite dramatically because of their different concrete structures. Taking the display of the indicators that show something is selected in the designer as an example:

if (selectedThing == null) {
  return [buildSelectedControl(selectedThing)];
} else {
  return [];
}

Now becomes:

return selectedThings.map(buildSelectedControl);

Make All The Changes

These are the simplest examples but fundamentally the logic always needed changing along these lines. As this felt a little unsatisfactory to me I pondered how this would’ve panned out with Haskell (or PureScript/Scala/etc). So we would’ve started with code that looked like:

selectedThing :: Maybe ThingTarget

That would’ve turned into:

selectedThings :: [ThingTarget]

However, in either of these cases the logic is the same and looks like:

fmap buildSelectedControl selectedThings
-- The type of fmap being:
-- fmap :: Functor f => (a -> b) -> f a -> f b

Similarly to check if something has been selected we’d use:

elem thing selectedThings
-- The type of elem being:
-- elem :: (Foldable t, Eq a) => a -> t a -> Bool

When people talk of abstraction it’s often in terms of things close to concrete use cases like a SelectionManager (let’s ignore that nonsense name) class which then doesn’t end up being very re-usable. However with abstractions like Foldable or Functor we have much less specific but just as if not more strict ones which tend more towards general purpose use cases. But also because the language lacks as many special case features (like null and undefined are in JavaScript, even if we don’t think of them traditionally as such) there are fewer hitches to the user of this kind of abstraction.

One Step Further

This extends beyond the functions that can be used to the functions that can be built too:

selectedControls :: (Functor f) = (Editor -> f Thing) -> Editor -> f Control
selectedControls getSelected editor = fmap buildSelectedControl $ getSelected editor

In this case the function is as general as fmap allows, if the accessor function has a type of Editor -> Maybe Thing, then the above function returns a Maybe Control. If no type was supplied, this is what GHC would determined the type of selectedControls would be.

GHCi> data Thing = Thing
GHCi> data Control = Control
GHCi> data Editor = Editor
GHCi> :{
Prelude| buildSelectedControl :: Thing -> Control
Prelude| buildSelectedControl _ = Control
Prelude| :}
GHCi> selectedControls getSelected editor = fmap buildSelectedControl $ getSelected editor
GHCi> :t selectedControls
selectedControls :: Functor f => (t -> f Thing) -> t -> f Control

It turns out this is more flexible that the version further above, as there’s no need (or even way) to constrain the function to the Editor type the compiler makes it polymorphic to any value t.

So What?

I posit that nullable values interfere with code reuse as they’re a disjoint union of the value type and null (undefined as well), but none of the common abstractions can be applied to a disjoint union. Simple types and simple abstractions lead to code reuse and less destructive refactors and if used, more flexible implementations.