1
\$\begingroup\$
import System.Environment (getArgs)
import Random
main = do
 args <- getArgs
 let len = length args
 let n = if len < 1 then 10000 else read (args !! 0) :: Int
 let fileName = if len < 2 then show n ++ "-output.txt" else args !! 1
 str <- rndStr n
 writeFile fileName (str)
-- writeFile fileName (rndStr n)
-- Couldn't match expected type `[Char]' with actual type `IO String'
rndStr :: Int -> IO String
rndStr n = sequence . replicate n . randomRIO $ (' ', '~')

This code works. It creates size n file of random chars. That's good.

I have 2 questions:
1. Is my argument sanitizing ok? If not, how to make it better?
2. Swapping writeFile str for writeFile (rndStr n) produces [Char] vs IO String error. Why? Shouldn't it be the same?
I manage this to work by accident, not because I know what I did.

asked Jun 14, 2012 at 3:12
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Congratulations on your progress, :) Welcome to monads

First thing you have to keep in mind is to restrict the time spent in imperative world. So take out as much stuff as you can from the do ..., and refactor them into smaller non-IO code.

import System.Environment (getArgs)
import System.Random
import Control.Monad

Another advice is to avoid magic numbers in your code. If there are any, they should be declared in a highly visible area along with their purpose rather than buried deep in the code.

minLen = 10000
main = do
 args <- getArgs
 writeFile (getFileName args) =<< rndStr (getFileSize args)
 where getFileSize [] = minLen
 getFileSize (x:xs) = read x :: Int
 getFileName [x] = x ++ "-output.txt"
 getFileName (x:y:[]) = y

As you can see, the reason you can't use rndStr directly is that rndStr uses IO. So it cannot be directly used as a function. You can think of it this way. rndStr returns some thing that is wrapped in a box. You need special constructs to unbox it, and the special construct is either <- . You might also notice that main has the same kind of signature.

That isn't a really good analogy, and I am not the best teacher :). So if you really want to understand what happens, it might be better to read any simple monad tutorial.

rndStr :: Int -> IO String
rndStr n = sequence . replicate n . randomRIO $ (' ', '~')

Try this approach if it makes better sense to you. When you start ghci, use this flag

ghci -XImplicitParams

On the prompt after loading your program, try to execute writeFile which was not accepting (rndStr number) earlier, but this time, instead of that expression, replace it by ?check

> writeFile (getFileName ["5"] ) ?check

You will get back some thing like

<interactive>:0:32:
 Unbound implicit parameter (?check::String)
 arising from a use of implicit parameter `?check'
 In the second argument of `writeFile', namely `?check'
 In the expression: writeFile (getFileName ["5"]) ?check
 In an equation for `it': it = writeFile (getFileName ["5"]) ?check

Ignore every thing except the second line, the Unknown implict .. tells you that ghc expected any exprssion in place of ?check would be a string.

Now try finding the type of our expression (rndStr number)

> :t rndStr 5
rndStr 5 :: IO String

As you can see, rndStr number has a different type IO String than the expected String. This is the reason you cant use rndStr number there, and why we have to do all that above.

Note that my statement writeFile (getFileName args) =<< rndStr (getFileSize args) is really same as

do
str <- rndStr (getFileSize args)
writeFile (getFileName args) str

Try to work out how it is so.

answered Jun 14, 2012 at 4:13
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the answer :) Sub questions: 1. I'll divide impure from pure f as much as I can/know. But why are we doing that? It's faster, compiler can optimize that code, less error prone, something else...? 2. you could wrote getFileSize and getFileName at a same indent as main. It would work. But you used where so you don't pollute global name space. Right? Or there's something else to it? \$\endgroup\$ Commented Jun 14, 2012 at 12:39
  • \$\begingroup\$ getFileName [] = show minLen ++ "-output.txt" is needed because of Non-exhaustive patterns in function getFileName \$\endgroup\$ Commented Jun 14, 2012 at 12:56
  • 1
    \$\begingroup\$ @CoR 1) The names pure and impure should give you a hint :). Things that are in the impure land are harder to reuse or test (or even reason about). Always be on the look out for places where a general pattern that can be reused can be isolated. 2) I could have, and I debated it myself. The reason I went with the current is that it is too specific still (using minLen and "-output.txt"). If I can make it general enough, it should be in the top level. 3) yes, I missed it :) \$\endgroup\$ Commented Jun 14, 2012 at 14:30
1
\$\begingroup\$

For learning purposes, I am posting the evolution of the rndStr function I've used above.
Copy/paste code in a file, load it in ghci, and it will work. All comments are in the code. It shows the difference between [IO Char] and IO [Char], Haskell Platform 2011/2012 and (Eq a, Num a) vs Int.

import System.Random
rndChar :: IO Char
rndChar = randomRIO (' ', '~') -- ('a','z'), ('A', 'z'), (' ', '~') all printable chars
-- bad f, don't know how to use [IO Char] 
--2011 str :: Num a => a -> [IO Char]
--2012 str :: (Eq a, Num a) => a -> [IO Char]
str 0 = []
str n = rndChar : str (n-1)
--2011 rndStr1 :: Num a => a -> [IO Char]
--2012 rndStr1 :: (Eq a, Num a) => a -> IO [Char]
rndStr1 n = sequence (str n)
-- sequence :: Monad m => [m a] -> m [a]
-- sequence xs -- sequence xs evaluates all monadic values in list xs, from left to right, and returns a list of "contents" of these monads, placing this list in a monad of same type.
-- "evaluating" can be "performing an action", as in print
rndStr2 n = sequence $ str n -- rndStr2' = sequence $ str n
 where
 str 0 = []
 str n' = randomRIO (' ', '~') : str (n'-1)
rndStr3 n = let -- rndStr3 = let in ... in sequence $ str n
 str 0 = []
 str n' = randomRIO (' ', '~') : str (n'-1)
 in sequence $ str n
--2011 rndStr4'' :: Num a => a -> [IO Char] -- must be wrapped in sequence
--2012 rndStr4'' :: (Eq a, Num a) => a -> [IO Char]
rndStr4'' n = case n of
 0 -> []
 _ -> randomRIO (' ', '~') : rndStr4'' (n-1)
rndStr4 :: (Eq a, Num a) => a -> IO [Char]
rndStr4 n = sequence $ rndStr4'' n
-- should be tail recursion because it returns acc as last f call
rndStr2t n = sequence $ help n []
 where
 help 0 acc = acc -- brake tail recursion
 help x acc = help (x-1) (randomRIO (' ', '~') : acc)
rndStr3t n = let
 help 0 acc = acc
 help n acc = help (n-1) (randomRIO (' ', '~') : acc)
 in sequence $ help n []
--
rndStr8 n = sequence (take n (repeat (randomRIO (' ', '~'))))
rndStr8' n = sequence $ take n $ repeat $ randomRIO (' ', '~')
rndStr9 n = sequence $ replicate n $ randomRIO (' ', '~')
rndStr9' n = sequence . (replicate n) . randomRIO $ (' ', '~')
--replicateM :: Monad m => Int -> m a -> m [a]
--replicateM n action -- performs action n times, return results. 
--rndStr10 :: Int -> IO [Char]
rndStr10 n = replicateM n (randomRIO (' ', '~'))
rndStr10a n = replicateM n $ randomRIO (' ', '~')
--2011 :: Num a => a -> IO [Char]
--2012 :: (Eq a, Num a) => a -> IO [Char]
rfN = [
 rndStr1, rndStr2, rndStr3, rndStr4, 
 rndStr2t, rndStr3t
 ]
-- :: Int -> IO [Char]
rfI = [rndStr8,
 rndStr8',
 rndStr9,
 rndStr9',
 rndStr10]
tf (x:xs) param = do
 print =<< (x param) -- (>>= print)
 if null xs then return () else tf2 xs param
-- usage:
-- tf rfI 10
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
answered Jun 18, 2012 at 18:56
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.