Random Group Assignments
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).
= do
main <- getArgs
groups <- getContents
rawMembers
<- shuffleM $ parseMembers rawMembers
shuffledMembers $ gather $ zip (cycle groups) shuffledMembers prettyPrint
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]
= map trim . separate
parseMembers where
= split . dropDelims . dropBlanks . oneOf $ ",\n"
separate = unpack . strip . pack trim
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])]
= Map.toList . foldl mergeIn Map.empty
gather where
map (k, v) = Map.insertWith (++) k [v] map mergeIn
After gather
, the data’s structure matches the output’s structure, but punctuation is needed.
prettyPrint :: [(String, [String])] -> IO ()
= mapM_ (putStrLn . format)
prettyPrint where
group, members) = group ++ ": " ++ intercalate ", " members format (
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.
Mainly because of the HsShellScript library, though I didn’t need it for this script.↩︎
Haskell needs a “quick start” library, with definitions like
join = intercalate
andtrim = unpack . strip . pack
.↩︎I typically receive the input as prose via chat message.↩︎
A method chaining version of
split . dropDelims . dropBlanks . oneOf $ ",\n"
in a language like Java might beoneOf(",\n") .dropBlanks() .dropDelims() .split(string)
.↩︎