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

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.

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.

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:

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.

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.