Dungeon & Dragons & Functors
Tags: ocaml, functional
Published: 2020-12-28 09:16:17

When I discover or want to explain a concept, I quite like to rely on a Kata, a short exercise that highlight a programming practice. I therefore propose the DnD Kata, whose objective is to model a team of characters from the Dungeon and Dragons role-playing game. Of course, modeling the set of rules is a complex exercise. 

We will content ourselves here with:
1) representing a character by:
- his/her name
- his/her race
- his/her skills, including and of course his racial bonuses

2) A team is a collection of characters, which can be of different races.

The objective of this solution is to illustrate OCaml functors and demonstrate how they contribute to applying the S.O.L.I.D principles in OCaml.

_This post is a translation from my blog post: https://oteku.github.io/ocaml-functors/ (FR)_

## About DnD

Dunjon & Dragons a.k.a DnD is a role playing game where players play heroes in a fantasy setup.
The main setup for this game is Faerûn, a continent of the Abeir-Toril planet.
We will use the [Dungeons & Dragons 5th Edition System](https://github.com/Umbyology/OGL-SRD5) under the Open-Gaming License (OGL).

## We are the Dwarves

First we want to modelize Dwarves, one of the playable races in Faerûn, by their names.

We already know that a good way to have a namespace in OCaml is to use modules, so we can start with this representation:

```ocaml
module Dwarf = struct
  type t = string
end
```

In this implementation, the type of the module is infered. We can also make it explicit by adding a module signature and modelize Elves at the same time:

```OCaml
module Dwarf : sig
  type t = string
end = struct
  type t = string
end

module Elf : sig
  type t = string
end = struct
  type t = string
end
```

At this step we notice that the 2 modules are sharing the same signature. Since both Elf and Dwarf modules are representing playable heroes, it seems legit and we would make explicit that all playable heroes are sharing the same signature. To do that we can use a module type:

```OCaml
module type PLAYABLE = sig
  type t = string
end

module Dwarf : PLAYABLE = struct
  type t = string
end

module Elf : PLAYABLE = struct
  type t = string
end
```

Other modules do not need to know the shape of a `PLAYABLE.t`, they only need to know it exists and the module should expose functions to work with it.

We call this make an **abstraction**:

```ocaml
module type PLAYABLE = sig
  type t
  val to_string : t -> string
  val of_string : string -> t
end
```

Now each module of type PLAYABLE must implement those functions. Let's do it:

```ocaml
module Dwarf : PLAYABLE  = struct
  type t = {name : string}
  let to_string dwarf = dwarf.name
  let of_string name = {name}
end

module Elf : PLAYABLE = struct
  type t = string
  let to_string elf = elf
  let of_string name = name
end
```

Since `t` is abstract, you may notice that each module implementing `PLAYABLE` may have a different concret type for `t`. It's totally fine while they respect their module type contract.

Other modules cannot access a concrete value of `t`, but we can create a dwarf or get a string representation.

```ocaml
let gimly = Dwarf.of_string "Gimly"
let () = Dwarf.to_string gimply |> print_endline
```

## Heroes have abilities

In DnD, a Hero is also represented by its abilities.
There is several option rules for abilities at the creation, we will only implement the _Standard scores_ one. At the beginning each ability have a value of 10:

```ocaml
module Abilities = struct
  type t = {
    strength : int
  ; dexterity : int
  ; constitution : int
  ; intelligence : int
  ; wisdom : int
  ; charisma : int
  }

  let init () =  {
    strength = 10
  ; dexterity = 10
  ; constitution = 10
  ; intelligence = 10
  ; wisdom = 10
  ; charisma = 10
  }
end
```

We can upgrade our Dwarf modules this way:

```ocaml
module Dwarf: PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let to_string dwarf = dwarf.name
  let of_string name = {name ; abilities = Abilities.init()}
end
```

Naming for our function is no more logical, so we will update `PLAYABLE` module type and then `Elf` and `Dwarf` modules:

```ocaml
module type PLAYABLE = sig
  type t
  val name : t -> string
  val make : string -> t
end

module Dwarf: PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let name dwarf = dwarf.name
  let make name = {name ; abilities = Abilities.init()}
end

module Elf: PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let name elf = elf.name
  let make name = {name ; abilities = Abilities.init()}
end
```

## Races give modifiers

### The Darves have a constition bonus +2.

In OCaml, modules are first-class, it means you can use module as value. So we can create a new module type to represent a bonus and functions to represent a bonus of 2:

```ocaml
module type BONUS = sig
  type t
  val value : t
end

let bonus_2 : (module BONUS with type t = int) = (module struct
    type t = int
    let value = 2
end)
```

`bonus_2` is a module as value. Because `t` is abstract we must add a type witness `with type t = int`.

To unwrap the value of the bonus we also need a getter:

```ocaml
let get_bonus b = let module M = (val (b : (module BONUS with type t = int))) in M.value
```

> If you need more explaination about First-Class, you should read : https://dev.realworldocaml.org/first-class-modules.html

Now we can write:

```ocaml
module Dwarf: PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let name dwarf = dwarf.name
  let make name = {name ; abilities = Abilities.init()}
  let constitution dwarf = dwarf.abilities.constitution + get_bonus bonus_2
end
```

#### Also are Elves, Half-orc, Halflings, Tieflings

Dwarves are not the only race in Faerun. Each have a different constitution bonus. Half orcs have +1 while Elves, Halflings and Tieflings don't have constitution bonus.

When data varies inside a function we add a function parameter to avoid code duplication. We can do the same at module level. OCaml provides **functors** which are functional modules : function from module to module.

So we can create a `Race` functor:

```ocaml
module Race (B : BONUS with type t = int) : PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let name character = character.name
  let make name = {name ; abilities = Abilities.init()}
  let constitution_bonus = B.value (* here we get the value from module B *)
  let constitution character = character.abilities.constitution + constitution_bonus
end
```

You read this as : the functor `Race` take a module `B` of type `BONUS` whom type `t` is `int` as parameter and then return a module of type `PLAYBLE`.

Then we can easily have our modules:

```ocaml
(* we add a function to manage all bonus *)
let bonus (x:int) : (module BONUS with type t = int) = (module struct
    type t = int
    let value = x
end)

(* we use our Race functor to create the five races *)
module Dwarf = Race (val bonus 2)
module Elf = Race (val bonus 0)
module Tiefling = Race (val bonus 0)
module Halfling = Race (val bonus 0)
module HalfOrc = Race (val bonus 1)
```

#### All abilities may have bonus

Functors are not limited to one parameter, so we can use the same trick to manage all bonuses:

```ocaml
module Race
    (BS : BONUS with type t = int)
    (BD : BONUS with type t = int)
    (BC : BONUS with type t = int)
    (BI : BONUS with type t = int)
    (BW : BONUS with type t = int)
    (BCh : BONUS with type t = int) : PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let name character = character.name
  let make name = {name ; abilities = Abilities.init()}
  let bonus = Abilities.{
      strength = BS.value
    ; dexterity = BD.value
    ; constitution = BC.value
    ; intelligence = BI.value
    ; wisdom = BW.value
    ; charisma = BCh.value
    }
  let abilities character = Abilities.{
      strength = character.abilities.strength + bonus.strength
    ; dexterity = character.abilities.dexterity + bonus.dexterity
    ; constitution = character.abilities.constitution + bonus.constitution
    ; intelligence = character.abilities.intelligence + bonus.intelligence
    ; wisdom = character.abilities.wisdom + bonus.wisdom
    ; charisma = character.abilities.charisma + bonus.charisma
    }
end

module Dwarf = Race (val bonus 0) (val bonus 0) (val bonus 2)(val bonus 0) (val bonus 0) (val bonus 0)
```

For your use case it's not so convenient, we have to remember the order of bonuses. We already have a type that represent all abilities values `Abilities.t`, just use it instead of `int`:

```ocaml
(* just create a bonus function that take a Abilities.t and return a Bonus module *)
let bonus (x:Abilities.t) : (module BONUS with type t = Abilities.t) = (module struct
    type t = Abilities.t
    let value = x
end)

(* the functor `Race` take a module `B` of type `BONUS` whom type `t` is `Abilities.t`
** as parameter and then return a module of type `PLAYBLE`  *)
module Race
    (B : BONUS with type t = Abilities.t) : PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let name character = character.name
  let make name = {name ; abilities = Abilities.init()}
  let bonus = Abilities.{
      strength = B.value.strength
    ; dexterity = B.value.dexterity
    ; constitution = B.value.constitution
    ; intelligence = B.value.intelligence
    ; wisdom = B.value.wisdom
    ; charisma = B.value.charisma
    }
  let abilities character = Abilities.{
      strength = character.abilities.strength + bonus.strength
    ; dexterity = character.abilities.dexterity + bonus.dexterity
    ; constitution = character.abilities.constitution + bonus.constitution
    ; intelligence = character.abilities.intelligence + bonus.intelligence
    ; wisdom = character.abilities.wisdom + bonus.wisdom
    ; charisma = character.abilities.charisma + bonus.charisma
    }
end

(* create our Dwarf module *)
module Dwarf = Race (val bonus Abilities.{
    strength = 0
  ; dexterity = 0
  ; constitution = 2
  ; intelligence = 0
  ; wisdom = 0
  ; charisma = 0
  })
```

To be more concise and explicit we can work from a `no_bonus` value:

```ocaml
let no_bonus = Abilities.{
    strength = 0
  ; dexterity = 0
  ; constitution = 0
  ; intelligence = 0
  ; wisdom = 0
  ; charisma = 0
  }

module Dwarf = Race (val bonus Abilities.{
    no_bonus with constitution = 2
  })
module Elf = Race (val bonus Abilities.{
    no_bonus with dexterity = 2
  })
module Halfling = Race (val bonus Abilities.{
    no_bonus with dexterity = 2
  })
module Tiefling = Race (val bonus Abilities.{
    no_bonus with charisma = 2  ; intelligence = 1
  })
module HalfOrc = Race (val bonus Abilities.{
    no_bonus with strength = 2
  })
```

### Summary

At the end of this section you should have:

```ocaml

module Abilities = struct
  type t = {
    strength : int
  ; dexterity : int
  ; constitution : int
  ; intelligence : int
  ; wisdom : int
  ; charisma : int
  }

  let init () =  {
    strength = 10
  ; dexterity = 10
  ; constitution = 10
  ; intelligence = 10
  ; wisdom = 10
  ; charisma = 10
  }
end

module type BONUS = sig
  type t
  val value : t
end

let bonus (x:Abilities.t) : (module BONUS with type t = Abilities.t) = 
(module struct
  type t = Abilities.t
  let value = x
end)

let no_bonus = Abilities.{
    strength = 0
  ; dexterity = 0
  ; constitution = 0
  ; intelligence = 0
  ; wisdom = 0
  ; charisma = 0
  }

module type PLAYABLE = sig
  type t
  val make : string -> t
  val name : t -> string
  val abilities : t -> Abilities.t
end


module Race
    (B : BONUS with type t = Abilities.t) : PLAYABLE  = struct
  type t = {name : string ; abilities : Abilities.t}
  let name character = character.name
  let make name = {name ; abilities = Abilities.init()}
  let bonus = Abilities.{
      strength = B.value.strength
    ; dexterity = B.value.dexterity
    ; constitution = B.value.constitution
    ; intelligence = B.value.intelligence
    ; wisdom = B.value.wisdom
    ; charisma = B.value.charisma
    }
  let abilities character = Abilities.{
      strength = character.abilities.strength + bonus.strength
    ; dexterity = character.abilities.dexterity + bonus.dexterity
    ; constitution = character.abilities.constitution + bonus.constitution
    ; intelligence = character.abilities.intelligence + bonus.intelligence
    ; wisdom = character.abilities.wisdom + bonus.wisdom
    ; charisma = character.abilities.charisma + bonus.charisma
    }
end

module Dwarf = Race (val bonus Abilities.{
    no_bonus with constitution = 2
  })
module Elf = Race (val bonus Abilities.{
    no_bonus with dexterity = 2
  })
module Halfling = Race (val bonus Abilities.{
    no_bonus with dexterity = 2
  })
module Tiefling = Race (val bonus Abilities.{
    no_bonus with charisma = 2  ; intelligence = 1
  })
module HalfOrc = Race (val bonus Abilities.{
    no_bonus with strength = 2
  })
(* We can add new race with ease. Humans have +1 for all abilities *)
module Human = Race (val bonus Abilities.{
    strength = 1
  ; dexterity = 1
  ; constitution = 1
  ; intelligence = 1
  ; wisdom = 1
  ; charisma = 1
  })
```

## United color of Faerûn

Each player may play a character from different race. How to modelize a team ?

### The companions of the Hall

> The companions is a book from R.A. Salvatore a novelist who has written many novels set in Faerûn

We can create value for our teammates:

```ocaml
let catti = Human.make "Catti-brie"
let regis = Halfling.make "Regis"
let brenor = Dwarf.make "Bruenor Battlehammer"
let wulfgar = Human.make "Wulfgar"
let drizzt = Elf.make "Drizzt Do'Urden"
```

What if we create the companions:

```ocaml
❌ let companions = [catti; regis; bruenor; wulfgar;  drizzt]
```

**Error: This expression has type HalfLing.t but an expression was expected of type
Human.t**

Remember the type of `list` has type `type 'a t = 'a list` , inference engine set`'a = Human.t` because it's the type of he first element of our list `catti`, but `regis` type is `Halfling.t`.

How could we help the compiler ? Type parameters must be concrete types.

```ocaml
(* won't compile PLAYABLE is a module type  *)
❌ type team = PLAYABLE.t list

(* won't compile RACE is a functor
** aka a function from module to module  *)
❌ type team = RACE.t list
```

In reality, there is nothing too complicated, the main point is that OCaml lists are monomorphic, so we need a unique type that can represent a character, whatever their race:

```ocaml
type buddy =
  | Dwarf of Dwarf.t
  | Elf of Elf.t
  | Halfling of Halfling.t
  | Tiefling of Tiefling.t
  | HalfOrc of HalfOrc.t
  | Human of Human.t
  
let companions = [Human catti; Halfling regis; Dwarf bruenor; Human wulfgar;  Elf drizzt]
```

However, there are many other races in Faerûn, as well as variants. Drizzt for example is actually a dark elf and not an elf. It would be more appropriate to use polymorphic variants in order to facilitate the extensions of our library, because we are still at the early stage of a real character generator:

```ocaml
let companions_final = 
    [`Human catti; `Halfling regis; `Dwarf bruenor; `Human wulfgar;  `Elf drizzt]
```

whose type will be

```ocaml
val companions_final :
  [> `Dwarf of Dwarf.t
   | `Elf of Elf.t
   | `Halfling of Halfling.t
   | `Human of Human.t ]
  list =
  [`Human <abstr>; `Halfling <abstr>; `Dwarf <abstr>; `Human <abstr>;
   `Elf <abstr>]
```

## Take away

1 - OCaml provides abstractions for:

- namespaces: **module**
- protocole: **module type**
- extension: **functor**
- default value or implementation: **functor** or **first-class module**
  - functors are function from module to module
  - first-class modules are values and give a way to communicate between the type level and the module level. Exemple: a function from value to module.

2 - S.O.L.I.D is not only a OOP good pratice:

- Single responsibility principle => use **module**
- Open/closed principle => use **module**
- Liskov substitution principle => use **module type**
- Interface segregation principle => use **module type**
- Dependency inversion principle => use **functor**