Wednesday, March 2, 2022

Applicatives should usually implement Semigroup and Monoid

lift-monoid

The gist of this post is that any type constructor F that implements Applicative:

instance Applicative F where

… should usually also implement the following Semigroup and Monoid instances:

instance Semigroup a => Semigroup (F a) where
    (<>) = liftA2 (<>)

instance Monoid a => Monoid (F a) where
    mempty = pure mempty

… which one can also derive using the Data.Monoid.Ap type, which was created for this purpose:

deriving (Semigroup, Monoid) via (Ap F a)

Since each type constructor that implements Monad also implements Applicative, this recommendation also applies for all Monads, too.

Why are these instances useful?

The above instances come in handy in conjunction with utilities from Haskell’s standard library that work with Monoids.

For example, a common idiom I see when doing code review is something like this:

instance Monad M where


example :: M [B]
example = do
    let process :: A -> M [B]
        process a = do

            return bs

    let inputs :: [A]
        inputs =

    bss <- mapM process inputs

    return (concat bss)

… but if you implemented the suggested Semigroup and Monoid instances then you could replace this:

    bss <- mapM process inputs

    return (concat bss)

… with this:

    foldMap process inputs

These instances also come in handy when you need to supply an empty action or empty handler for some callback.

For example, the lsp package provides a sendRequest utility which has the following type:

sendRequest
    :: MonadLsp config f
    => SServerMethod m
    -> MessageParams m
    -> (Either ResponseError (ResponseResult m) -> f ())
    -- ^ This is the callback function
    -> f (LspId m)

I won’t go into too much detail about what the type means other than to point out that this function lets a language server send a request to the client and then execute a callback function when the client responds. The callback function you provide has type:

Either ResponseError (ResponseResult m) -> f ()

Sometimes you’re not interested in the client’s response, meaning that you want to supply an empty callback that does nothing. Well, if the type constructor f implements the suggested Monoid instance then the empty callback is: mempty.

mempty :: Either ResponseError (ResponseResult m) -> f ()

… and this works because of the following three Monoid instances that are automatically chained together by the compiler:

instance Monoid ()

-- The suggested Monoid instance that `f` would ideally provide
instance Monoid a => Monoid (f a)

instance Monoid b => Monoid (a -> b)

In fact, certain Applicative/Monad-related utilites become special cases of simpler Monoid-related utilities once you have this instance. For example:

  • You can sometimes replace traverse_ / mapM_ with the simpler foldMap utility

    Specifically, if you specialize the type of traverse_ / mapM_ to:

    traverse_ :: (Foldable t, Applicative f) => (a -> f ()) -> t a -> f ()
    mapM_     :: (Foldable t, Monad       f) => (a -> f ()) -> t a -> f ()

    … then foldMap behaves the same way when the Applicative f implements the suggested instances:

    foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
  • You can sometimes replace sequenceA_ / sequence_ with the simpler fold utility

    Specifically, if you specialize the type of sequenceA_ / sequence_ to:

    sequenceA_ :: (Foldable t, Applicative f) -> t (f ()) -> f ()
    sequence_  :: (Foldable t, Monad       f) -> t (f ()) -> f ()

    … then fold behaves the same way when the Applicative f implements the’ suggested instances:

    fold :: (Foldable t, Monoid m) => t m -> m
  • You can sometimes replace replicateM_ with mtimesDefault

    Specifically, if you specialize the type of replicateM_ to:

    replicateM_ :: Applicative f => Int -> f () -> f ()

    … then mtimesDefault behaves the same way when the Applicative f implements the suggested instances:

    mtimesDefault :: Monoid m => Int -> m -> m

And you also gain access to new functionality which doesn’t currently exist in Control.Monad. For example, the following specializations hold when f implements the suggested instances:

-- This specialization is similar to the original `foldMap` example
fold :: Applicative f => [f [b]] -> f [b]

-- You can combine two handlers into a single handler
(<>) :: Applicative f => (a -> f ()) -> (a -> f ()) -> (a -> f ())

-- a.k.a. `pass` in the `relude` package
mempty :: Applicative f => f ()

When should one not do this?

You sometimes don’t want to implement the suggested Semigroup and Monoid instances when other law-abiding instances are possible. For example, sometimes the Applicative type constructor permits a different Semigroup and Monoid instance.

