Schemers - Input

In my CS undergrad right now I've been taking an interesting course on the
Structure of Higher Level Languages. It's quite the eye opening class
and one of the things we are doing there is implementing a Scheme
interpreter in Scheme. I thought it would be fun to do something like a
tutorial such as Write Yourself a Scheme in 48 Hours
to teach others some Rust and create a Scheme interpreter. This series of
blog posts is going to be aimed at people completely new to the language
who have never touched it before. We'll be implementing the R5RS* version of Scheme since it's considered the good version. Don't worry too much
about all the language in the specification. It's just verbose so we
know exactly how to define it in our implementation. The main thing is
that we learn some Rust and have some fun doing it!

A little history on Scheme

Scheme is a derivative of LISP or LISt Processor (jokingly referred to as
'Lots of Irritating Stupid Parentheses' as you will soon come to realize).
LISP was one of the first programming languages created by an MIT professor
name John McCarthy back in the 1950's and first implemented on an old
computer known as the IBM 704. Scheme was a variant developed at MIT in the
1970's. The language itself is simple enough to understand but the implications,
and the things that can be learned from it apply to languages both young and
old today. I'd recommend learning it a bit so you can be familiar with it when
we go through each tutorial. Here is a great
place to start learning it. It's the text I've been using in class and provides
great examples to learn from as well as exercises you can use to learn it.

What we're covering

We won't be writing any Scheme in this tutorial. All we're going to do
is get the beginning of a REPL (Read Eval Print Loop) setup for us to
use. It'll keep taking input from us and printing out what we typed in
and loop until we put in (exit) as input.

Here are the Rust topics we'll cover:

  • Setting up a Rust Environment using Rustup
  • Compiling your first program
  • Cargo.toml files
  • External imports
  • Rust while loops
  • Rust if else statements
  • Rust match statements
  • Rust Result types
  • Mutability vs. Immutability

We'll cover topics in Rust as we need them. By the end of this tutorial
you should have a basic Read Print Loop that we can exit from. With that
in mind let's setup our project.

Let's get setup

Rust has two main tools to actually build your code, rustc which is
the Rust compiler and cargo it's package manager and build system.
We'll need both installed if we want to get anything done. We'll need
cargo to setup our project and have it build our project without us
having to mess around with getting dependencies, linking them, and how
to compile it all. We'll need rustc to actually build the project. The
easiest way, and the one that will be the supported way in the future is
using rustup. Don't let it's beta status scare you. It's fairly
stable! rustup gets us all of these components and installs them for
us. On top of that it also allows us to switch between the three release
channels (stable, beta, and nightly). We'll be working with the stable
compiler for this tutorial. You should still know what all the release
channels are for though! Stable is the last compiler version that the
dev team has marked as, well stable. It shouldn't cause any breakage or
errors. If it does that's really not good and you should file a bug.
Beta is for features that will be stabilized in the next release and
this lets us test them out as a release candidate just to make sure we
can fix any possible breaking changes in our code. Nightly is where the
kiddie gloves come off. It can break at any time, however here you get
to test all the crazy new features before they get considered for
stabilization. Some projects even only work with nightly because they
depend on certain features. Release cycles are every six weeks so you
get to have new features on a consistent quick schedule. It's great!

Alright, so you know about the tools we use and need so let's actually
install them! First up we need to get rustup installed. Without it we
won't be able to get anything working. We'll be doing most of our work
from the command line for this part. Open up your terminal and put this
in:

curl https://sh.rustup.rs -sSf | sh

Just follow the on screen instructions. At the very least you'll want
the stable channel installed and to be your default.

Run the following commands to make sure

rustc --version
cargo --version

Cargo might be output as a nightly with it's version. If rustc comes
out without a beta or nightly tag in it's name then this is the version
of cargo that goes with it. What we're really checking for is that the
compiler and cargo are there and that we can use the tools. At the
time of this writing rustc is on version 1.12.1 but any stable version
after it should work as well. If it's not the stable version then type
the following:

