7
\$\begingroup\$

I'm trying to pick up some Haskell skills so thought I'd write a random password generator to get to grips with using IO. It was far trickier than I expected and I employed rather more trial and error than I'd have liked.

What I've ended up with works, and while I'm happy with it generally I'd like to know if this is reasonable, idiomatic Haskell or if I've done anything peculiar.

import System.Random

-- Generate a random number in closed interval [lo, hi]
random_int :: Int -> Int -> IO Int
random_int lo hi = (randomRIO (lo,hi) :: IO Int)

-- Generate a random character from an internal alphabet
random_char :: IO Char
random_char = do
    index <- random_int 0 $ (length alphabet) - 1
    return $ alphabet !! index
    where alphabet = ['A'..'Z'] ++
                     ['a'..'z'] ++
                     ['0'..'9'] ++
                     "!\"£$%^&*()-_+={}[]:;@'~#|\\<,>.?/'"

-- Generate a random password of given length
random_password :: Int -> IO String
random_password length =
    sequence $ map (\x -> random_char) [1..length]

main :: IO ()
main = random_password 8 >>= putStrLn

The implementation of random_password feels odd and hacky, but I struggled to find another way to create an array of results from successive invocations of random_char or any way to avoid the need for sequence. Is it hacky?

\$\endgroup\$

2 Answers 2

7
\$\begingroup\$

hlint

You should run hlint on your program - it's a good way to become familiar with techniques and practices used by expert Haskellers.

Running hlint on your code returned the following suggestions:

* use camelCase instead of underscores

./passwd.hs:7:20: Warning: Redundant bracket
Found:
  (randomRIO (lo, hi) :: IO Int)
Why not:
  randomRIO (lo, hi) :: IO Int

./passwd.hs:19:5: Error: Use mapM
Found:
  sequence $ map (\ x -> random_char) [1 .. length]
Why not:
  mapM (\ x -> random_char) [1 .. length]

./passwd.hs:19:21: Warning: Use const
Found:
  \ x -> random_char
Why not:
  const random_char

sequence . map

You're right that the code for random_passwd feels hacky since the value x is ignored. The canonical way to write this is:

import Control.Monad (replicateM)

random_passwd = replicateM length random_char

replicateM repeats a monadic action a specified number of times collecting the results in a list.

!!

You probably already know that using !! to index into a list is inefficient. A better data structure to use for alphabet is Text from Data.Text which gives you O(1) indexing.

import qualified Data.Text as T

alphabet = T.pack $ ['A'..'Z'] ++ ['a'..'z'] ++ ...

random_char = do
  index <- randomR (0, T.length alphabet - 1)
  return $ T.index alpha index
\$\endgroup\$
1
  • \$\begingroup\$ Thanks, wasn't aware of hlint, and if I'd thought to look for a monadic equivalent of replicate I'd have saved myself a lot of time! Data.Text is a new one too, so all in all lots of useful stuff here :-) \$\endgroup\$
    – boycy
    Commented Oct 3, 2015 at 23:47
3
\$\begingroup\$

As a general point of Haskell style, you have "take N from a list" upside down. You are generating a list of numbers 1 to N but ignoring each number (replacing it with a function that takes no arguments) simply because you want N items in the list. It could be N of any given integer. It could be N of anything if you are just going to throw it away and replace it (hint, hint).

The idiomatic (and more flexible/reusable) way for those tasks in general is to generate an infinite list and take N from it (since laziness means that the elements you don't take aren't actually generated).

So both

take length [1..]

and

take length $ repeat 1

would give you your infinite list of integers. The Data.List module also offers

replicate length 1

Which would give you N copies of the number 1. That's one step closer to what you need.

But you don't want integers, you want your function. By now, you have probably worked out that

replicate length random_char

gives you an N-sized list where each element is your random_char function. So

sequence $ replicate length random_char

removes the unnecessary numbers and mapping from your solution.

NOTE: Code is data and data is code. You can tread functions like data in a functional languages. Building a list of functions is a perfectly reasonable thing to do.

All that said, Haskell's Control.Monad library provides the function you really want:

replicateM :: Monad m => Int -> m a -> m [a]

replicateM n act performs the action n times, gathering the results.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks, I'll try to think about that in future when using lists. I was aware at the time that it didn't feel like the right way to do it... \$\endgroup\$
    – boycy
    Commented Oct 3, 2015 at 23:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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