HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

My first Haskell: dice rolling

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
rollingfirstdicehaskell

Problem

Yesterday morning I decided to stop procrastinating and start learning me some Haskell.

So far I've made this, which is a simple cli 'dice rolling' utility that can be used like:

$ ./dice 3d20
You rolled: 17


The code looks like this:

import Data.List.Split
import Data.Char
import Control.Monad
import Control.Monad.Random
import System.Environment

type Dice = (Int, Int)

diceCode :: String -> Dice
diceCode die = (parts!!0, parts!!1)
  where
    parts = [read x :: Int | x  Int -> Rand g Int
rollDie sides = getRandomR (1, sides)

rollDice :: (RandomGen g) => Dice -> Rand g Int
rollDice dice = liftM sum (sequence(replicate rolls (rollDie sides)))
  where
    rolls = fst dice
    sides = snd dice

main = do
  args <- getArgs
  roll <- evalRandIO (rollDice (diceCode (args!!0)))
  putStrLn ("You rolled: " ++ show roll)


I was able to get the diceCode 'parsing' function by myself without too much trouble.

The rollDie function is almost straight from an example in the Control.Monad.Random docs, which helped a lot.

I struggled for quite a while to find a recipe for summing the rolls in rollDice... it seemed to me I ought to use msum but I couldn't find a way to make it work. liftM sum seems to do exactly what I wanted though.

I also found the use of tuples quite cumbersome. In Python I could just do:

rolls, sides = dice

but I seem to have to use the horribly-named fst and snd functions to access the members in Haskell (?)

I guess the next part of my adventure is to try and incorporate this into a larger program, eg a simple game. It seems like the monadically-wrapped random int values are going to force the rest of the code to be 'monad-aware' (i.e. lots of use of liftM) and I wonder if there is a way to avoid this?

Solution

The list comprehension you use in diceCode is a bit overkill: after all, you only want to split the list of characters into two. You can use break instead.

Here I have to use fmap tail to act on the second list because break retains the value it breaks the list at (the 'd' character here). This would be a good place to handle the sort of errors that would arise if there is no 'd' in the string.

diceCode :: String -> Dice
diceCode die = (read rolls, read sides) where
  (rolls, sides) = fmap tail $ break ('D' ==) $ fmap toUpper die


Your rollDice is a bit complex. Here is how I would rewrite it:

  • (rolls, sides) on the left hand side exposes the two components of the tuple



  • [1..rolls] generates a list of length rolls



  • mapM (const $ rollDie sides) replaces all the numbers in that list with an invocation of rollDie sides and performs the same job as sequence thus returning an Random g [Int]



  • sum (or fmap sum $) goes under the Random g part and sums the elements in the [Int] value.



Putting all this together we get:

rollDice :: (RandomGen g) => Dice -> Rand g Int
rollDice (rolls, sides) = sum  mapM (const $ rollDie sides) [1..rolls]


The rest of the code is quite idiomatic. One thing I was a bit concerned about is the fact that a Dice is represented as a pair of Ints: to understand which is which, you need to read the code manipulating them and if you make a mistake the compiler won't warn you: they have the same type! It's quite annoying. One thing you could do is use a record type instead in order to name the two fields:

data Dice' = Dice' { rolls :: Int
                   , sides :: Int }


This way you can access them by their names, build the Dice' using the named syntax, etc.

Code Snippets

diceCode :: String -> Dice
diceCode die = (read rolls, read sides) where
  (rolls, sides) = fmap tail $ break ('D' ==) $ fmap toUpper die
rollDice :: (RandomGen g) => Dice -> Rand g Int
rollDice (rolls, sides) = sum <$> mapM (const $ rollDie sides) [1..rolls]
data Dice' = Dice' { rolls :: Int
                   , sides :: Int }

Context

StackExchange Code Review Q#114725, answer score: 5

Revisions (0)

No revisions yet.