This is on GitHub

One of the unique things about Nix is its extensible configuration system. Extensible configs allow you to override their fields in a way that will update all the other fields that depend on what you override. In Nix, this is particularly useful for package management. It allows you to override a package (for example; change its version), and all the packages that depend on that package will subsequently use your overridden version.

We can represent this in Haskell, since it’s a lazy language. This is based on chains of functions that each take the previous one’s output as an argument, typically called super . By overriding a field in super , you get to change that field for every function that comes after you.

data MyConfig = MyConfig { _a :: Int , _b :: Int } makeLenses '' M yConfig initial :: MyConfig initial = MyConfig { _a = 1 , _b = 2 } overrideB :: MyConfig -> MyConfig overrideB = b .~ 3 overrideA :: MyConfig -> MyConfig overrideA super = super & a .~ ( super ^. b ) final :: MyConfig final = ( overrideA . overrideB ) initial -- final = MyConfig { _a = 3, _b = 3 }

This forms a pretty convenient monoid called Endo . This monoid is just the monoid of endomorphism composition. That is, it just composes functions with the same argument and return types.

import Data.Monoid ( Monoid ( .. ), ( <> )) newtype Endo a = Endo { appEndo :: a -> a } instance Monoid ( Endo a ) where mempty = Endo id Endo f ` mappend ` Endo g = Endo ( f . g ) -------------------------------------------------------------------------------- overrideB :: Endo MyConfig overrideB = Endo ( b .~ 3 ) overrideA :: Endo MyConfig overrideA = Endo $ \ super -> super & a .~ ( super ^. b ) final :: MyConfig final = appEndo ( overrideA <> overrideB ) initial

So an extensible config is an Endo monoid that you can compose with mappend or (<>) . But this only causes updates to the following overrides. Previous overrides will still see old records, which makes the config incoherent. You need to give the final version of the record (typically called self ) to all overrides so they can use coherent records whenever possible. In a lazy language, this is easy; you can recursively give the final thunk to all overrides without having to evaluate those overrides first. An easy way to do this is to extend Endo to monadic endomorphisms, and use the Reader monad.

import Control.Monad (( <=< )) import Control.Monad.Reader import Data.Functor.Identity import Data.Function ( fix ) newtype EndoM m a = EndoM { appEndoM :: a -> m a } instance Monad m => Monoid ( EndoM m a ) where mempty = EndoM return EndoM f ` mappend ` EndoM g = EndoM ( f <=< g ) type Endo = EndoM Identity type Configurable a = EndoM ( Reader a ) a configure :: Configurable a -> a configure ( EndoM f ) = fix ( \ self -> runReaderT ( f self ) self ) -- | Lens convenience overriding :: Setter' ( EndoM m s ) s overriding = sets $ \ f ( EndoM g ) -> EndoM ( g . f ) -------------------------------------------------------------------------------- -- This one serves as a bootstrap, allowing the constructor to -- evaluate to WHNF. Whenever you don't have initial values for -- fields, you can set the bootstrap to `EndoM $ \(~MyConfig {..}) -> -- MyConfig {..}`, using lazy matching and `RecordWildCards` to -- boostrap the thunk with nonterminating fields. initial :: Configurable MyConfig initial = EndoM $ \ _ -> return MyConfig { _a = 1 , _b = 2 } overrideA :: Configurable MyConfig overrideA = EndoM $ \ super -> do self <- ask return $ super & a .~ ( self ^. b ) overrideB :: Configurable MyConfig overrideB = mempty & overriding . b .~ 3 final :: MyConfig final = configure ( overrideB <> overrideA <> initial ) -- MyConfig { _a = 3, _b = 3 }

Now, even though overrideA comes earlier than overrideB in the chain (remember, function composition, and therefore EndoM ’s monoid instance, reads right to left), a still gets set to the value that’s defined for b later in the chain.

There’s also no reason that self and super have to have the same type. You can convert super into self at the end of the chain to get whatever finalization you need (even at the type level).

import Control.MonadFix ( mfix ) type Configurable self super = EndoM ( Reader self ) super configure :: ( super -> self ) -> Configurable self super -> self configure f ( EndoM g ) = fix ( f . runReader ( mfix g ))

And finally, this can of course work for any MonadFix , meaning your configuration steps can be monadic.