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?
1 Answer 1
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 lengthrolls
mapM (const $ rollDie sides)
replaces all the numbers in that list with an invocation ofrollDie sides
and performs the same job assequence
thus returning anRandom g [Int]
sum <$>
(orfmap sum $
) goes under theRandom 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 Int
s: 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.
-
\$\begingroup\$ thanks, I will digest this a bit. first question - I'm just googling what is
fmap
and why notmap
... it seems likefmap
is more 'general' and I should always just use it instead ofmap
? stackoverflow.com/a/6824333/202168 \$\endgroup\$Anentropic– Anentropic2015年12月22日 11:24:03 +00:00Commented Dec 22, 2015 at 11:24 -
\$\begingroup\$ I don't understand why
fmap tail
only strips the first char from the second half and not from the first half too. I tried in ghcifmap 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– Anentropic2015年12月22日 11:35:51 +00:00Commented Dec 22, 2015 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 whyfmap sum
is appropriate here (or even works).liftM sum
made sense to me but now I am confused. \$\endgroup\$Anentropic– Anentropic2015年12月22日 12:01:53 +00:00Commented Dec 22, 2015 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 infixliftM
? I had to add parentheses around the right-hand-side to get it to compile \$\endgroup\$Anentropic– Anentropic2015年12月22日 12:06:26 +00:00Commented Dec 22, 2015 at 12:06 -
1\$\begingroup\$ I'm going to have to read more about
fmap
, it's hurting my brain. If I understood the docs you linked it does different things depending on the type of the second arg. In ghci it does what I'd expect if passed a list, so this special behaviour for a pair seems a major WTF. I'm sure there's good reason for this but I find it hard to conceptualise currently \$\endgroup\$Anentropic– Anentropic2015年12月23日 01:02:59 +00:00Commented Dec 23, 2015 at 1:02