Small changes to XMonad like changing keyboard shortcuts require little Haskell knowledge. However, bigger customizations, especially adapting other people’s configurations, are easier when you understand Haskell types and records.

The so-called “record notation” is a convenient and concise notation to create, read, write and update a special kind of Haskell value, called a record, that is a tightly-related bundle of data. A record combines related values into a single “thing” like a row in an Excel spreedsheet or like a struct in C, C++, Java or C# programming languages. The main record that you will find in every XMonad configuration file is called XConfig which bundles all settings that you can set up in XMonad.

Actually, XConfig is not a value but a type. Our goal is to present you the difference between the two, types and values, as well as record notation. This will allow you to understand the type XConfig on its documentation page, understand its source code, as well as to read and modify XConfig record values for your XMonad configuration.

Briefly, the following Haskell source code introduces the XConfig type to define which settings are available and configurable in XMonad.

data XConfig l = XConfig
  { normalBorderColor  :: String
  , focusedBorderColor :: String
  , terminal           :: String
  -- ...
  }

The following Haskell code uses an existing XConfig record value called def and modifies it slightly:

myTerminal :: String
myTerminal = "gnome-terminal"

myConfig = def {
  { terminal = myTerminal
  }

The notation in this code with the curly braces {...} is called record syntax and constructs a (new) concrete value to configure concrete settings You will find code like this in other people’s configuration code and write code like this in your own.

Type

Understanding the difference between types and values is important to understand Haskell code and XMonad configuration files. If you have programmed in languages like Java, C++ or Python, you were already exposed to these concepts but may have used the words “class” instead of “type” and “object” instead of “value”.

Briefly, a type is a collection of related values. Intuitively, you put items, which you think are related, into a bag and give that bag a name. Then the items are called values and the name of that bag is called the type. For example, three widespread types, that intelligent people put together, are the Booleans, Integers and Strings, which are available in every programming language. In Haskell these types are concisely named Bool, Int and String:
Generic Haskell type as a bag of values Haskell type Bool as a bag of values True and False Haskell type Int as a bag of integer values

Why is the notion of types useful?

Types make Haskell a great programming language for building reliable software. Among other things, types act as a fast and lightweight verification tool. With types you establish what values certain parts of your program can work with. Then a tool called “type-checker” can automatically either verify that you work with acceptable values or warn you when try to work with inappropiate ones. For example, XMonad’s XConfig type establishes that its terminal settings has to be a String value. It contains the name of your favorite terminal app. If by accident you assign it a number as in

myConfig = def
  { terminal = 2 -- error, terminal is a String, but is given a number
  }

XMonad’s type checker will complain and tell you that you made an error, even before you started XMonad. So on a high level, types ensure that different program parts fit together nicely like pieces in a puzzle. In Haskell this notion is strongly enforced and is one of the reasons why many Haskell programmers say: “Once it compiles, it works correctly.”

However, type checkers aren’t perfect. In this terminal example you could still mistype the name of your terminal app.

myConfig = def
  { terminal = "gnome-tarminal"
  }

XMonad’s type checker would accept your setting, but once XMonad was running it would simply not open any terminal window, because there probably is no “gnome-tarminal”.

Thus, types as a verification tool is helpful but not perfect. In that sense, even though a type checker can’t tell you when your program is definitely correct, on most occasions it tells you when your program is definitely wrong.

Create a new type for a record

The authors of XMonad introduced the XConfig type as the name for the collection of all possible XMonad configurations. This type is introduced with the data keyword using the following notation:

data <name of new type> = <name of new value constructor>
  { <name of setting 1> :: <type of setting 1>
  , <name of setting 2> :: <type of setting 2>
    ...
  }

Since the XConfig type is complicated, we will explain this notation with a contrived example of a Person type:

-- Person.hs file
data Person = PersonValueConstructor { name :: String, height :: Int, isAdult :: Bool }
-- or
data Person = PersonValueConstructor
  { name :: String
  , height :: Int
  , isAdult :: Bool
  }

This data keyword introduces a new type called Person, whose values will be records containing a name, a height and information about whether this person is an adult. The words name, height and isAdult are called fields and are slots for information values that will be part of a concrete Person value. In that sense records are built like Lego bricks: from smaller, existing pieces.

Fields are annotated with :: <type for slot> to define what values are allowed in each slot. For example

name :: String

tells you that the name field only accepts and contains a string like "Jane". You can read this double colon :: as “has type” and the type after that must already exist.

Finally, this PersonValueConstructor as its name suggest will be an indicator that your are creating a Person value. In Haskell Types and value constructors always begin with an uppercase letter. Thus, when you see Person in Haskell code, you will know that it refers to the type, and when you see PersonValueConstructor you will know that you are creating a concrete value.

Create a record value

Having introduced a Person type there are two styles to create a concrete Person value:

-- Person.hs file continued
myConcretePerson1 :: Person -- optional
myConcretePerson1 = PersonValueConstructor "John" 180 True -- first style

myConcretePerson2 = PersonValueConstructor -- second style
  { name = "Jane"
  , height = 134,
  , isAdult = False
  }

You create a Person value by using its PersonValueConstructor followed by concrete values to fill the required Person fields. Normally, the second way is easier to read and recommended. This myConcretePerson2 is a variable/name for the concrete Person value you create on the right-hand side of =. You can optionally annotate a variable like myConcretePerson1 with a type by using :: Person. XMonad’s type checker then will check that the value you assign to this myConcretePerson1 variable is indeed a Person value. Roughly, everything that is not a type or value constructor, for example a variable name, begins with a lowercase letter.

In Haskell it is possible and recommended to give a type and its value constructor an identical name in order avoid having to invent another name, because naming is hard.

--- BetterPerson.hs file
data Person = Person
  { name :: String
  , height :: Int
  , isAdult :: Bool
  }

Here, the left occurence of Person is the type and the right occurence is its value constructor. With practice you won’t be confused about whether you mean the type or the value, because they are used in separated places.

-- BetterPerson.hs file continued
myConcretePerson :: Person -- Person type
myConcretePerson = Person  -- Person value constructor
  { name = "Jane"
  , height = 134,
  , isAdult = False
  }

In fact XConfig also uses the same name for the type and its value constructor. Furthermore, just as we defined myConcretePerson a concrete Person value, the authors of XMonad defined a concrete, built-in XConfig value called def, which represents a full configuration.

Modify a record value

Because an XConfig value needs a lot of settings to be set up, it is a lot faster to use an existing XMonad configuration like def and only tweak a few settings rather than to build it from the ground up. In the Person example, we can modify a concrete value as follows:

-- BetterPerson.hs file continued
myConcreteModifiedPerson = myConcretePerson
  { name = "Lisa"
  }

Here we take the myConcretePerson value from before and create a copy named myConcreteModifiedPerson with all the same fields except the name set to "Lisa" instead of "Jane".

One drawback of Haskell is that all variables like myConcretePerson can only be assigned once and contain the same value forever. Thus we can never modify an existing variable but only make a copy with our desired changes. Actually, this is one of Haskell’s strengths.

Now you should roughly be able to understand XConfig code in example configurations on the web, that usually look as follows:

windowsKey = mod4Mask
-- ...

myConfig = def
  { modMask = windowsKey
  , terminal = "gnome-terminal"
  -- ...
  }

Here, people use XMonad’s default XConfig value def and tweak a few settings they aren’t happy with.

Access fields of a record value

As your XMonad configuration becomes more and more complicated or you start to set up settings like keyboard shortcut in the keys field/setting of XConfig, you need to write functions that need to access some field of a record value. Especially for the keys field you may need access to some other fields of XConfig. This field has this weird-looking type:

data XConfig l = XConfig
    { ...
    , keys :: XConfig ... -> M.Map ...
    , ...
    }

Briefly, this type tells us that keys is not just an ordinary setting. Instead the -> symbol tells us that it’s a function that expects a value of type XConfig and, given such an XConfig value, returns a value of type M.Map.
[How to set up keys.]
[How to write functions.]

Since XConfig is complicated, let us get back to Person example to learn how to access fields of a record value.

-- BetterPerson.hs file
data Person = Person
  { name :: String
  , height :: Int
  , isAdult :: Bool
  }

myConcretePerson :: Person -- Person type, optional
myConcretePerson = Person  -- Person value constructor
  { name = "Jane"
  , height = 134,
  , isAdult = False
  }

myGreetText :: String -- optional
myGreetText = "Hello " ++ (name myConcretePerson) -- result: "Hello Jane"

When you define the Person type as above, the field names also become so-called “accessor functions”. For example, field name name also becomes an (accessor) function of type Person -> String, meaning that, given a Person value, it returns its name string. This is why name myConcretePerson computes "Jane". This string by the way then is combined with "Hello " using the ++ operator to build a new string "Hello Jane".

So now when you see code like

... (terminal def)...

and you remember that terminal is a string field of XConfig, you know that the string value of the terminal setting of the def value is computed.

Furthermore, you can use accessor functions on functions arguments as follows:

myGreetFunc :: Person -> String -- optional
myGreetFunc p = "Hello " ++ (name p)

Here, by the type of myGreetFunc you know that this argument variable p is a Person value, so that you can use the name accessor function.
[How to read and write functions.]

For the keys setting of XConfig, for example, you also can access other XConfig settings in the same style:

-- myKeysFunc :: XConfig ... -> M.Map ...
myKeysFunc baseConfig = M.fromList $
 [ (((modKey baseConfig), xK_Return), spawn  (terminal baseConfig))
 , (((modKey baseConfig), xK_q),      restart "xmonad" True)
 ]

myConfig = def
  { keys = myKeysFun -- override previous keymaps
  }

Here myKeysFunc is a function that gets an XConfig value, which it uses locally (inside this function) under the name baseConfig. With terminal baseConfig it extracts the terminal string value from this given baseConfig record value, and with modKey baseConfig it extracts the modKey value.

Pattern matching to access a field from a record value

There is another style called “Pattern Matching” to extract a value from a record.

myConcretePerson :: Person -- Person type, optional
myConcretePerson = Person  -- Person value constructor
  { name = "Jane"
  , height = 134,
  , isAdult = False
  }

myGreetFunc2 :: Person -> String
myGreetFunc2 (Person { name = localName }) = "Hello " ++ localName

Here in the myGreetFunc2 function we match a given Person value structurally against the pattern Person { name = ... }, where Person in front of { name ...} is the value constructor for the Person type. Here we match the person’s field name and use its value locally under the name localName in "Hello o" ++ localName.

In many XMonad configurations you will finde code that use pattern matching mixed with accesor functions as follows:

-- myKeysFunc :: XConfig ... -> M.Map ...
myKeysFunc baseConfig@(XConfig {modMask = modKey}) = M.fromList $
 [ ((modKey, xK_Return), spawn (terminal baseConfig))
 , ((modKey, xK_q),      restart "xmonad" True)
 ]

myConfig = def
  { keys = myKeysFun -- override previous keymaps
  }

Here in the myKeysFunc function we match a given XConfig value structurally against (XConfig {modMask...}) to extract the modMask field and use it locally under the name modKey. This baseConfig@(...) notation with the @ symbol is called “As-pattern” and is used to give a name, namely baseConfig, to the whole given XConfig value. This allows us to use accesor functions like terminal on it as in (terminal baseConfig) while using pattern matching at the same time.

More on data structures

The following section is only useful to read if you want to dive deeper into Haskell. It will explain data types in more detail so that you can understand it in Haskell code outside of XMonad.
data structure notation from the ground up.

The data keyword introduces a new type.

data <new type name> = <new value 1> | <new value 2> | ...

For example, the Bool type of the Haskell standard library is written

data Bool = True | False

Here Bool is the type and True and False, separated by |, are the only values of this type. Note how these three names start with an uppercase letter. Types and values always begin with an uppercase letter Values like True and False are also called value constructors, because they construct a value.

You will understand why the word “constructor” is useful when we construct new types with their values out of existing ones. For example, we can reuse an existing type like Bool to build a new one like Box as in this contrived example:

data Box = EmptyBoxConstructor | NonEmptyBoxConstructor Bool

With this data notation we introduce a new type called Box with three values, even though we only mention two value constructors:

  • EmptyBoxConstructor representing an empty box
  • NonEmptyBoxConstructor True representing a box containing the Bool value True.
  • NonEmptyBoxConstructor False representing a box containing the Bool value False.

Here the distinction between value and value constructor is essential. The NonEmptyBoxConstructor Bool in the data notation tells us that this thing/word NonEmptyBoxConstructor alone is not a value but a value constructor. Only in combination with a specific Bool value, like NonEmptyBoxConstructor True in that order, does it become a Box value! Similarly, you can view EmptyBoxConstructor as a constructor that needs “nothing” to become a value. That is, it becomes a value on its own. Note that a type like Box that is constructed out of other existing types is called an algebraic data type.

The value constructors EmptyBoxConstructor and NonEmptyBoxConstructor from before are way too wordy and unnecessarily long. In practice you use short and readable names like:

data Box = Empty | Box Bool

One cool feature of Haskell is that it’s possible and popular to give a type and one of its value constructors an identical name. This relieves you from having to invent another name, because naming is hard. There is no confusion in this data notation. The Box word to the left of = is the type and the right one is a value constructor. With practice you won’t confuse whether something is a type or a value (constructor), because they appear in completely separated places. For example, when you see Box True in code, you know that it must be the Box value constructor, because the type Box makes no sense here.

The authors of XMonad also used the same name for the XConfig type as well as one (and the only one) value constructor:

data XConfig l = XConfig
     { ...
     }

It also uses these two braces { ... } as record notation. Just take a look at XMonad’s open source code.

But what about this letter l? In Haskell it is possible and popular to define several types at once when you have a container like Box and a payload like Bool but when the container doesn’t care about the type of its payload. We will explain this letter by using our contrived Box example.

You define several similary shaped types as follows:

data Box payload = Empty | Box payload

Here in this data notation the box can not only contain booleans but any “type” of values. On the left side of = the name Box is the container type and payload is a type variable for the payload type.A type variable is a placeholder for a concrete type like Bool and begins with a lowercase letter (Actually, all variables begin with a lowercase letter).

With this single line you have defined infinitely many types of a similar shape at once. On the left-hand side of = you can now generate types like Box Bool and Box Int as if you would have written these data notations:

---equivalent notation
data BoxBool = Empty | Box Bool
data BoxInt = Empty | Box Int
...

The name Box on the left of = is not a type anymore but a type constructor. Just like value concstructors, only in combination with concrete type like Bool does it become a type. Thus, Box Bool in that order is a type, with Box True and Box False being possible values. Box Int in that order is another type with Box 1 or Box -3 being possible values. However, as the code with “equivalent” data notations suggests, the Box Int type is completely separated from and has nothing to do with the Box Bool type.

Finally, one cool feature which the Haskell community agreed on is to give a type variable a really, really short name. So instead of payload you see a single letter like a, p, or l. The reason is that a type variable is so generic (it can be replaced by any concrete type) that is reasonable to give it an equally generic name. Some examples are:

data Box a = Empty | Box a

data NonEmptyBox p = NonEmptyBox p

data XConfig l = XConfig {... l ...}

data Map k a = ...

You sometimes get a small hint at what a type variable is intended to represent. For example, with the letter l you get a hint that XConfig expects a value of some layout type as its payload, but leaves open which concrete one. With a type for a mapping like Map k a you get a hint that type variable k needs to be replaced by the type you want to use as a “key”, and a represents “anything” as it payload type.
[Layouts in XMonad.]

Tags: xmonad haskell types records tutorial beginner

Malte Neuss

Java Software Engineer by day, Haskell enthusiast by night.

Other Posts In Series

Just Enough Haskell For XMonad

XMonad Layouts

With XMonad you can not only control which apps are shown on which screen or workspace but also how app windows are arranged on a single workspace: These window arrangements are called layouts.

Read More

Just Enough Haskell For XMonad

Learn Haskell Functions with XMonad's XConfig example

Small changes to XMonad like changing keyboard shortcuts require little Haskell knowledge. However, bigger customizations, especially to organize your code when adapting other people’s configurations, are easier when you understand Haskell functions.

Read More

Just Enough Haskell For XMonad

Configure a light-weight status bar for XMonad: Xmobar

The status bar Xmobar works out of the box on the top edge of your screen and shows useful system info. However, if you want to make it look nice and change what information to show, you can customize it with a tiny bit of Haskell.

Read More