Default values in Haskell data types

There's already good answers here, so this answer is only meant as a supplement to the fine answers from chi and Willem Van Onsem.

In mainstream object-oriented languages like Java and C#, it's not that a default object is uninitialised; rather, a default object is normally initialised with default values for their types, and it just happens that for reference types, the default is a null reference.

Haskell doesn't have null references, so records can't be initialised with nulls. The most direct translation of objects would be records where every single constituent element is a Maybe. That's not particularly useful, however, but it highlights how hard it is to protect invariants in OOP.

The Builder pattern doesn't solve that problem at all. Any Builder has to start with an initial Builder object, and that object is going to have to have default values as well.

For more details, and lots of examples, I wrote an article series about this. The article series specifically focuses on the Test Data Builder pattern, but you should be able to see how it generalises to the Fluent Builder pattern in general.


Is there any mechanism in Haskell to do the same thing in record types?

What you can do is hide the constructor, and provide a function as constructor instead.

Say for instance we have a list we want to update, together with a revision number, then we can define it as:

data RevisionList a = RevisionList { theList :: [a],
                                     revision :: Int }
                          deriving Show

Now we can define a function that initializes the BuildList with an initial list:

revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }

and by hiding the constructor in the module export, we thus hide the possibility to initialize it with another revision than revision 0. So the module could look like:

module Foo(RevisionList(), revisionList)

data RevisionList a = RevisionList { theList :: [a],
                                     revision :: Int }

revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }

something like the builder pattern from OOP?

We can for instance use a State monad for that. For instance:

module Foo(RevisionList(), revisionList,
           increvision, RevisionListBuilder, prefixList)

import Control.Monad.State.Lazy

type RevisionListBuilder a = State (RevisionList a)

increvision :: RevisionListBuilder a ()
increvision = do
    rl <- get
    put (rl { revision = 1 + revision rl})

prefixList :: a -> RevisionListBuilder a ()
prefixList x = do
    rl <- get
    put (rl { theList = x : theList rl })
    increvision

So we get the RevisionList thus far, perform updates, put the new result back, and increment the revision number.

So now another module can import our Foo, and use the builder like:

some_building :: RevisionListBuilder Int ()
some_building = do
    prefixList 4
    prefixList 1

and now we can "make" a RevisionList at revision 2 with as final list [1,4,2,5] with:

import Control.Monad.State.Lazy(execState)

some_rev_list :: RevisionList Int
some_rev_list = execState some_building (revisionList [2,5])

So it would look approximately like:

Foo.hs:

module Foo(RevisionList(), revisionList,
           increvision, RevisionListBuilder, prefixList)

data RevisionList a = RevisionList { theList :: [a],
                                     revision :: Int }
                          deriving Show
type RevisionListBuilder a = State (RevisionList a)

revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }

increvision :: RevisionListBuilder a ()
increvision = do
    rl <- get
    put (rl { revision = 1 + revision rl})

prefixList :: a -> RevisionListBuilder a ()
prefixList x = do
    rl <- get
    put (rl { theList = x : theList rl })
    increvision

Bar.hs:

import Foo
import Control.Monad.State.Lazy(execState)

some_building :: RevisionListBuilder Int ()
some_building = do
    prefixList 4
    prefixList 1

some_rev_list :: RevisionList Int
some_rev_list = execState some_building (revisionList [2,5])

So now we have constructed a some_rev_list with the "building" of some_building:

Foo Bar> some_rev_list 
RevisionList {theList = [1,4,2,5], revision = 2}

A common idiom is to define a default value.

data A = A { foo :: Int , bar :: String }

defaultA :: A
defaultA = A{foo = 0, bar = ""}

This can be then (purely) "updated" later on with real values.

doSomething :: Bool -> A
doSomething True  = defaultA{foo = 32}
doSomething False = defaultA{bar = "hello!"}

Pseudocode example:

data Options = O{ textColor :: Bool, textSize :: Int, ... }

defaultOptions :: Options
defaultOptions = O{...}

doStuff :: Options -> IO ()
doStuff opt = ...

main :: IO ()
main = do
     ...
     -- B&W, but use default text size
     doStuff defaultOptions{ color = False }

If there are no sensible default values, you can wrap the field values in Maybe.

If you feel adventurous, you can even use a more advanced approach to statically separate "intermediate" options values, which can lack a few fields, from "finalized" ones, which must have all the fields. (I'd not recommend this to Haskell beginners, though.)

Tags:

Types

Haskell