The classic example is lists, where the Semigroup / Monoid instances behave like list concatenation. Also, most of the exceptions that fall in this category are list-like, in the sense that they use the Semigroup / Monoid instances to model some sort of element-agnostic concatenation.

I view these “non-lifted” Monoid instances as a missed opportunity, because these same type constructors will typically also implement the exact same behavior for their Alternative instance, too, like this:

instance Alternative SomeListLikeType where
    empty = mempty

    (<|>) = (<>)

… which means that you have two instances doing the exact same thing, when one of those instances could have potentially have been used to support different functionality. I view the Alternative instance as the more natural instance for element-agnostic concatenation since that is the only behavior the Alternative class signature permits. By process of elimination, the Monoid and Semigroup instances should in principle be reserved for the “lifted” implementation suggested by this post.

However, I also understand it would be far too disruptive at this point to change these list-like Semigroup and Monoid instances and expectations around them, so I think the pragmatic approach is to preserve the current Haskell ecosystem conventions, even if they strike me as less elegant.

Why not use Ap exclusively?

The most commonly cited objection to these instances is that you technically don’t need to add these lifted Semigroup and Monoid instances because you can access them “on the fly” by wrapping expressions in the Ap newtype before combining them.

For example, even if we didn’t have a Semigroup and Monoid instance, we could still write our original example using foldMap, albeit with more newtype-coercion boilerplate:

    fmap getAp (foldMap process (map Ap inputs))

… or perhaps using the newtype package on Hackage:

    ala Ap foldMap process inputs

This solution is not convincing to me for a few reasons:

  • It’s unergonomic in general

    There are some places where Ap works just fine (such as in conjunction with deriving via), but typically using Ap directly within term-level code is a solution worse than the original problem; the newtype wrapping and unwrapping boilerplate more than counteracts the ergonomic improvements from using the Semigroup / Monoid instances.

  • In my view, there’s no downside to adding Semigroup and Monoid instances

    … when only one law-abiding implementation of these instances is possible. See the caveat in the previous section.

  • This line of reasoning would eliminate many other useful instances

    For example, one might remove the Applicative instance for list since it’s not the only possible instance and you could in theory always use a newtype to select the desired instance.

Proof of laws

For completeness, I should also mention that the suggested Semigroup and Monoid instances are guaranteed to always be law-abiding instances. You can find the proof in Appendix B of my Equational reasoning at scale post.

Sunday, February 27, 2022

What is a monad morphism (in Haskell)?

monad-morphism

This post briefly explains what a monad morphism is in the context of Haskell, since I wanted a succinct explanation that I could refer to for other posts. In order to keep things short, I will assume that you are already comfortable with Haskell’s Monad class and do notation.

A monad morphism is:

  • … a natural transformation
  • … that obeys two laws

… so first I’ll explain what a natural transformation is followed by an explanation of the two monad morphism laws.

Natural transformations

A natural transformation in Haskell is a function whose type can be expressed in terms of the following type-level operator:

{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE RankNTypes    #-}

type m ~> n = forall a . m a -> n a

In other words, a natural transformation is a function that converts a value of type m a into a value of type n a such that it works for any possible type a.

Here are some examples of natural transformations in the wild whose types could be written in terms of the above (~>) type-level operator:

Control.Error.Util.hush
    :: Either e a -> Maybe a
    -- Either e ~> Maybe

Data.Maybe.listToMaybe
    :: [a] -> Maybe a
    -- [] ~> Maybe

UnliftIO.Exception.fromEither
    :: Exception e => Either e a -> IO a
    -- Exception e => Either e ~> IO

Control.Monad.State.UnificationExtras.liftReaderT
    :: Monad m => ReaderT e m a -> StateT e m a
    -- Monad m => ReaderT e m ~> StateT e m

Laws

Now that we’ve defined what a natural transformation is we can define what a monad morphism is.

A monad morphism is a special case of a natural transformation (which we will denote nat) that satisfies the following two laws, known as the monad morphism laws:

nat (return x) = return x

nat (do { x <- m; f x }) = do { x <- nat m; nat (f x) }

In other words, a monad morphism “distributes over” Monad operations, leaving them undisturbed, which is a useful guarantee.

Three of the above example natural transformations (hush, fromEither, and liftReaderT) are also monad morphisms, since they satisfy those two monad morphism laws. In contrast, listToMaybe is an example of a natural transformation that is not a monad morphism.

Example proof of laws

Here is an example of how you would prove the monad morphism laws for the hush function, which is defined like this:

hush :: Either e a -> Maybe a
hush (Left  _) = Nothing
hush (Right x) = Just x

We’ll begin by proving the first monad morphism law:

hush (return x)

-- Add a type annotation to note that `hush` only accepts an `Either` as input
= hush (return x :: Either e a)

-- return x :: Either e a = Right x
= hush (Right x)

-- Definition of `hush`
= Just x

-- return x :: Maybe a = Just x
= return x :: Maybe a

-- Drop the type annotation
= return x

… and here is how we prove the second monad morphism law:

hush (do { x <- m; f x })

-- Desugar `do` notation
hush (m >>= \x -> f x)

-- There are two possible cases: `m = Left l` or `m = Right r`

-- Case: `m = Left l`:
  = hush (Left l >>= \x -> f x)

  -- Definition of `(>>=)` for `Either e` `Monad`
  = hush (Left l)

  -- Definition of `hush`
  = Nothing

  -- Definition of `(>>=)` for `Maybe` `Monad`, but in reverse
  = Nothing >>= \x -> hush (f x)

  -- Definition of `hush`, but in reverse  
  = hush (Left l) >>= \x -> hush (f x)

  -- Resugar `do` notation
  = do { x <- hush (Left l); hush (f x) }

  -- `m = Left l` in this case
  = do { x <- hush m; hush (f x) }

-- Case: `m = Right r`
  = hush (Right r >>= \x -> f x)

  -- Definition of `(>>=)` for `Either e` `Monad`
  = hush (f r)

  -- Definition of `(>>=)` for `Maybe` `Monad`, but in reverse
  = Just r >>= \x -> hush (f x)

  -- Definition of `hush`, but in reverse
  = hush (Right r) >>= \x -> hush (f x)

  -- Resugar `do` notation
  = do { x <- hush (Right r); hush (f x) }

  -- `m = Right r` in this case
  = do { x <- hush m; hush (f x) }

Rationale behind the laws

The reason we choose those laws is because they are similar to the functor laws when viewed through the appropriate lens.

As a recap, the functor laws say that any implementation of Haskell’s Functor class must obey these two laws:

fmap (f . g) = fmap f . fmap g

fmap id = id

However, the category theory notion of a functor is more general than the Haskell notion of a functor. Specifically, the more general notion of a functor lets you replace (.) with any associative operator, meaning that the following law still holds:

(f . g) . h = f . (g . h)

… and also lets you replace id with any expression that is the identity of the corresponding (.) operator, meaning that the following two laws still hold:

f . id = f

id . f = f

Once we allow ourselves to introduce more exotic analogs to (.) and id then we can similarly introduce more exotic analogs to fmap that don’t fit into the mold of Haskell’s Functor class.

A monad morphism is one such exotic analog to fmap, where we:

  • … substitute (.) with the (>=>) operator from Control.Monad
  • … substitute id with return from the Monad class
  • … substitute fmap with (nat .) where nat is any monad morphism

This works out because the (>=>) operator is associative:

(f >=> g) >=> h = f >=> (g >=> h)

… and return is the identity of the (>=>) operator:

f >=> return = f

return >=> f = f

… so if we restate the functor laws with those substitutions, we get:

(nat .) (f >=> g) = (nat .) f >=> (nat .) g

(nat .) return = return

… and those are the monad morphism laws, just stated in a different way.

We can see the correspondence to the original monad morphism laws if we simplify things a bit. We’ll begin by simplifying the first equation to get back the original first monad morphism law:

(nat .) (f >=> g) = (nat .) f >=> (nat .) g

-- Reparenthesize things, for clarity:
nat . (f >=> g) = (nat . f) >=> (nat . g)

-- Definition of `(>=>)`:
--
-- f >=> g = \x -> f x >>= \y -> g y
nat . (\x -> f x >>= \y -> g y) = \x -> (nat . f) x >>= \y -> (nat . g) y

-- Apply both sides to `x`
(nat . (\x -> f x >>= \y -> g y)) x = (nat . f) x >>= \y -> (nat . g) y

-- Definition of `(.)`:
--
-- (f . g) x = f (g x)
nat ((\x -> f x >>= \y -> g y) x) = nat (f x) >>= \y -> nat (g y)

-- β-reduce
nat (f x >>= \y -> g y) = nat (f x) >>= \y -> nat (g y)

-- Replace `f x` with `m`
nat (m >>= \y -> g y) = nat m >>= \y -> nat (g y)

-- Resugar using `do` notation
nat (do { y <- m; g y }) = do { y <- nat m; nat (g y) }

-- Rename `y` to `x` and rename `g` to `f`, for consistency with original laws
nat (do { x <- m; f x }) = do { x <- nat m; nat (f x) }

Similarly, we can simplify the second equation to get back the original second monad morphism law:

(nat .) return = return

-- Remove unnecessary parentheses
nat . return = return

-- Apply both sides to `x`
(nat . return) x = return x

-- Definition of `(.)`:
--
-- (f . g) x = f (g x)
nat (return x) = return x

In other words, the monad morphism laws are functor laws in disguise.

Wednesday, January 26, 2022

Nixpkgs overlays are monoids

overlay-monoid

Nixpkgs supports overriding sets of packages using overlays and these overrides bear many similarities to object-oriented inheritance.

As a simple example, I can disable tests for the hello package in Nixpkgs using code that superficially resembles a subclass overriding a superclass method:

# ./example.nix

let
  overlay = self: super: {
    hello = super.hello.overrideAttrs (old: {
      doCheck = false;
    });
  };

  pkgs = import <nixpkgs> { overlays = [ overlay ]; };

in
  pkgs.hello

Here the super identifier plays a role similar to an object-oriented superclass: if you squint and view the package set preceding the override as the “superclass” then the above code defines the hello package for my “subclass” as being the same as the hello package from the “superclass” except without tests.

Also, just like in object-oriented programming, your “methods” (i.e. packages) can refer to other “methods” from within the same “class”, using self. For example, I can specify that I want the hello package to gratuitously run cowsay after the installation phase, like this:

# ./example.nix

let
  overlay = self: super: {
    hello = super.hello.overrideAttrs (old: {
      postInstall = "${self.cowsay}/bin/cowsay 'Installation complete'";
    });
  };

  pkgs = import <nixpkgs> { overlays = [ overlay ]; };

in
  pkgs.hello

… and if we build that we’ll see the output from cowsay in the build logs:

$ nix-build ./example.nix

 _______________________ 
< Installation complete >
 ----------------------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
@nix { "action": "setPhase", "phase": "fixupPhase" }
post-installation fixup
gzipping man pages under /nix/store/xid1pxd0bccq8592pbjrb5b9k4qv3zzq-hello-2.10/
strip is /nix/store/hm2qzyqh6bh872rwlpjl4kw7a1nk173d-clang-wrapper-11.1.0/bin/st
stripping (with command strip and flags -S) in /nix/store/xid1pxd0bccq8592pbjrb5
patching script interpreter paths in /nix/store/xid1pxd0bccq8592pbjrb5b9k4qv3zzq
/nix/store/xid1pxd0bccq8592pbjrb5b9k4qv3zzq-hello-2.10

You can also combine multiple overlays, which lets you spread out these sorts of overrides into separate logical units. For example, we can create one overlay that disable tests for the hello package and a second overlay that adds the cowsay post-install step, like this:

let
  overlay0 = self: super: {
    hello = super.hello.overrideAttrs (old: {
      doCheck = false;
    });
  };

  overlay1 = self: super: {
    hello = super.hello.overrideAttrs (old: {
      postInstall = "${self.cowsay}/bin/cowsay 'Installation complete'";
    });
  };

  pkgs = import <nixpkgs> { overlays = [ overlay0 overlay1 ]; };

in
  pkgs.hello

Under the hood, when you specify multiple overlays they are combined using the pkgs.lib.composeExtensions function (which will be relevant in just a second).

Monoids

Overlays are not only practical, but also theoretically cool, because overlays are monoids!

To see why, we have to first establish the three things that constitute a monoid:

  • Every monoid has a corresponding type, which we’ll denote as M

  • Every monoid has an associative binary operator, which we’ll denote as ×

    This operator has type: M -> M -> M (using Haskell notation). In other words it is a function of two inputs of type M and returns an output, also of type M.

  • Every monoid has an “identity” of that operator, which we’ll denote as 1

    This “identity” is a value of type M.

When we say that × is associative, we mean that for any three values (e.g. a, b, and c) of type M, the following equation is true:

(a × b) × c = a × (b × c)

In other words, the way in which we group multiplication does not change the result. In fact, associativity means that we can elide the parentheses and the meaning of the expression is still unambiguous:

a × b × c

When we say that 1 is the “identity” of ×, we mean that for any value (e.g. a) of type M, the following equations are true:

1 × a = a

a × 1 = a

Other monoids

Here we use × to denote the binary operator and 1 to denote the corresponding identity value because we want to reuse our intuition that multiplication is associative and the number 1 is the identity of multiplication. However, monoids are more general than numbers and multiplication; there might be other binary operators and identity elements that behave in the same way.

For example, Nix attribute sets are also monoids, if we replace × with // and replace 1 with { }:

# // is an associative binary operator
(a // b) // c = a // (b // c)

# … and { } is the identity of //
{ } // a = a

a // { } = a

Also, as the title suggests, overlays from Nixpkgs are yet another example of a monoid! We can illustrate this by defining the three components of the “overlay monoid”:

  • The type M (using Haskell notation) is:

    -- Nix derivations are not actually text, but this keeps the example simple
    type Derivation = Text
    
    type PackageSet = Map Text Derivation
    
    type M = PackageSet -> PackageSet -> PackageSet
    
    -- … which we can expand out to the following equivalent type if we prefer:
    type M = Map Text Derivation -> Map Text Derivation -> Map Text Derivation
  • The binary operator is pkgs.lib.composeExtensions (the same function I mentioned earlier which combines overlays)

    The composeExtensions function is defined like this in Nixpkgs (refactored to improve the clarity of upcoming examples):

    composeExtensions = f: g:
        self: super:
            f self super // g self (super // f self super);

    Using object-oriented terminology, you can read that as saying: “to combine the methods defined in both f and g, first extend g’s superclass with all of f’s methods, then combine both sets of methods, giving precedence to methods defined in g”.

    The equivalent Haskell type and implementation would be:

    composeExtensions :: M -> M -> M
    composeExtensions f g =
        \self super ->
            Map.union (g self (Map.union (f self super) super)) (f self super)

    Carefully note that in the above Haskell code:

    • f has type M
    • g has type M
    • Everything past the = sign (i.e. \self super -> …) has type M

    It might help to expand out the Haskell type since the M type synonym is hiding a lot of complexity:

    -- This larger type is equivalent to the previous type:
    composeExtensions
      :: (PackageSet -> PackageSet -> PackageSet)  -- ← M
      -> (PackageSet -> PackageSet -> PackageSet)  -- ← M
      -> (PackageSet -> PackageSet -> PackageSet)  -- ← M
    
    -- We can expand things out further to this even larger equivalent type:
    composeExtensions
      :: (Map Text Derivation -> Map Text Derivation -> Map Text Derivation)
      -> (Map Text Derivation -> Map Text Derivation -> Map Text Derivation)
      -> (Map Text Derivation -> Map Text Derivation -> Map Text Derivation)
  • The identity value is (self: super: { }) (the empty overlay)

    The equivalent Haskell type and implementation would be:

    identityExtension :: M
    identityExtension =
        \self super -> Map.empty
    
    -- The following types are also equivalent:
    identityExtension
        :: PackageSet -> PackageSet -> PackageSet
    
    identityExtension
        :: Map Text Derivation -> Map Text Derivation -> Map Text Derivation

All we’re missing to establish that this is a bona-fide monoid is to prove that composeExtensions is an associative function and that identityExtension is its identity. We’ll do so using equational reasoning, but in Nix rather than Haskell:

It’s easier to prove the identity laws, so we’ll start with those:

composeExtensions (self: super: { }) f

# Replace composeExtensions with its definition
=   self: super:
            (self: super: { }) self super
        //  f self (super // (self: super: { }) self super)

# (self: super: e) self super = e
=   self: super: { } // f self (super // { })

# { } // a = a
=   self: super: f self (super // { })

# a // { } = a
=   self: super: f self super

# η-reduction
=   f

The other identity law is equally simple to prove:

composeExtensions f (self: super: { })

# Replace composeExtensions with its definition
=   self: super: f self super // (self: super: { }) self (super // f self super)

# β-reduction
=   self: super: f self super // { }

# a // { } = a
=   self: super: f self super

# η-reduction
=   f

Now, let’s prove that composeExtensions is associative, which means proving that:

composeExtensions (composeExtensions f g) h =
  composeExtensions f (composeExtensions g h)

We’ll begin from the left-hand side of the equation and keep rewriting until we reach the right-hand side of the equation:

composeExtensions (composeExtensions f g) h

# Replace the inner composeExtensions with its definition
=   composeExtensions
        (self: super: f self super // g self (super // f self super))
        h

# Replace the outer composeExtensions with its definition
=   self: super:
            (self: super: f self super // g self (super // f self super))
                self
                super
        //  h self
                (   super
                //  (self: super:
                        f self super // g self (super // f self super)
                    )
                        self
                        super
                )

# (self: super: e) self super = e
=   self: super:
            f self super
        //  g self (super // f self super)
        //  h self (super // f self super // g self (super // f self super))

# Factor out the three occurrences of `super // f self super` using a lambda
=   self: super:
            f self super
        //  (super: g self super // h self (super // g self super))
                (super // f self super)

# e = (self: e) self
=   self: super:
            f self super
        //  (self: super: g self super // h self (super // g self super)) self
                (super // f self super)

# Definition of composeExtensions, but in reverse
=   composeExtensions
        f
        (self: super: g self super // h self (super // g self super))

# Definition of composeExtensions, but in reverse
=   composeExtensions f (composeExtensions g h)

Generalization

In fact, the above implementation and the proofs of associativity/identity still work if you:

  • replace // with any associative operator
  • replace { } with the corresponding identity of the associative operator

In other words, we can generalize our implementation by replace Nix attribute sets with any arbitrary monoid! We can codify this neat property with the following Haskell type and Monoid instance:

-- This generalizes our previous overlay type
newtype Overlay m = Overlay { runOverlay :: m -> m -> m }

-- The `(<>)` operator generalizes our previous `composeExtensions` function
instance Semigroup m => Semigroup (Overlay m) where
    Overlay f <> Overlay g =
        Overlay (\self super -> f self super <> g self (super <> f self super))

-- `mempty` generalizes our previous `identityExtension`
instance Monoid m => Monoid (Overlay m) where
    mempty = Overlay (\self super -> mempty)

We can prove that this code generalizes the previous code by showing how we can implement the old overlay type and functions in terms of the new ones. The old code becomes a special case of the new code when we replace the type parameter m with Dual (Map Text Derivation):

{-# LANGUAGE TypeApplications #-}

import Data.Coerce (coerce)
import Data.Map (Map)
import Data.Monoid (Dual(..))

import Data.Text (Text)

newtype Overlay m = Overlay { runOverlay :: m -> m -> m }

instance Semigroup m => Semigroup (Overlay m) where
    Overlay f <> Overlay g =
        Overlay (\self super -> f self super <> g self (super <> f self super))

instance Monoid m => Monoid (Overlay m) where
    mempty = Overlay (\self super -> mempty)

type Derivation = Text

type PackageSet = Map Text Derivation

type OldOverlay = PackageSet -> PackageSet -> PackageSet

type NewOverlay = Overlay (Dual PackageSet)

composeExtensions :: OldOverlay -> OldOverlay -> OldOverlay
composeExtensions =
    coerce @(NewOverlay -> NewOverlay -> NewOverlay)
           @(OldOverlay -> OldOverlay -> OldOverlay)
           (<>)

identityExtension :: OldOverlay
identityExtension = coerce @NewOverlay @OldOverlay mempty

Conclusion

I’m not sure if there would be any applications of this Overlay type within the Haskell ecosystem, so I didn’t bother packaging up the latter Overlay type and Monoid instance. However, if you think this might be useful in your code then just let me know and I can create a tiny package for this purpose.

Hopefully this post illustrates how Nixpkgs overlays are actually a pretty principled system for overrides. Also, you might be able to formulate object-oriented inheritance in the same way, too, due to the similarities between Nix’s overlay system and object-oriented programming. The main difference is that Nix’s overlay system is weakly-typed, so to really give this a proper OOP treatment, you’d probably have to:

  • replace Nix’s attribute sets with more strongly-typed records
  • replace the Monoid with a more strongly-typed Category

… but I haven’t taken the time to fully flesh out that idea. Also, I’m not sure if it could be implemented in Haskell, since Haskell does not support anonymous records, but it might work just fine in PureScript.