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
= "gnome-terminal"
myTerminal
= def {
myConfig = myTerminal
{ terminal }
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
:
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
= def
myConfig = 2 -- error, terminal is a String, but is given a number
{ terminal }
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.
= def
myConfig = "gnome-tarminal"
{ terminal }
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
= PersonValueConstructor "John" 180 True -- first style
myConcretePerson1
= PersonValueConstructor -- second style
myConcretePerson2 = "Jane"
{ name = 134,
, height = False
, isAdult }
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
= Person -- Person value constructor
myConcretePerson = "Jane"
{ name = 134,
, height = False
, isAdult }
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
= myConcretePerson
myConcreteModifiedPerson = "Lisa"
{ name }
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:
= mod4Mask
windowsKey -- ...
= def
myConfig = windowsKey
{ modMask = "gnome-terminal"
, 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
= Person -- Person value constructor
myConcretePerson = "Jane"
{ name = 134,
, height = False
, isAdult
}
myGreetText :: String -- optional
= "Hello " ++ (name myConcretePerson) -- result: "Hello Jane" myGreetText
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
= "Hello " ++ (name p) myGreetFunc 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 ...
= M.fromList $
myKeysFunc baseConfig
[ (((modKey baseConfig), xK_Return), spawn (terminal baseConfig))"xmonad" True)
, (((modKey baseConfig), xK_q), restart
]
= def
myConfig = myKeysFun -- override previous keymaps
{ keys }
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
= Person -- Person value constructor
myConcretePerson = "Jane"
{ name = 134,
, height = False
, isAdult
}
myGreetFunc2 :: Person -> String
Person { name = localName }) = "Hello " ++ localName myGreetFunc2 (
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 ...
@(XConfig {modMask = modKey}) = M.fromList $
myKeysFunc baseConfig
[ ((modKey, xK_Return), spawn (terminal baseConfig))"xmonad" True)
, ((modKey, xK_q), restart
]
= def
myConfig = myKeysFun -- override previous keymaps
{ keys }
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 boxNonEmptyBoxConstructor True
representing a box containing theBool
valueTrue
.NonEmptyBoxConstructor False
representing a box containing theBool
valueFalse
.
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.]