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 created simple app that read ~ delimited txt file, convert it to [String] and display quotes on screen.

  • How would you improve this code? To be more Haskell like...
  • in splitStr (c/=) is faster than (/=c) by 20-50 ns. Why?
  • Any other ways to clear screen in windows console app?
  • If we compile this code as win console app, I assume threadDelay pauses whole app not just outQ function. Right?
  • Can someone point me in right direction how to refactor this code to use someting like JavaScripts setInterval or setTimeout.
  • is there some easy way to catch ctrl+c so I can showCursor on exit?

.

import System.IO
import System.Random
import Control.Concurrent (threadDelay) -- microseconds
import System.Console.ANSI      -- clearScreen


fileName = "quotes.txt"
oneSecond = 1000000
delay = 1   -- sec

splitStr _ [] = []
splitStr c xs = takeWhile (c/=) xs : splitStr c (drop 1 $ dropWhile (c/=) xs)

--clear = putStr "\ESC[2J"      -- not working on windows

outQ list l sec g = do
  clearScreen
  setCursorPosition 0 0    -- row col
  let (index, gen') = randomR (0, l) g
  putStrLn $ list !! index

  threadDelay $ sec * oneSecond
  outQ list l delay gen'  -- works


main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  gen <- newStdGen    
  str <- readFile fileName
  let qlist = splitStr '~' str
  let len = length qlist -1

  outQ qlist len delay gen  
share|improve this question
Re (c/=) -v- (/=c): did you compile using optimisation (-O switch to ghc)? – dave4420 Jul 25 '12 at 8:47
You are right! I test it in ghci!!! Didn't use any -O – CoR Jul 25 '12 at 11:56

2 Answers

up vote 3 down vote accepted

Let me deal with outQ. I see 3 problems there:

  1. stateful computation without a monad (passing the generator state around)
  2. recursion instead of library function (forever from Control.Monad is appropriate)
  3. using !! to repeatedly access lists which is O(N) and thus not a good idea in most cases.

Let's clean up recursive calls so only the changing parts are passed around and constant parts are kept in a closure:

outQ list l sec = foo where
    foo g = do
        clearScreen
        setCursorPosition 0 0    -- row col
        let (index, gen') = randomR (0, l) g
        putStrLn $ list !! index

        threadDelay $ sec * oneSecond
        foo gen'  -- works

It's a good practice to split recursive parts from non-recursive, so you more easier can see which library function to use for recursion:

forever1 f = f >=> forever1 f

outQ list l sec = forever1 foo where
    foo g = do
        clearScreen
        setCursorPosition 0 0    -- row col
        let (index, gen') = randomR (0, l) g
        putStrLn $ list !! index

        threadDelay $ sec * oneSecond
        return gen'

Now we can load ghci Quotes.hs and check :t forever1. It tells us that

forever1 :: Monad m => (b -> m b) -> b -> m c

Using Hoogle we check that there's no standard function with such type, so we have to stick with our own. Now we can get rid of foo:

outQ list l sec = forever1 $ \g -> do
    clearScreen
    setCursorPosition 0 0    -- row col
    let (index, gen') = randomR (0, l) g
    putStrLn $ list !! index

    threadDelay $ sec * oneSecond
    return gen'

As for getting rid of generator passing, there are two options:

  1. To use RandT monad transformer from Control.Monad.Random
  2. To use randomRs function to generate all random numbers outside of outQ

For such simple case it's better to stick with option 2. Note as we don't need to pass anything from previous iteration call to next one, we can now use library function mapM_:

outQ list l sec = mapM_ $ \index -> do
    clearScreen
    setCursorPosition 0 0    -- row col
    putStrLn $ list !! index

    threadDelay $ sec * oneSecond

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  gen <- newStdGen    
  str <- readFile fileName
  let qlist = splitStr '~' str
  let len = length qlist -1
  let rand = randomRs (0, len) gen
  outQ qlist len delay rand

At this point we can convert our list to an immutable array:

outQ array sec = mapM_ $ \index -> do
    clearScreen
    setCursorPosition 0 0    -- row col
    putStrLn $ array ! index

    threadDelay $ sec * oneSecond

listArray' l = listArray (1, length l) l

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  gen <- newStdGen    
  str <- readFile fileName
  let qlist = splitStr '~' str

  let qarray = listArray' qlist
  let rand = randomRs (bounds qarray) gen
  outQ qarray delay rand

At this point I see there's no need in passing both array and rand to outQ:

outQ sec = mapM_ $ \s -> do
    clearScreen
    setCursorPosition 0 0    -- row col
    putStrLn s
    threadDelay $ sec * oneSecond

listArray' l = listArray (1, length l) l

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  gen <- newStdGen    
  str <- readFile fileName
  let qlist = splitStr '~' str

  let qarray = listArray' qlist
  let rand = randomRs (bounds qarray) gen
  let stringsToDisplay = map (qarray !) rand
  outQ delay stringsToDisplay

Now outQ is crystal clear, so we can condense main:

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  qarray <- listArray' <$> splitStr '~' <$> readFile fileName
  map (qarray !) <$> randomRs (bounds qarray) <$> newStdGen >>= outQ delay 

Finally, to make code more readable, we can extract two general functions which may become useful elsewhere:

mapM_interval sec f = mapM_ $ \s -> do
    f s
    threadDelay $ sec * oneSecond

randomElemsFrom qarray = map (qarray !) . randomRs (bounds qarray)

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  qarray <- listArray' <$> splitStr '~' <$> readFile fileName
  randomElemsFrom qarray <$> newStdGen >>= mapM_interval delay outQ

As the delay may be applied to other functions such as forever, mapM without underscore and, forM_ etc, you may want to generalize mapM_interval further:

withInterval sec mapFn innerFn = mapFn $ \s -> innerFn s >> threadDelay (sec * oneSecond)

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  qarray <- listArray' <$> splitStr '~' <$> readFile fileName
  randomElemsFrom qarray <$> newStdGen >>= withInterval delay mapM_ outQ

So the final version is:

import System.Random (randomRs, newStdGen)
import Control.Concurrent (threadDelay) -- microseconds
import System.Console.ANSI (clearScreen, setCursorPosition, setTitle, hideCursor)
import Control.Applicative ((<$>))
import Data.Array (listArray, bounds, (!))
import Data.List.Split (splitOn)

fileName = "quotes.txt"
oneSecond = 1000000
delay = 1   -- sec

--clear = putStr "\ESC[2J"      -- not working on windows

outQ s = do
    clearScreen
    setCursorPosition 0 0    -- row col
    putStrLn s

listArray' l = listArray (1, length l) l

randomElemsFrom qarray = map (qarray !) . randomRs (bounds qarray)

withInterval sec mapFn innerFn = mapFn $ \s -> innerFn s >> threadDelay (sec * oneSecond)

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  qarray <- listArray' <$> splitOn "~" <$> readFile fileName
  randomElemsFrom qarray <$> newStdGen >>= withInterval delay mapM_ outQ

Found yet another improvement:

randomElemsFrom x = map (qarray !) . randomRs (bounds qarray) where
    qarray = listArray' $ splitOn "~" x

main :: IO ()
main = do
  setTitle "Quotes"
  hideCursor             -- catch ctrl+c for showCursor
  liftM2 randomElemsFrom (readFile fileName) newStdGen >>= withInterval delay mapM_ outQ

This version has more separation between monadic and non-monadic code (which is good) but randomElemsFrom was a generally useful function before but now it is tied to the task at hand (which is bad).

share|improve this answer
WoW! Thanks for answer. I just came back from 58 days long vacation :) I didn't quit Haskell! – CoR Oct 24 '12 at 16:34

I'm going to talk about your splitStr function.

First, it's almost the same as splitOn from Data.List.Split in the split package.

  • Run cabal install split
  • Add import Data.List.Split at the start of your code
  • Replace

    let qlist = splitStr '~' str
    

    with

    let qlist = splitOn "~" str
    

But let's suppose you can't use the split package for some reason, and try to improve your splitStr.

splitStr :: Eq a => a -> [a] -> [[a]]
splitStr _ [] = []
splitStr c xs = takeWhile (c/=) xs : splitStr c (drop 1 $ dropWhile (c/=) xs)

We can use span instead of takeWhile and dropWhile. This is clearer and also more efficient as we only have to traverse the list once instead of twice.

splitStr :: Eq a => a -> [a] -> [[a]]
splitStr _ [] = []
splitStr c xs = ys : splitStr c (drop 1 zs)
  where (ys, zs) = split (c/=) xs
share|improve this answer

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.