9
\$\begingroup\$

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 <- take 2 (splitOn "D" (map toUpper die))]


rollDie :: (RandomGen g) => 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?

\$\endgroup\$
3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ thanks, I will digest this a bit. first question - I'm just googling what is fmap and why not map... it seems like fmap is more 'general' and I should always just use it instead of map? stackoverflow.com/a/6824333/202168 \$\endgroup\$ – Anentropic Dec 22 '15 at 11:24
  • \$\begingroup\$ I don't understand whyfmap tail only strips the first char from the second half and not from the first half too. I tried in ghci fmap tail ([1,2,3],[4,1,2,3,4]) returns ([1,2,3],[1,2,3,4]) ...I don't understand why this result \$\endgroup\$ – Anentropic Dec 22 '15 at 11:35
  • \$\begingroup\$ following your hint in 4th bullet point I wrote this line rollDice (rolls, sides) = fmap sum $ sequence $ replicate rolls (rollDie sides) which seems to work fine... however I don't understand why fmap sum is appropriate here (or even works). liftM sum made sense to me but now I am confused. \$\endgroup\$ – Anentropic Dec 22 '15 at 12:01
  • \$\begingroup\$ on the other hand, I just found this works rollDice (rolls, sides) = sum <$> (sequence $ replicate rolls (rollDie sides)) ...this makes sense to me if I understand <$> as a sort of 'monadic $' or ...an infix liftM? I had to add parentheses around the right-hand-side to get it to compile \$\endgroup\$ – Anentropic Dec 22 '15 at 12:06
  • \$\begingroup\$ fmap and liftM are the same thing. As you noticed,(<$>) is a nice infix operator for it. It does not have the same precedence as $ hence the fact that you needed to add parentheses to get haskell to parse the expression the right way. \$\endgroup\$ – gallais Dec 22 '15 at 12:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.