Write Yourself a Scheme in 48 Hours/Creating IO Primitives

Our Scheme can't really communicate with the outside world yet, so it would be nice if we could give it some I/O functions. Also, it gets really tedious typing in functions every time we start the interpreter, so it would be nice to load files of code and execute them.

The first thing we'll need is a new constructor for s.  s have a specific type signature that doesn't include the IO monad, so they can't perform any IO. We want a dedicated constructor for primitive functions that perform IO:

While we're at it, let's also define a constructor for the Scheme data type of a port. Most of our IO functions will take one of these to read from or write to:

A is basically the Haskell notion of a port: it's an opaque data type, returned from   and similar IO actions, that you can read and write to.

For completeness, we ought to provide  methods for the new data types:

This'll let the REPL function properly and not crash when you use a function that returns a port.

We also need to update apply, so that it can handle :

We'll need to make some minor changes to our parser to support. Since Scheme files usually contain several definitions, we need to add a parser that will support several expressions, separated by whitespace. And it also needs to handle errors. We can reuse much of the existing infrastructure by factoring our basic readExpr so that it takes the actual parser as a parameter:

Again, think of both  and   as specializations of the newly-renamed. We'll be using  in our REPL to read single expressions; we'll be using   from within load to read programs.

Next, we'll want a new list of IO primitives, structured just like the existing primitive list:

The only difference here is in the type signature. Unfortunately, we can't use the existing primitive list because lists cannot contain elements of different types. We also need to change the definition of  to add our new primitives:

We've generalized makeFunc to take a constructor argument, and now call it on the list of  in addition to the plain old primitives.

Now we start defining the actual functions. is a very thin wrapper around apply, responsible for destructuring the argument list into the form apply expects:

wraps the Haskell function, converting it to the right type and wrapping its return value in the   constructor. It's intended to be partially-applied to the,   for   and   for  :

also wraps the equivalent Haskell procedure, this time :

(named to avoid a name conflict with the built-in read) wraps the Haskell  and then sends the result to , to be turned into a   suitable for Scheme:

Notice how  is of type   yet   is of type , so they both need to be converted (with   and  , respectively) to the   monad. Only then can they be piped together with the monadic bind operator.

converts a  to a string and then writes it out on the specified port:

We don't have to explicitly call show on the object we're printing, because  takes a value of type. It's calling show for us automatically. This is why we bothered making  an instance of  ; otherwise, we wouldn't be able to use this automatic conversion and would have to call   ourselves. Many other Haskell functions also take instances of, so if we'd extended this with other IO primitives, it could save us significant labor.

reads the whole file into a string in memory. It's a thin wrapper around Haskell's, again just lifting the IO action into an   action and wrapping it in a   constructor:

The helper function  doesn't do what Scheme's load does (we handle that later). Rather, it's responsible only for reading and parsing a file full of statements. It's used in two places:  (which returns a list of values) and   (which evaluates those values as Scheme expressions).

then just wraps that return value with the  constructor:

Implementing the actual Scheme  function is a little tricky, because   can introduce bindings into the local environment. Apply, however, doesn't take an environment argument, and so there's no way for a primitive function (or any function) to do this. We get around this by implementing  as a special form:

Finally, we might as well change our  function so that instead of evaluating a single expression from the command line, it takes the name of a file to execute and runs that as a program. Additional command-line arguments will get bound into a list  within the Scheme program:

That's a little involved, so let's go through it step-by-step. The first line takes the original primitive bindings, passes that into, and then adds a variable named   that's bound to a   containing   versions of all but the first argument. (The first argument is the filename to execute.) Then, it creates a Scheme form, just as if the user had typed it in, and evaluates it. The result is transformed to a string (remember, we have to do this before catching errors, because the error handler converts them to strings and the types must match) and then we run the whole  action. Then we print the result on. (Traditional UNIX conventions hold that  should be used only for program output, with any error messages going to  . In this case, we'll also be printing the return value of the last statement in the program, which generally has no meaning to anything.)

Then we change main so it uses our new  function. Since we no longer need a third clause to handle the wrong number of command-line arguments, we can simplify it to an if statement: