Design question: encoding 'default' data at runtime

Hey everyone! I’m looking for a way to encode the concept of ‘default’ in datatypes, which can be resolved at runtime (as opposed to e.g. the Default typeclass, which is static).

Here’s a contrived example. I want to describe a desktop theme:

data Theme = Theme { backgroundColor :: Color
                   , edgeColor       :: Color
                   , transparency    :: Double
                   }

There’s a default theme:

defaultTheme :: Theme
defaultTheme = ...

Now I want to modify this theme with a different background color. Easy:

myTheme :: Theme
myTheme = defaultTheme {backgroundColor = ...}

That’s all well and good if the default theme is static. What if the default theme could be dynamically set? I would like to describe myTheme by showing what do I want to keep from the default theme (known at runtime), and what I want to override. Can I encode this at the type-level?

Here’s my idea:

-- | Marks that data can be inherited from the runtime default
data Inherit a = Default
               | Override a

-- | Marks that the data is fully resolved and ready to be acted upon
data Resolved a = Resolved { resolved :: a }

data Theme m = Theme { backgroundColor :: m Color
                     , edgeColor       :: m Color
                     , transparency    :: m Double
                     }

myTheme :: Theme Inherit
myTheme = Theme { backgroundColor = Override "black"
                -- Use the runtime default values for the following fields
                , edgeColor       = Default
                , transparency    = Default
                }

-- | Resolve theme at runtime
resolve :: Theme Resolved -- ^ Runtime default theme
        -> Theme Inherit  -- ^ My modified theme
        -> Theme Resolved -- ^ The combined

In practice, I’m going to be looking at multiple data types like Theme which can go from a Inherit to a Resolved, so I would build a typeclass that formalizes this relationship:

class Resolvable a where
    resolve :: a Resolved -> a Inherit -> a Resolved

This feels a bit clunky. Is there a better way to do something like this?

1 Like

I think one straightforward approach would be to make myTheme a function:

myTheme :: Theme -> Theme
myTheme dflt = dflt { backgroundColor = "black" }

and get rid of the resolve function altogether. And maybe make some type alias or even a newtype, like type InheritedTheme = Theme -> Theme. That comes at the cost though of not being able to inspect the contents of myTheme until it has been “resolved” by giving it the default theme. Nor could you equate these “inherited themes” or order them. (You can still edit the theme via function composition).

1 Like

Thanks for your reply. In this case you lose the type-level check of whether themes are still up-in-the-air or ‘final’.

You could imagine a DSL where you build a theme and then at the end, apply it to your desktop environment. In this sense, you would only want to apply a Theme Resolved. It would be nice to encode this at the type-level where

apply :: Theme Resolved -> IO ()

exists but not

apply:: Theme Inherit -> IO ()

What do you mean with «final»? myTheme :: Theme -> Theme needs to be applied to Theme to get something that we can pass to apply (if you are not using undefined in the definition, of course).

Theme -> Theme is actually your Resolved -> Inherit -> Resolved, or am I missing something?

Indeed this approach works! It’s totally usable. I’m just wondering if it’s also possible to track this at the type-level as an added bonus.

You could imagine a large program where ‘resolving’ the theme (or applying functions to the theme to modify it, as ntwilson suggested) happens at multiple points in separate code paths.

I think you’re saying you want to check at runtime whether a theme is resolved or not? You can easily tell at compile time the difference between a resolved theme (Theme) and a not-yet-resolved theme (Theme -> Theme). One way to get that runtime check is to define some sort of ADT:

data ResolvableTheme 
  = Resolved Theme
  | NotYetResolved (Theme -> Theme)

and then you can mix and match resolved and not-yet-resolved themes together and get that runtime check.

Is this getting at what you’re trying to do?

3 Likes

You can easily tell at compile time the difference between a resolved theme (Theme) and a not-yet-resolved theme (Theme -> Theme).

Exactly! This is what I was looking for. Much simpler. Thanks :slight_smile:

2 Likes