Tell me more ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I want an experienced Haskeller to critique my code and offer changes/suggestions (so I can learn the idioms of the language and culture around Haskell).

I've been using a Tabula Recta for my passwords and used a Python script someone wrote to generate the table and do table traversal.

I've been wanting to learn Haskell and decided to take on building the same program but in Haskell.

I'm new to Haskell but not functional programming (I have Scheme and Erlang under my belt - Erlang I use in production on a daily basis). I feel like I kept compositional style but I doubt I adhered to idiomatic Haskell.

Here's the code:

{-# LANGUAGE BangPatterns #-}
{- |
Module      :  tabula.hs
Description :  Generates a tabula recta
Copyright   :  (c) Parnell Springmeyer
License     :  BSD3

Maintainer  :  ixmatus@gmail.com
Stability   :  experimental

Generates and manipulates a tabula recta.
-}

module Main where

import Control.Monad.CryptoRandom
import Crypto.Random

import Data.String.Utils
import Data.List.Split

main :: IO()
main = do
    values <- tabulay ['A'..'Z']
    putStrLn $ "    " ++ join " " (chunksOf 1 ['A'..'Z'])
    putStrLn $ "  + " ++ (take 52 $ cycle "- ")
    putStrLn $ join "\n" (reverse values)

tabulay :: [Char] -> IO [[Char]]
tabulay cmap = tabulay' cmap []
    where tabulay' [] acc    = do return acc
          tabulay' (x:xs) acc = do
            chars <- tabulax 26
            let formatted = [x, '|'] ++ chars
                spaced = join " " (chunksOf 1 formatted)
            tabulay' xs $! (spaced:acc)

tabulax :: Int -> IO [Char]
tabulax cint = tabulax' cint []
    where tabulax' 0 acc    = do return acc
          tabulax' iint acc = do 
            val <- randomChoice characterMap
            tabulax' (iint-1) $! (val:acc)

characterMap :: [Char]
characterMap =
    let 
        cmap = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']
        symb = [',', '.', '/', '?', '~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '+', '=', '{', '[', ']', '}', '\\', '|', ':', ';', '"', '\'']
    in cmap ++ symb

randVal :: Int -> IO Int
randVal len = do
    g <- newGenIO :: IO SystemRandom
    let Right (int, _) = crandomR (0, len-1) g :: Either GenError (Int, SystemRandom)

    -- This MUST be strict - otherwise this function will keep too many file handles open
    return $! int

randomChoice :: [Char] -> IO Char
randomChoice ls = do
    randomIndex <- randVal (length ls)
    let val = ls !! randomIndex
    return val
share|improve this question

2 Answers

up vote 6 down vote accepted

Yet another problem is the usage of lists. !! is O(N) operation. Use Data.Array instead.

As for randoms, you should call newGenIO only once and use the cryptographically secure generator it returned after that instead of creation of new generators each time you need a random character.

Your randVal function should look something like this:

randVal :: Int -> SystemRandom -> (Int, SystemRandom)
randVal len g = fromRight $ crandomR (0, len-1) g

It accepts old generator state and returns the new state to use in subsequent generations. To simplify passing the generator state around you can use a state monad. But there is already such monad - the CRand monad and corresponding CRandT monad transformer to combine random number generation with other monadic computations.

The example from the documentation for CRand fails to typecheck because of monomorphism restriction - getRandPair looks like a value but it isn't, so the compiler detects a potential problem. You can disable it for now by adding {-# LANGUAGE NoMonomorphismRestriction #-} before module Main where line.

Also, it is not clear how the SystemRandom generator works. It is secure, but I think it's not adviced to drain system entropy source. I'd rather use it just to seed HashDRBG from Crypto.Random.DRBG, which is SHA512-based RNG.

Here is a hint:

type Gen a = CRand HashDRBG GenError a

randValM :: Int -> Gen Int
randValM len = getCRandomR (0, len-1)

randomChoice :: [a] -> Gen a
randomChoice ls = do
    randomIndex <- randValM (length ls)
    return $ ls !! randomIndex

Your tabulax function will remain the same, but will operate in a different monad, so it's type will change to randomChoice :: [a] -> Gen a. Note also that I used getCRandomR from Control.Monad.Crypto.Random to make use of the monadic helper to implicitly pass the cryptogenerator around and perform error checking.

Your main will change too:

printGenerated values = do
    putStrLn $ "    " ++ join " " (chunksOf 1 ['A'..'Z'])
    putStrLn $ "  + " ++ (take 52 $ cycle "- ")
    putStrLn $ join "\n" (reverse values)

fromRight (Right a) = a

generate = evalCRand $ do
    a <- tabulay ['A'..'Z']
    return a

main = do
    g <- newGenIO :: IO HashDRBG
    printGenerated $ fromRight $ generate g

The idea is that we extracted generate - a pure function which accepts a generator and returns generated data. evalCRand is used to extract the inner a value from monadic Gen a value. It accepts two arguments: a monadic computation in Gen monad and initial state of random generator. There is also runCRand function which returns the final state of the generator in addition to the value, but we don't need the final state here.

I abused the do-notation here to help you understand code better. I would write generate and main shorter:

generate = evalCRand $ tabulay ['A'..'Z']

main = newGenIO >>= printGenerated . fromRight . generate

Your tabulatex' function is already there in the standard library. It is Control.Monad.replicateM:

import Control.Monad hiding (join)

tabulax :: Int -> Gen [Char]
tabulax cint = replicateM cint $ randomChoice characterMap

tabulatey' is also there in Control.Monad, it's mapM:

tabulay :: [Char] -> Gen [[Char]]
tabulay = mapM $ \x -> do 
            chars <- tabulax 26
            let formatted = [x, '|'] ++ chars
                spaced = join " " (chunksOf 1 formatted)
            return spaced

Next step is to use immutable arrays:

import Data.Array.IArray

tabulax :: Int -> Gen [Char]
tabulax cint = replicateM cint $ randomChoiceA arrMap

arrMap :: Array Int Char
arrMap = listArray (0, (length characterMap) - 1) characterMap

randomChoiceA ls = do
    randomIndex <- randValM $ 1 + snd (bounds ls)
    return $ ls ! randomIndex

Now randValM seems to be perverted: we convert bounds from tuple to size and back. Eliminating the extra conversion we get rid of randValM completely, as getCRandomR and bounds perfectly fit each other:

randomChoiceA ls = do
    randomIndex <- getCRandomR $ bounds ls
    return $ ls ! randomIndex

Now we can shorten it by desugaring do-notation and using function composition and infix operator sections:

randomChoiceA ls = getCRandomR (bounds ls) >>= return . (ls !)

And even more using <$> from Control.Applicative (which is the same as fmap):

randomChoiceA ls = (ls !) <$> getCRandomR (bounds ls)

Finally, we can extract separate = join " " . chunksOf 1 as it is used 2 times and shorten tabulay just for fun:

tabulay :: [Char] -> Gen [[Char]]
tabulay = mapM $ \x -> separate <$> ([x, '|'] ++) <$> tabulax 26

and get rid of ugly -1 in array bounds:

arrMap :: Array Int Char
arrMap = listArray (1, length characterMap) characterMap

So the final source is:

{-# LANGUAGE NoMonomorphismRestriction #-}
module Main where

import Control.Monad.CryptoRandom
import Crypto.Random.DRBG
import Data.String.Utils
import Data.List.Split
import Control.Monad hiding (join)
import Control.Applicative
import Data.Array.IArray

printGenerated values = do
    putStrLn $ "    " ++ separate ['A'..'Z']
    putStrLn $ "  + " ++ take 52 (cycle "- ")
    putStrLn $ join "\n" (reverse values)

fromRight (Right a) = a

generate = evalCRand $ tabulay ['A'..'Z']

main = newGenIO >>= printGenerated . fromRight . generate

separate = join " " . chunksOf 1

tabulay :: [Char] -> Gen [[Char]]
tabulay = mapM $ \x -> separate <$> ([x, '|'] ++) <$> tabulax 26

tabulax :: Int -> Gen [Char]
tabulax cint = replicateM cint $ randomChoiceA arrMap

arrMap :: Array Int Char
arrMap = listArray (1, length characterMap) characterMap

randomChoiceA ls = (ls !) <$> getCRandomR (bounds ls)

characterMap :: [Char]
characterMap =
    let 
        cmap = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']
        symb = ",./?~`!@#$%^&*()-_+={[]}\\|:;\"'"
    in cmap ++ symb

type Gen a = CRand HashDRBG GenError a
share|improve this answer
This is very helpful, thank you! – Ixmatus Sep 3 '12 at 21:58
Reading this some more - thank you for putting so much time into it, very helpful. A lot for me to learn obviously as your use of >>= is new to me. I'll have to learn more about monads I guess (yes, I'm that new to the language). This will be a good reference for me, thank you again. – Ixmatus Sep 4 '12 at 1:08
I rewrote this and it is MUCH faster than what I had wrote - I assume that's because with your code the newGenIO generator is being called only once rather than 676 times!! See I thought the generator had to be called multiple times in order to get a new random value each time - which when I originally wrote the program had "too man files open" errors - so this makes more sense now. – Ixmatus Sep 4 '12 at 3:34
Also I attempted to use replicateM before but I had issues with the IO Char type being expected - same thing with mapM (hence why I custom wrote the accumulators). – Ixmatus Sep 4 '12 at 3:35
1  
See also stackoverflow.com/a/12229728/805266 . There is a separate discipline of functional-level programming, which is different from pointfree programming in Haskell. And regarding composition - one subtle advantage is that (.) is associative (that is, (f . g) . h == f . (g . h)) while $ is not. For the same reason >=> is a 'better' way to compose monadic computations than >>=. This makes refactoring easier, as you can always extract a part of long composition chain into a separate function. – nponeccop Sep 4 '12 at 11:59
show 1 more comment

Too much IO — most of this code could be pure. Seed the pure random generator with crypto source, you don't need to read from it every time, if you really want crypto source for this. tabulay' and tabulax' look suspiciously like map. symb in characterMap could be written with string notation.

share|improve this answer
That was my suspicion (re: IO) but I noticed every time I tried to pass the integer from randVal I would get type errors with regards to "expected type Int, got IO Int instead"... Most of the information on the internet I read said I could pass IO Int to functions expecting Int as long as I was inside of a do block. Could you provide some examples of how I might do it the way you are talking about? – Ixmatus Sep 3 '12 at 16:44
@lxmatus: The type error is because you can't escape from IO (well, without using unsafe functions). What I'm proposing is removing the need of keeping the RNG in IO: RNG by nature is pure, the only thing you're using IO for is reading from urandom or whatnot. Take a look at System.Random instead of using Crypto.Random. You can initialise the generator from main, using external seed (like urandom or just time, really) and then pass it along to pure functions. I'll write you an example in a bit. – Cat Plus Plus Sep 3 '12 at 17:33
The only problem with System.Random is that it isn't Cryptographically secure random number generation... That's what I want to achieve with the tabula program. – Ixmatus Sep 3 '12 at 17:47
1  
@lxmatus: What do you need SPRNG for? Oh, passwords. Anyway, you can seed from secure source and then use pure generator and it'll have pretty much the same effect. – Cat Plus Plus Sep 3 '12 at 17:52
Hmmm, everything I've played around with so far has produced nothing but type errors :-/ if you have any free time I would love (even just small) an example to help me a long. – Ixmatus Sep 3 '12 at 19:27

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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