Rolling Random Romans

I’ve recently been reading about the Roman Republic as well as digging into the Elm language. I decided to try my hand at randomly generating Roman names. This would involve multiple random components, both dependent and independent.

Roman names

Romans names during the republican period followed a pattern known as the tria nomina. They were composed of:

  • Praenomen a personal name given to an individual.
  • Nomen a family or clan name.
  • Cognomen an optional nickname. Some of these were hereditary and identified particular branches of a family.
  • Agnomen an optional nickname, given if you already had a cognomen.

Random in a functional world

In a functional language like Elm, all functions must be pure, that is that a function should always return the same output given the same arguments. Random operations are inherently not pure. You call your random function and might get a different value each time. That’s the whole point.

Elm tackles this issue via a divide-and-conquer approach. It separates the process of generating randomness from the process of converting that randomness into data or operations such as a number or picking an item from a list. Randomness in Elm is represented by a Seed while values are generated from Generators. The two are combined together with the Random.generate function to generate a random value based on the randomness of the seed. Random.generate : Generator a -> Seed -> (a, Seed) is a pure function, it will return the same value every time it is called with the same seed and generator.

Note that is type of random number generation, called deterministic random or pseudorandom generation, while great for applications like procedurally generating a game level or displaying a list in random order is not cryptographically secure and should not be used for security-related functionality.

Dealing with seeds

We still haven’t solved the issue. Where do the random seeds come from? Is this a “turtles all the way down” kind of problem? At least initially, the random seed is passed into the program from the outside world. It could be generated by JavaScript and passed in via a port, it might come from a time signal, it might even be user input (a common pattern when generating maps in games).

Once we have a seed, we don’t want to keep using it multiple times because that will keep giving us the same values. To solve this problem, Random.generate doesn’t just return a random value. Instead it returns a tuple of (value, newSeed). We can then use this new seed in our next random calculation. Any number of random operations can be chained together like this, each using the seed generated by the previous operation.

diagram of chaining random operations and passing seeds

Thinking in generators

Dealing with seeds quickly gets cumbersome, particularly when generating more complex random data. There are at least 6 random operations required to generate our random Roman names:

  • pick a random praenomen from a Generator String
  • pick a random nomen from a Generator String
  • randomly decide if this character has a cognomen from a Generator Bool
  • if yes pick a random cognomen from a Generator String
  • randomly decide if this character has an agnomen from a Generator Bool
  • if yes pick a random agnomen from a Generator String

In an imperative language, I would generate these 6 values individually and then combine them together to get a full name. In Elm, it’s better to transform and combine simple generators into more complex generators. Ideally, we would only call Random.generate once with a Generator Roman.

Simple transformations

The NoRedInk/elm-random-extra package provides some great utility functions that are not available in the core Random module. In particular, it provides the selectWithDefault : a -> List a -> Generator a function that picks a random value from a list or returns a default if the list is empty. We can use this to create a generator of praenomenina:

import Random.Extra as RandomE
import Random exposing(Generator)

praenomen : Generator String
praenomen =
  RandomE.selectWithDefault "Marcus" ["Marcus", "Quintus", "Gaius"]

Now we can define a very simple Roman type:

type alias Roman =
  { praenomen : String

We can transform the praenomen generator into a roman generator by using : (a -> b) -> Generator a -> Generator b.

roman : Generator Roman
roman = Roman praenomen takes a function that will transform the values returned by the given generator. Here, we’re using the constructor function Roman : String -> Roman to convert the string returned by the praenomen generator (e.g. “Marcus”) into a roman (e.g. { praenomen = "Marcus" }). Note that we haven’t actually generated values here, only described how to transform them when they are generated.

Combining generators

Adding a nomen generator is very similar to our praenomen generator:

nomen : Generator String
nomen =
  RandomE.selectWithDefault "Julius" ["Julius", "Cornelius", "Junius"]

we can add a nomen to our Roman type:

type alias Roman =
  { praenomen : String
  , nomen : String

Our constructor now has two arguments: Roman : String -> String -> Roman. Just like List, Random has map2, map3, and friends which allow us to map a function that takes n arguments over n generators.

roman : Generator Roman
roman =
  Random.map2 Roman praenomen nomen

Again, we aren’t actually generating any random values here, just saying “to generate a random Roman, generate a random praenomen and nomen and pass them to the Roman function”.

Complex values

Adding a cognomen isn’t quite as straightforward because not all Romans have one. Our Roman type would now look like:

type alias Roman =
  { praenomen : String
  , nomen : String
  , cognomen : Maybe String

Maybe represents an optional value. Valid cognomina could be Just "Caesar" and Nothing. A generator that returns Nothing 50% of the time and Just String 50% of the time might look like:

import Random.Maybe as RandomM

cognomen : Generator (Maybe String)
cognomen =
  RandomE.selectWithDefault "Metellus" ["Metellus", "Caesar", "Brutus"]
    |> RandomM.maybe

The first line is familiar by now. Random.Maybe.maybe : Generator a -> Generator (Maybe a) is a function provided by the NoRedInk/elm-random-extra package. It takes a generator as input and will wrap the values of that generator in Just 50% of the time and return Nothing otherwise.

Now we can add the cognomen generator to the list of generators mapped by the roman generator.

roman : Generator Roman
roman =
  Random.map3 Roman praenomen nomen cognomen

Dependent values

Like the cognomen, the agnomen is also an optional value.

type alias Roman =
  { praenomen : String
  , nomen : String
  , cognomen : Maybe String
  , agnomen : Maybe String

There is a twist. We should only roll an agnomen for Romans that already have a cognomen. Romans with a cognomen of Nothing should also have an agnomen of Nothing.

Due to this dependency, the agnomen generator takes in a cognomen as an argument.

agnomen : Maybe String -> Generator (Maybe String)
agnomen cognomen' =
  case cognomen' of
    Nothing -> RandomE.constant Nothing
    Just _ -> RandomE.selectWithDefault "Pius" ["Pius", "Felix", "Africanus"]
      |> RandomM.maybe

Note that the cognomen passed into this function is an actual value (Maybe String) and not a generator. We pattern match on that value and return either return a generator that always returns Nothing or a generator that randomly returns either Nothing or Just a random agnomen from the list.

So how do we combine this generator with the others to get a Roman generator? Random provides the Random.andThen : Generator a -> (a -> Generator b) -> Generator b function that allows us to chain two dependent random operations.

roman : Generator Roman
roman =
  let agnomen' = cognomen `andThen` agnomen
      Random.map4 Roman praenomen nomen cognomen agnomen'

Correctly combining dependent values

The latest implementation of the roman generator has a bug in it. The cognomen generator is being called twice. Once to generate the cognomen and again when generating the agnomen. This means it is possible to get a Roman that has an agnomen but no cognomen. We want the same cognomen to be used for both the Roman’s cognomen and generating the agnomen.

We can handle this by creating a nickNames generator that returns a tuple of (cognomen, agnomen).

Our roman generator would now look like:

roman : Generator Roman
roman =
  let nickNames' = cognomen `andThen` nickNames
      roman' pn n (cn, an) = Roman pn n cn an
     Random.map3 roman' praenomen nomen nickNames'

We can can no longer use the Roman constructor directly in our map3 function because some of the values are combined together in a tuple. Notice that we only call the cognomen generator once here.

The nickNames generator would look like:

nickNames : Maybe String -> Generator (Maybe String, Maybe String)
nickNames cognomen =
  case cognomen of
    Just _ -> (\agnomen' -> (cognomen, agnomen')) agnomen
    Nothing -> RandomE.constant (Nothing, Nothing)

Since nickNames now takes care of calling the dependency on whether or not the cognomen is present, we can simplify the agnomen generator to:

agnomen : Generator (Maybe String)
agnomen =
  RandomE.selectWithDefault "Pius" ["Pius", "Felix", "Africanus"]
    |> RandomM.maybe

Viewing the results

We now have a Generator Roman that will randomly generate a Roman with a valid tria nomina. Now we need to display it.

We can add a name function that will turn a Roman into a formatted string.

name : Roman -> String
name roman =
  let cognomen' = Maybe.withDefault "" roman.cognomen
      agnomen' = Maybe.withDefault "" roman.agnomen
     String.join " " [roman.praenomen, roman.nomen, cognomen', agnomen']

We also need to actually generate the Roman based on a random seed passed in via a port and display the name to the user:

randomRoman : Roman
randomRoman =
  Random.generate RandomR.roman (Random.initialSeed jsSeed) |> fst

port jsSeed : Int

main : Html
main =
  main' []
  [ h1 [] [ text "Rolling Random Romans" ]
  , p [] [ randomRoman |> |> text ]

Finally, we need to generate a random initial seed in javascript and pass it to the port (main.js is the compiled Elm program):

    <script src="main.js"></script>
      Elm.fullscreen(Elm.Main, { jsSeed: Math.floor(Math.random() * 10000000000) })

That’s how you roll random Romans.

Deja vu

If calling map and andThen seem familiar from working with other types such as List, Signal, and Maybe, that’s because there is a pattern going on here. In functional terminology, types that have map functions are called Functors and types that have an andThen function are the infamous Monad.

Lessons from Random

Working with Random and Generator, I’ve learned to approach purely functional randomness with a different mindset. Some big takeaways were:

  • Don’t call Random.generate all the time, instead try to think in terms of generators.
  • Transform generators from one type to another with
  • Combine independent generators together with Random.map2 and family
  • Chain multiple dependent random operations with Random.andThen
  • Any complex generator can be built up from simpler generators via these functions.

Rolling really realistic Romans

There is a lot more fun to be had with Romans and randomness. We can keep using the patterns discussed earlier to make our generated names more realistic by adding more variables and dependencies.

  • Romans had a different naming scheme for women and men. We could randomly generate a gender and then conditionally generate the proper name based of the result.

  • Romans were also broken into two broad social classes: patricians and plebians. Some families (and thus the nomen) were exclusively patrician while others were exclusively plebian. Some families had both patrician and plebian branches. We could assign a random social status and then conditionally pick the nomen from a list of historical patrician or plebian names.

  • Some cognomina such as “Caesar” were hereditary and identified a particular branch of a family (in this case the Julia family). We could conditionally generate the cognomen based on the nomen from a list of historical cognomina used by that family. For characters without a hereditary cognomen we can still generate a random cognomen or Nothing.

  • Some families strongly preferred (or avoided) a set of praenomina. We could generate the praenomen biased by family preferences.

  • Any time we’ve done one thing or another, we’ve used a 50% chance. Not all rolls should have even distribution of outcomes.

Source and demos

I’ve published the source for this article on GitHub. I’ve also implemented the “really realistic” features described above as version 2. In addition, I’ve published a demo of version 2.