Input & Output; Reading Files, Writing Files, Files Files Files!

Posted on November 28, 2016 by Adam Wespiser

Theirs not to make reply, theirs not to reason why, theirs but to do and die. Alfred Tennyson

Input Output

Evaluating S-Expressions as pure functions is conceptually simple: reduce terms, modify environments, and return the result. However, if usefulness is an aim, we must introduce side effects that model realworld interactions, namely reading and writing files. Haskell has already tackled this awkward squad via the IO monad, which we can use for our Scheme.

Working with IO inside Eval

Eval is defined with IO inside our monad transformer stack:

newtype Eval a = Eval { unEval :: ReaderT EnvCtx IO a }                            
  deriving (Monad, Functor, Applicative, MonadReader EnvCtx,  MonadIO)             

Which results in EnvCtx -> IO a monadic action. We also have the liftIO helper function:

liftIO :: MonadIO m => IO a -> m a

Thus, we have contained evaluation within an IO monadic action, and have a way, liftIO, to encapsulate IO LispVal into Eval LispVal. However, before we cover the functions provided for IO, there is still one important point, Exceptions.

Exceptions

We covered exceptions and safeExec from Eval. hs in Chpater 4.
To review. safeExec wraps runs a basic “try/catch” over the monadic action of evaluation to make sure exceptions are caught, and control resumed. Now that we are relatively ‘safe’ using IO inside evaluation, let’s take a look at some of input and output functions.

IO Strategy: Goals and approach

We are going to support two main operations: inputting in a script to execute, and reading and writing data files. We approach this by building smaller functions: primitives that compose well. To run scripts, we’ll also need parse and eval functions to ingest the String values inputted from files. That’s pretty much it, and we we are ready for our Haskell definitions.

Running A Script

The first function we need for running a program within Scheme is called slurp, which takes a filename and returns a String. Within slurp, we are using liftIO to interleave IO LispVal and Eval LispVal actions. To avoid complete irresponsibility when reading files, readTextFile ensures the file being read actually exists, and if not, throws an informative IOError message.

slurp :: LispVal  -> Eval LispVal
slurp (String txt) = liftIO $ wFileSlurp txt
slurp val          =  throw $ TypeMismatch "read expects string, instead got: " val

wFileSlurp :: T.Text -> IO LispVal
wFileSlurp fileName = withFile (T.unpack fileName) ReadMode go
  where go = readTextFile fileName

readTextFile :: T.Text -> Handle -> IO LispVal
readTextFile fileName handle = do
  exists <- doesFileExist $ T.unpack fileName
  if exists
  then (TIO.hGetContents handle) >>= (return . String)
  else throw $ IOError $ T.concat [" file does not exits: ", fileName]

We are going to need a few more helper functions to run a program.
parse is defined in Eval. hs. For an inputed String, a LispVal representing the parsed structure is returned.

parseFn :: LispVal -> Eval LispVal
parseFn (String txt) = either (throw . PError . show) return $ readExpr txt
parseFn val = throw $ TypeMismatch "parse expects string, instead got: " val

Next, the LispVal can be evaluated using eval. We define a Scheme function eval in the primitive environment, which is just a shadowed version of the Haskell eval function defined throughout Eval. hs.
Putting all of this together we get:

(eval (parse (slurp "test/let.scm")))

which can be later defined as an entry in the standard library !

Reading and Writing Data Files

We’ve covered reading files, lets take a look at writing to files. The situation is a bit complicated, as we need to use String to represent multiple types of LispVal if we want to store data. Fortunately, we have the Show typeclass for LispVal defined in a way that lets us parse/show LispVal values for all data constructors except Fun and Lambda. (See showVal in Prim.hs) Not being able to represent Fun and Lambda won’t hold us back, as they only emerge to represent lambda functions during evaluation or primitive the primitive environment. Let’s take a look at put, which follows the same structure as slurp.

put :: LispVal -> LispVal -> Eval LispVal
put (String file) (String msg) =  liftIO $ wFilePut file msg
put (String _)  val = throw $ TypeMismatch "put expects string in the second argument (try using show), instead got: " val
put val  _ = throw $ TypeMismatch "put expects string, instead got: " val

wFilePut :: T.Text -> T.Text -> IO LispVal
wFilePut fileName msg = withFile (T.unpack fileName) WriteMode go
  where go = putTextFile fileName msg

putTextFile :: T.Text -> T.Text -> Handle -> IO LispVal
putTextFile fileName msg handle = do
  canWrite <- hIsWritable handle
  if canWrite
  then (TIO.hPutStr handle msg) >> (return $ String msg)
  else throw $ IOError $ T.concat [" file does not exits: ", fileName]

There are a couple of differences besides arity, particularly putTextFile making a safety check via hIsWritable, and it should be noted that put returns the String value written. Looking at put, we see the second argument needs to be a String.
We define a primitive function show, defined as Fun $ IFunc $ unop (return . String . showVal)), taking advantage showVal defined in LispVal.hs.
Putting it all together we get:

Repl> (put "tmp1"  (show '(1 2 3)))
"(1 2 3)"
Repl> (parse (slurp "tmp1"))
"(1 2 3)"
Repl> (put "tmp1"  (show (+ 1 2 3)))
"6"
Repl> (parse (slurp "tmp1"))
"6"

Helper Functions

Reading from a non-existent file is one of the most common and preventable IO mistakes, and a good demonstration of a helper function.

fileExists :: LispVal  -> Eval LispVal
fileExists (String txt) = Bool <$> liftIO (doesFileExist $ T.unpack txt)
fileExists val          = throw $ TypeMismatch "read expects string, instead got: " val

fileExists is a mapping of a single LispVal input into a function provided by System.Directory. For any serious use of our Scheme, it would be useful to wrap additional functions from System.Directory for file system manipulation.

wslurp: Files From The Web

We can add functions from Network.Http as primtive function in our Scheme. A simple one, is an extended slurp that downloads websites.

openURL :: T.Text -> IO LispVal
openURL x = do
  req  <- simpleHTTP (getRequest $ T.unpack x)
  body <- getResponseBody req
  return $ String . T.pack body

wSlurp :: LispVal -> Eval LispVal
wSlurp (String txt) =  liftIO  $  openURL txt
wSlurp val = throw $ TypeMismatch "wSlurp expects a string, instead got: " val

This is another exmample of embedding IO into Eval monadic actions.

Conclusion

Sincle we have IO within the monad transformer stack, we can use liftIO to perform than convert IO a to Eval a actions. This is important for reading and writing files, as well as other things, like foriegn function interfaces, or concurrency/parallel support. To prevent unchecked exceptions from crashing the REPL, we use the safeExec function, which runs actions within a try/catch block and displays the result. To read and write from files, we define our Scheme functions using a series of smaller, composable functions. Together, they can run scripts and read/write data files, and perform some basic checks on the file system.

[ Understanding Check ]

Add a new Scheme function appendTo which appends its argument to a file.
Create some more helper functions from System.Directory, something like rmFile, createDirectory. If possible, abstract out as much of the interface as possible.
One interesting addition would be a way to take a list of expressions, then execute each entry in a different thread, returning the results in a list.

homebacknext