Random Group Assignments

Posted on October 13, 2013 by Brian Jaress
Tags: code, japan, haskell

This small script randomly assigns members to groups of (as much as possible) equal size. I wrote it for Katie’s weekend “coffee talks” where her students practice English with a native speaker.

Recently, I decided1 to write my personal scripts in Haskell. I’m still a Haskell beginner, so even small scripts like this one are learning experiences.

Example Run

Groups are command-line arguments, and members are passed through stdin:

# randgroups Katie Brian James
Hiro, Kazumi, Yoko M, Keiko,
Kazuko, Ritsuko, Hide, Miki

Possible output:

Brian: Hide, Keiko, Kazuko
James: Hiro, Kazumi
Katie: Ritsuko, Miki, Yoko M

Libraries

Most work is handled by libraries. The randomness comes from the random-shuffle package, based on code by Oleg Kiselyov. Also, since Haskell isn’t tailored toward string manipulation, we need a fair number of imports for that.2

import Data.Char (isSpace)
import Data.List (intercalate)
import Data.List.Split (split, dropDelims, oneOf, dropBlanks)
import Data.Text.Lazy (strip, pack, unpack)
import qualified Data.Map as Map
import System.Directory (getHomeDirectory)
import System.Environment (getArgs)
import System.Random.Shuffle (shuffleM)

Core Logic

The members are shuffled into a random order, then assigned round-robin to the groups. Because of that, input order of the groups determines which groups are larger (if they cannot be exactly the same).

main = do
    groups <- getArgs
    rawMembers <- getContents

    shuffledMembers <- shuffleM $ parseMembers rawMembers
    prettyPrint $ gather $ zip (cycle groups) shuffledMembers

Input

Group members are separated by a either a newline, a comma, or a comma followed by some number of spaces. Internal spaces are allowed, but internal commas and newlines are not.3

Valid input:

Alexander, Ashley J., Ashley K., Ava, Christopher R.,
Christopher S., Dominick,
Donnie, Hiram, Lucinda, Marcella, Michael,
Nikki
parseMembers :: String -> [String]
parseMembers = map trim . separate
    where
        separate = split . dropDelims . dropBlanks . oneOf $ ",\n"
        trim = unpack . strip . pack

The split library is based on function composition, an approach that ends up similar to method chaining but with the order reversed.4

Output

The assignment above creates a list of pairs, each member paired with its group. The gather function brings together members assigned to the same group, so that each group becomes paired with its full list of members.

gather :: Ord a => [(a, b)] -> [(a, [b])]
gather = Map.toList . foldl mergeIn Map.empty
    where
        mergeIn map (k, v) = Map.insertWith (++) k [v] map

After gather, the data’s structure matches the output’s structure, but punctuation is needed.

prettyPrint :: [(String, [String])] -> IO ()
prettyPrint = mapM_ (putStrLn . format)
    where
        format (group, members) = group ++ ": " ++ intercalate ", " members

Possible output:

Group A: Donnie, Lucinda, Ashley K., Christopher R., Marcella
Group B: Hiram, Christopher S., Nikki, Michael
Group C: Alexander, Ashley J., Dominick, Ava

Final Thoughts

Most of the work was looking up libraries in the documentation. In fact, the most challenging part was navigating documentation that is organized according to the rules of Haskell.

For example, when a library function returns an unfamiliar type class, information on how to use it might be in the list of known implementations, rather than the type class definition. That’s because Haskell can allow the caller to choose the type of the result. Some type classes only define creation functions and rely on each type to define its own way of accessing the result.

Overall, I enjoyed writing this, and it’s useful to me. Haskell was surprisingly suitable for a small text-processing script.


  1. Mainly because of the HsShellScript library, though I didn’t need it for this script.↩︎

  2. Haskell needs a “quick start” library, with definitions like join = intercalate and trim = unpack . strip . pack.↩︎

  3. I typically receive the input as prose via chat message.↩︎

  4. A method chaining version of split . dropDelims . dropBlanks . oneOf $ ",\n" in a language like Java might be oneOf(",\n") .dropBlanks() .dropDelims() .split(string).↩︎