rustup default stable

Then check that it's the stable version. Cool we've got all of our tools now!

Initialize the project

Now that we have our tools installed let's start using them. First thing
we're going to do is create a project for a binary. We're going to call
our project schemers in this tutorial but you can name your project
whatever you want! You'll need to run this command:

cargo new --bin schemers

We're telling cargo to create a new project that is a binary that we
can run, thus the bin flag, and we're going to call it schemers. If
you drop the bin flag it automatically creates a library project. We
won't be covering that in this tutorial but essentially it's a packages
of functions you can write and publish that others can use. We'll be
using one of these libraries in our own project! That's later though.
Let's take a look inside our project directory. It'll look like this:

Cargo.toml
.gitignore
.git
src
main.rs

Let's start with the git portion. cargo automatically initializes
the directory as a git repository and contains a .gitignore file that
contains common things that Rust generates that you wouldn't want to
check into source control. It differs between the binary and the
library. In our case .gitignore only contains the target directory
which is where everything that's compiled is put in.

Cargo.toml is our configuration file where we list our dependencies as
well as provide metadata about the package itself such as the license
used or it's name. We'll come back to this later but if you're curious
take a peak!

The last part is our src directory. This is where our source code
lives and where cargo checks for files to compile so it can invoke rustc to
actually make the binary. main.rs is the entry point for any binary
in Rust and lib.rs is the entry point for any library.

I fought immutability and immutability won

The code is actually setup to do the classic "Hello world!" already.
Just use cargo run in the terminal and it'll compile and print it out. Let's face it
though. It's an old and boring example. Let's actually learn
something by running into compiler errors! I'm not kidding. One of the
fastest ways to learn Rust is to face failure with it head on. You'll
learn more from that, especially since we have some shiny new error
messages!

Okay first let's setup a loop with a variable done that lets us loop
until we change done to true.

Open up your main.rs file with whatever editor you want. You should
change it to look like this:

fn main() {
  let done = false;
  while !done {
    done = true;
  }
}

