Do people actually use data-default (Data.Default) Default type class in production?

Please excuse this beginner question.

I’m wondering if this is a pattern people actually use. I’m having situations where I have huge record fields where I occasionally only want to set a few and leave the rest at their zero values.

Can you share examples of how people use the Default typeclass?

https://hackage.haskell.org/package/data-default-0.5.3/docs/Data-Default.html
https://hackage.haskell.org/package/data-default-extra-0.1.0/docs/Data-Default.html

1 Like

The main example I know of is for configurations in the xmonad package, here is the implementation:

https://hackage.haskell.org/package/xmonad-0.17.0/docs/src/XMonad.Config.html#line-258

An example of how to use it can be found here:

For example:

main :: IO ()
main = xmonad $ def
    { modMask = mod4Mask  -- Rebind Mod to the Super key
    }

This starts xmonad with the default config with only the modifier key changed.

2 Likes

Are those few fields always the same? If so, maybe a smart constructor myConst :: Int -> Sting -> Foo would do?

What I don’t like about Default: I expect me messing with a datatype will — in most cases — break compilation, which forces me to fix small type errors and in the process reviewing my decision and checking if everything is correct; with def more things slip through the fingers of the compiler and it is not always obvious (especially if you automagically dervie stuff).

2 Likes

Or just a non-typeclass value defaultConfig.

8 Likes

There is an old post somwhere explaining the issues with the Default
typeclass, but I can’t find it now!

Some points I remember:

  • You have a typeclass, but you never abstract over it. doStuff :: Default a => a -> x is not useful.

  • You can not add required parameters, or introduce side effects (Default UUID should probably generate a new UUID)

  • If you are writing a library, you force downstream users to add a
    dependency to the package and an import, or you need to re-export
    def.

  • The class is lawless. There is often not a clear answer to what a
    Default should be. What is Default Int? Often 0, sometimes
    -1. Monoid solves this by having mappend that needs to work with
    mempty.

  • Seeing def all over the place makes code hard to
    understand. Seeing the actual value you want is a lot clearer.

Default is very prevalent in Rust, it’s more convenient there as it
is built into the standard library and you can derive instances out of
the box. But most of the problems remain, and I’m not a fan. The
official rust documentation disagrees with itself on how to pick an
implementation and when to use it.

Usually there is a better name than default that you can
use. empty may be a better name in your case?

Another thing to consider in your case, is an empty struct actually
valid? Often they are not and it may be more correct to use some sort
of builder to enforce that all required values are present before the
resulting value is constructed.

Perhaps you want randomized values with QuickCheck’s Arbitrary for testing? I
would only recommend this if you intend to run tests a lot of times (e.g. with QuickCheck),
in my experience randomizing for just a few iterations of a test leads
to flakiness.

10 Likes

What I usually do is that I have a defaultMyRecord function. Less type inference, more explicit to read, overall better development experience.

8 Likes

All the time. I use it especially in state-based applications which are built using a combination of Lens and MonadState.

For example, on a project I’m working on now, I have a set of controllers (i.e. physical devices with buttons) and I need to keep track of the estimated state of each controller. I want to have a total map from controller name to controller state, implemented as a normal partial map with empty assumed to mean default. A simplified snippet to demonstrate what I mean:

data Button = U | D | L | R deriving ...
data ButtonState = Up | Down
instance Default ButtonState where def = Up

data ControllerState = ControllerState {buttons :: Map Button ButtonState, ...} deriving (Generic, ...)
instance Default ControllerState where def = ControllerState def ...

data Controller = A | B | C deriving ...

data SystemState = SystemState {controllers :: Map Controller ControllerState, ...} deriving (Generic, ...)

type AppMonad m = (MonadState SystemState m, ...)

pushButton :: AppMonad m => Controller -> Button -> m ()
pushButton controller button = #controllers . at controller . non def . #buttons . at button . non def .= Down

The above snippet will set the given button on the given controller to “down”, using “def” to interpret the meaning of an empty map entry. at and non come from Lens, and the #buttons syntax is OverloadedLabels plus generic-lens-labels.

1 Like

What I usually do is that I have a defaultMyRecord function.

A variant of this approach I’ve tried is to make configurable functions accept a MyOptionsRecord -> MyOptionsRecord instead of MyOptionsRecord, so that they themselves apply the modifier to the default record.

For the sake of clarity, one can even define defaults :: a -> a as a synonym for id.

No. I don’t find this typeclass valuable and I think it’s unnecessary indirection in most codebases. I remember when I first encountered this typeclass years ago I got hung up on where the default values where coming from (being used to the far more common pattern of using something like defaultOptions instead)—then I eventually found out it was from the typeclass instance.

I think that fundamentally my biggest issue with this is that it takes away from code readability. I don’t want every default-value-producing function to be called def. I’d much rather have defaultOptions and defaultUserConnectionState and defaultTransaction because I can immediately see which default I am producing when I use these. Using def to produce default values for many different types in a single codebase is the opposite approach that serves to obscure.

6 Likes

It’s definitely controversial! But I can point to a situation where I’ve used it, and I’m convinced it’s justified. In HMock (HMock: A flexible mock framework for testing effectful code.), which is about producing mock implementations of interfaces, when you tell HMock to expect an action to be used in the code under test, you can either specify what its result should be or just let it return the default value.

A lot of the time, the action is of type m (), so there’s no question about what the default should be. But sometimes you just don’t care about a result even when it’s an integer or some other type. I’ve relied on the Default type class to determine what should be returned in both cases. (If there is no Default instance, then the action defaults to returning bottom.) I think that was the right choice.

  • You could argue that I should require that you explicitly specify a result value for each expected action. That would be tedious when the result type is often ().
  • You could argue that I should have always used undefined to fail if you even look at the result. But not caring what the result is for this particular test doesn’t mean you want the test to fail because the system under test looks at it!
  • You could argue that I should have special-cased (), and fail (in the sense of either of the previous two points, at type-checking or at runtime) if you leave out the result otherwise. This is probably the strongest alternative, and corresponds to the precise-and-complete approach to testing, as opposed to the sloppy-but-easy approach that I favor. I prefer the sloppy approach specifically because I want to encourage people to write one test for each property they are testing, rather than having to verify cover everything in one big test that covers all the possibilities. (This is very different from my approach to type-safety in production code, where I find it better to verify everything.)

So, yeah, you can argue the choice, but I think relying on Default to produce values you don’t care about is the best option here.

3 Likes

As mentioned previously, xmonad uses it a fair bit. I’m still opposed to it, though, and Prevent grabKey from accidentally grabbing all unbound keys · xmonad/xmonad@383ffb7 · GitHub is a good example of why. It’s a lovely way for someone to shoot themself in the foot.

2 Likes

Why not just add a type annotation where you think it’s appropriate?

I prefer

def :: FooConfig

To

defaultFooConfig

The former gives you strictly more information, and the verbosity is optional.

1 Like

Three reasons:

  1. That’s harder to read. defaultFooConfig as a single term is definitely easier to comprehend than a term + a type signature.

  2. Sometimes default isn’t the right word, or there are several types of defaults, and the Default typeclass approach boxes you into only one. For example, it might make more sense to call it an initialFooConfig because there’s also a resetFooConfig, where the latter is statically defined but slightly different from the former (“a FooConfig that has been reset” vs. “the initial state of a FooConfig”).

  3. Related to the above, even if the word “default” is appropriate, but I’m using the Default typeclass, my initial config will be def and my reset variant is resetFooConfig, which is weird and inconsistent.

4 Likes

I’m surprised to see a Haskeller prefer what essentially amounts to Hungarian notation over a terse notation with an actual type.

If you put the type of your term in its name, this is just a colloquial hint that could easily become untrue after a refactor. I always prefer, ceteris paribus, an actual type annotation which the compiler will check.

I can’t really identify with the claim that xFoo is harder to read than x :: Foo. Why do you think the former is easier? Is it really a matter of “a single term” (i.e. something with the same semantic connotation but one fewer lexeme) being easier to comprehend?

If your type has a bunch of standard constructors which you need to intentionally pick from, Default is not appropriate in the first place, so I don’t think that example is really germane.

1 Like

I don’t believe @christian’s objection is to specifying a type! Mine certainly isn’t. defaultFooConfig :: FooConfig is perfectly acceptable to me.

On the other hand, if you have a Default instance, use def without specifying a type, and then the surrounding code changes the inferred type of def then it can cause issues that are very hard to track down.

So yes, if one (bizarrely) changes the type of defaultFooConfig it can cause a type error. But a def which is mistakenly unannotated can cause a run time error!

4 Likes

I agree with this concern - my only question is why xFoo is supposed to be better as a value than x :: Foo

Resilience to refactor: slight advantage for x :: Foo because it’s actually checked by the compiler and not arbitrary text.

Clarity: slight advantage for x :: Foo because you are 100% certain of the type, whereas with xFoo you are hoping that the most obvious type is the correct one

Readability: I would say wash although it sounds like some people prefer xFoo because it has fewer lexemes as far as the language’s formal grammar is concerned (even though it has exactly the same number of lexemes under the informal grammar we’re using to name it)

I think this situation will be improved somewhat once the visible forall proposal is implemented. Then you can write a type signature:

def :: forall a -> a

With that signature the type application is no longer optional and you don’t have to write the usual @ sign, for example:

def Foo

Edit: although I wonder how it interacts with type classes, would you be able to write:

class forall a -> Default a where
  def :: a

Or do you have to write a helper function:

class Default a where
  def' :: a

def :: forall a -> Default a => a
def _ = def'

Yes, this is a rather interesting design question! I assume it has been considered but I don’t know what the answer is.

For me, generally because there are a few possible such choices, xFoo1, xFoo2, xFooGreen, xFooBlue. There is not a priviliged one that deserves to be called x at the exclusion of the others. Even if I’ve only defined one so far, that’s not enough evidence to privilige it. It may well be painting myself into a corner when I subsequently discover I want another one.

One other issue that comes up is that it’s implemented via typeclasses, which puts limits on how you can use it. The xmonad issue I referenced can’t be fixed by providing an appropriate def, because it would be an overlapping instance with Default b => a -> b.