Let's go through each line before we run this. First you'll see our
function header main. fn tells us that whatever comes next is
a function. main() tells us it's name is main and has no parameters
as input (the ()) and we denote the body of the function with an
opening {. These function headers can be more involved to say
different things and we'll cover them eventually, but for now this is
how you declare a basic function. Now let's look at the next line:

let done = false;

let is the keyword that Rust uses to declare a variable. Although Rust
is strongly typed, unlike languages like Java, we don't need to declare
the type every time we declare a variable. The compiler is pretty smart to
infer what type it is. Sometimes though you'll need to let it know but
those times are few and far between. The = lets us know this is an
assignment of a value on the right to the variable on the left. false
is how we show a boolean in Rust (true being the opposite value). We
denote statements have ended with semicolons like many other C like syntax
languages.

  while !done {
    done = true;
  }

This is the syntax for a while loop. No parentheses needed, just
while then the boolean statement you want to evaluate, in this case
not done. Then we assign true to done! Let's give it a whirl and
compile it. Run the command cargo run and it'll attempt to compile the
code.

Except, it's going to fail. Let's take a look at the error message:

error[E0384]: re-assignment of immutable variable `done`
 --> src/main.rs:4:9
  |
2 |     let done = false;
  |         ---- first assignment to `done`
3 |     while !done {
4 |         done = true;
  |         ^^^^^^^^^^^ re-assignment of immutable variable

I didn't include a crucial detail about this code. When you assign
variable names with let then Rust makes the variable immutable.
Meaning no matter what you do, if you try to do something to change it,
Rust will throw an error. This means if you mean to mutate a variable
you'll get an error if you forgot to make it mutable. You'll also get
a warning if you make a variable mutable and don't actually change it!
This might seem weird if you've never dealt with languages where things
are immutable by default (like Haskell), but it's a great thing trust
me! It clearly lets you know what's changing, what's allowed to change,
and will warn you in the event something that shouldn't happen does. The
compiler is the debugger for your mind. It reduces the need for you to
think about the state of the program and let you do the important things
like writing new features. If you run into errors writing Rust, it'll feel
frustrating at first, but you'll soon see that you'll run into them less
over time. In fact you might even come to love them!

Good points aside, you're probably wondering how to fix this whole problem.
Must be hard right? Nope! We just need to make done a mutable variable.
Change the let statement to say this instead:

let mut done = false;

The mut keyword is how we tell Rust that a variable is mutable and so
we can reassign values to it if we need too. Now if you use cargo run
again you'll see that it compiled and ran. Our program doesn't actually
do anything right now. It's quite useless. Let's change that by getting
ourselves some input from users to print out!

Depend on dependable dependencies

We need a way to get input from users, but we also want them to move
the cursor around, and later on we might want to save that history so
they can go back and use an old command again. We could write our own
implementation, but dealing with display buffers and terminal keys just
doesn't seem like a fun time. I want to write my program not reimplement
what's been implemented! Luckily for us we have
crates.io a site for Rust programmers to upload
packages they've written and a place where others can download them to
use the code in their own! Did I mention you can publish a library using
cargo and that you can also use it to pull in dependencies for you
automatically? Pretty neat huh? Let's get this into our code then!

First up we are going to modify our Cargo.toml file so we can list our
new dependency. We'll be using a library called rustyline a library
based off of the famous readline, minus all of the C code. It has all of
the features I mentioned before and works really well for our REPL. If
you open up your file you'll see something that looks like this:

[package]
name = "schemers"
version = "0.1.0"
authors = ["Michael Gattozzi <mgattozzi@gmail.com>"]

[dependencies]

This is where your metadata and dependencies live. The configuration
file is much more human readable compared to something like JSON or
XML. There's a ton of options you can configure this file with and
I encourage you to look through the documentation
here. However, we
won't be covering it right now. What I want to teach you is how to put
in a new dependency and get it in your code! Add this line to your file:

[dependencies]
rustyline = "1.0.0"

What we're saying is "Cargo add rustyline as a dependency to our code
and use the 1.0.0 version of the package." It makes it really easy to
specify versions and dependencies and when you do cargo run again
it'll pull it automatically from crates.io for you and link it in
automagically. Okay but putting it in our file isn't enough really. It
won't actually be in our codebase. Let's open up main.rs again and add
this to the top of the file:

extern crate rustyline;

Let's look at this a little bit. First extern means we're using code
outside of Rust or we might be exporting code outside of Rust to be
called from other languages or vice versa. In this case though check the
next word crate. This means we'll be using an external library (or
crate in Rust parlance) and that it's name is rustyline. In most of
your use cases you'll be using extern crate to define dependencies.
Don't worry too much about the FFI bit. That's more of a "for your
knowledge" then anything else and to let you know it's possible! Now
do cargo run again. You might notice this time it's pulling in
rustyline and it's dependencies, then compiling it, then your code,
then running it! Neat huh? You didn't even have to tell cargo how to
get it, compile it, or link it to your code. This is one of it's many
strengths that can be leveraged to write code without worrying about the
small details. Let's actually write some code using our new library we
imported!

Change your main.rs to look like this:

extern crate rustyline;
fn main() {
    let mut done = false;
    let mut reader = rustyline::Editor::<()>::new();
    while !done {
        match reader.readline(">> ") {
            Ok(line) =>
                if line == "(exit)" {
                    done = true;
                } else {
                    println!("{}",line);
                },
            Err(e) => println!("Couldn't readline. Error was: {}", e),
        }
    }
}

Okay, what? There are a lot of new things I just threw in your face. Good
news is this is the rest of the code we'll be writing for this tutorial.
Before we run it though let's break it down so you can understand what's
going on, line by line.

extern crate rustyline;
fn main() {

We covered this before but let's reiterate the point:

  • We're importing a crate rustyline for use in this file
  • We've declared a main function so that when we execute the binary we
    know where to start our program's execution
let mut done = false;
let mut reader = rustyline::Editor::<()>::new();

We've seen that first line before, we've created a boolean variable
called done that can change it's value. We've also set it to false.
What's that next line though? Well we've declared a variable reader that
is mutable. Okay, that makes sense, what's all the stuff on the right?
Well Rust borrows this syntax style from C++. What we're telling the
compiler is how to find the method we want it to use. rustyline:: is
saying look in the rustyline crate. Editor is saying use a function
from the Editor struct. A struct in Rust is a way to store various data type
in fields we can access. We can also implement functions that manipulate
or create these structs we've designed. We'll cover this more in depth
in the future but this should be enough to understand what's going on
now. ::<()>:: is a type declaration for the rustyline editor. This
isn't all that important right now for what we're doing, we just need it
there to compile. It might seem like this is a bit hand wavy saying not
to worry about it, really I'm trying to focus on the syntax here and
giving you a basic understanding of concepts and elaborating on them as
we progress. Bear with me in this respect. Overloading you with too much
too fast won't help and only serves to cause frustration. new() is the
important bit here. This function is the one that actually creates an
Editor struct that we can use to get input from the user and assigns
it to the variable reader!

while !done {
    match reader.readline(">> ") {

We covered the while loop earlier in the article. As long as !done
evaluates to true the code inside will continue to loop. The interesting
bit is this match statement here. What is it? If you're familiar with
switch or case statements in other languages it's like that except
slightly more powerful because we can implement pattern matching,
a powerful concept that languages like Haskell have! In this statement
it's saying, use the readline function from our Editor struct and
make it's prompt look like >>. When a user types in input and presses
enter we'll pattern match on whatever data it returns! Nifty huh? Let's
look at the final block of code to understand what happens once we get
user input.

Ok(line) =>
    if line == "(exit)" {
        done = true;
    } else {
        println!("{}",line);
    },
Err(e) => println!("Couldn't readline. Error was: {}", e),

The function readline has a return type of Result<String, ReadLineError>.
ReadLineError is a custom error from the rustyline library. If you've
used Haskell before Result is like the Either monad. If you haven't
then this should help explain things. When we compute things sometimes
we get errors, for instance you divide a number by 0 or something like
that. If the program panicked and crashed every time something like that
happened then as a programmer it would be really hard to deal with
something like that. We wouldn't have control over how to handle the
error. What if the calculation was successful? Then we need to be able
to return that value. However, if a function could fail or work then we
can't return one type because it could be either a failure we would want
to handle ourselves, or the result of a successful computation. That's
where Result comes in! It keeps track of whether it was a failure or
a success. We'll know what type of failure it could be or what type of
success it will be if it returns a value with Result. By pattern
matching on it we can determine what to do if there is an error or if it
worked. We also can unwrap the values inside an Ok (successful
computation) or an Err (unsuccessful computation) and use that value
in another computation! Let's look at the easier case here, Err(e).
What we're saying is if the Result type returns an error bind the
inner data of the error to the variable e. => is the syntax we use
to say "if we match this pattern do what's to the right of me." The , at
the end is how you end each pattern in a match statement. Now let's look at what
happens if we get an error. println! is what's known as a macro in
Rust. They can get pretty crazy with what they can do but in this case
it figures out the input and what to print out to the console! Our first
argument is a String. We'll dive into the nitty gritty of Strings in
the future since it can be a confusing topic to new users of Rust. For
now you need to know that your first argument of input will be printed.
Note the {} in the text. This is where the value of e will be placed
when printed out! Each {} that's in the first argument corresponds to
a variable added as an argument. To make that clear:

let a = "Hello";
let b = "world";
println!("{} {}!", a ,b);
// This prints out "Hello world!"
println!("{} {}!", b ,a);
// This prints out "world Hello!"

You can add as few or as many {} as you want in your printed output,
you'll just need a corresponding variable to fill it. Let's look at our
use case again:

println!("Couldn't readline. Error was: {}", e)

Any time we get an error when getting input for readline it'll print
out "Couldn't readline. Error was: " and it will include the error as
part of the output. You'll run an example to see what this looks like
yourself soon enough!

Let's look at the Ok(line) statement.

Ok(line) =>
    if line == "(exit)" {
        done = true;
    } else {
        println!("{}",line);
    },

What's going on here is "we got input from the user and we've stored it
into the variable line". The statement following it is an if else
statement. Like while, Rust doesn't use the parentheses around what you
want to evaluate as a boolean statement like some other languages. I find
this to be more readable and reminds me of Python's syntax. What we're
saying here is, "If the user has input the string (exit) then make done true
so that the program will exit on the next loop, otherwise print out that
value to the command prompt and get the next input from them." Simple right?

Let's actually run it and try it out! Type type in cargo run and watch
what happens. After a lot of output you should see >> pop up on your
screen! Type in some things and press enter and watch it get put on to
your screen!

>> hello
hello
>> goodbye
goodbye

Let's see if we can get it to print out our error message. Hit Ctrl-c
then Ctrl-d. You should see the following outputs:

>>
Couldn't readline. Error was: Interrupted
>>
Couldn't readline. Error was: EOF

Now type in (exit) your program should close out!

>> (exit)
michael@kotetsujo ~/Code/schemers (git)-[master] %

Cool huh? You've written your first program in Rust and it can read and
evaluate input! I'm going to give you some exercises to try out between
now and the next post. Unlike many other "exercise left to the reader"
tutorials I'll give you an answer in the next post on how to do it in
case you couldn't figure it out. You should really try though because
that's how you'll learn. If you run into difficulty ask on the
#rust-beginners irc channel for help! The community is great and loves
to help new users.

Exercises

Here's what I want you to do:

  • Modify the Err line so that the program exits gracefully on an EOF
    or Interrupted, but prints an error out like before otherwise.
    (Hint: You'll need to modify done here and check for the error
    somehow)
  • What happens when you modify ">> " to be something else? If you
    understand what is happening when you change it, modify it to be
    something you like!
  • What happens when I put in something like " (exit) " to the
    interpreter? What method in the standard
    library
    would get rid of
    the whitespace? Find the method in the linked documentation and use it
    in the interpreter so that " (exit)", "(exit) ", and "(exit)"
    all cause the program to exit.

Conclusion

We've covered a lot, like getting Rust all setup, setting up a project,
a little bit of Rust syntax, mutability, and how to get dependencies
into your project. This is but a sample of what we can do with Rust. We
grazed over some topics because there are situations where it will be
more relevant and worth diving into in depth. Don't worry if you run
into errors or feel frustrated. The community loves helping new users
and we want to make this easier for you to understand by helping you
over that initial hump. The irc channels are a great way to learn and
ask questions on rather than a post and waiting for an answer. It's
a resource you should really use. Ask, we were all Rust beginners once
and we know it can be hard at times. We want to make this experience
easier for you.

Hopefully you learned some things here and will continue to learn Rust!
The next article will be implementing a parser to actually be able to
turn our input into something that we can later evaluate as Scheme code.
We'll also be checking to make sure that the code doesn't have any
syntax errors (at least in terms of () not lining up properly). If you
have comments or questions feel free to ping me on Twitter or open up
issues on the repository itself where all of this code is hosted!

You can find all of the code from this article in the schemers repo here. The next article can be found here.

* It should be noted prior to writing this article I had no idea that
schemers was the name of the website. This project is not associated
with that website at all. We're just using it to reference R5RS.