Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Rust for Clojurists

Contents

Why care about Rust?

You already write software in Clojure. It pays your bills. You enjoy it. You're in an industry reaping disproportionate benefit from loose money policies, leading to a trend-chasing culture of overpaid nerds making web apps. You feel guilty about this, but there is nothing you can do about it because you have no other talents that a rational person would pay you for.

Learning Rust will probably not do much to solve that problem for you. It won't assist you in making the ontological leap from a tired stereotype into something sentient and real. You will remain a replaceable silhouette with no discernible identity. It might even exacerbate the problem. However, it will give you a useful tool for writing low-level software.

Let's start by reaffirming why we love Clojure:

  • Expressiveness (Lisp syntax, functional programming)
  • Interoperability (hosted on the JVM and JavaScript)
  • Concurrency (pmap/pvalues, atoms/agents/refs, core.async)

Now let's think about Clojure’s weaknesses:

  • Performance (fast for a dynamic lang, but slow for a compiled lang)
  • Safety (would you program avionics or pacemakers in it?)
  • Embeddability (garbage collected, requires an external runtime)

Rust’s strengths are Clojure’s weaknesses, and vice-versa. Rust isn’t as expressive or interoperable, and its concurrency story isn’t as complete. That said, it’s much better for performance or safety critical needs, and it can be embedded inside other programs or on very limited hardware.

Many people try to compare Rust to Go, but this is flawed. Go is an ancient board game that emphasizes strategy. Rust is more appropriately compared to Chess, a board game focused on low-level tactics. Clojure, with its high-level purview, is a better analogy to the enduring game of stones.

The Toolchain

With Clojure, we typically start by installing Leiningen, which builds projects and deploys libraries to clojars.org. The project.clj file at the root of a project specifies metadata like dependencies. One elegant aspect of Clojure we take for granted is that it is just a library, so projects can specify in this file what version of Clojure to use just like any other library.

With Rust, we start by installing Cargo, which builds projects and deploys libraries to crates.io. The Cargo.toml file at the root of a project specifies metadata like dependencies. Rust takes the more traditional approach of being bundled with its build tool; it isn't a library and its compiler can't be embedded into programs for REPL-driven development.

Creating a New Project

With Clojure, we start an app with lein new app hello-world, which creates a project containing this:

(ns hello_world.core
  (:gen-class))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

With Rust, we start an app with cargo new hello_world --bin, which creates a project containing this:

fn main() {
    println!("Hello, world!");
}

As you can see, the process is basically identical, and apart from syntactic differences, they both start you off with the same main function. With Cargo, if you leave out the "--bin", it will create a library instead. Either way, be sure to think hard about your project's name. A name like "Rust" ensures many opportunities for clever puns that will surely never get tiresome or old.

Modules

While the Rust project doesn't start off with any equivalent to Clojure's namespace declaration, once you move beyond a single source file you'll need to use it. This isn't C or C++, where you just include files like a caveman. Rust separates code into modules, and each source file is automatically given one based on the file name. We can make a functions in a separate file like this:

// utils.rs

pub fn say_hello() {
    println!("Hello, world!");
}

pub fn say_goodbye() {
    println!("Goodbye, world!");
}
// main.rs

mod utils;

fn main() {
    utils::say_hello();
    utils::say_goodbye();
}

Rust's mod is similar to Clojure's ns in that it creates a module, but they all go at the top of main.rs instead of in the files that the modules come from. From there, we can just prepend utils:: to the function names to use them. Note that they are declared with pub. Unlike Clojure, Rust makes functions private by default.

Rust's use is similar to Clojure's require in that it brings in an existing module. Here's a slightly modified main.rs, where we are bringing in symbols explicitly so we don't need to alias it, much like Clojure's require does with the :refer keyword:

// main.rs

use utils::{say_hello, say_goodbye};

mod utils;

fn main() {
    say_hello();
    say_goodbye();
}

Read Crates and Modules to learn more.

Crates

As you know, languages with their own package managers usually have a special format for their libraries with their own unique name. Python has its "eggs" and Ruby has its "gems". Clojure has the disadvantage of being on an existing ecosystem, so it couldn't invent its own format; it uses the same boring "jars" as other JVM languages.

Thankfully, Rust does not have this problem, and it chose to call its format "crates". This reflects the language's industrial roots and the humble, blue collar town of its sponsor: Mountain View, California. To use a crate, you add it to Cargo.toml much like you would with project.clj. Here's what mine looks like after adding the time crate:

[package]

name = "hello_world"
version = "0.0.1"
authors = ["oakes <zsoakes@gmail.com>"]

[dependencies.time]

time = "0.1.2"

To use it, we first need to declare the crate at the top of main.rs:

// main.rs

extern crate time;

use utils::{say_hello, say_goodbye};

mod utils;

fn main() {
    say_hello();
    say_goodbye();
}

Then, in the file we want to use it in, we'll bring it in with use:

// utils.rs

use time;

pub fn say_hello() {
    println!("Hello, world at {}!", time::now().asctime());
}

pub fn say_goodbye() {
    println!("Goodbye, world at {}!", time::now().asctime());
}

Read Crates and Modules to learn more.

Types

Until now, we've avoided seeing types because none of our functions take arguments. Rust is statically typed. The upside is, you will curse it at compile-time instead of at runtime. The downside is, "exploratory programming" means exploring how to convince the compiler to let you try an idea. Let's modify our functions so we pass the "Hello" or "Goodbye" as an argument:

// main.rs

extern crate time;

use utils::say_something;

mod utils;

fn main() {
    say_something("Hello");
    say_something("Goodbye");
}
// utils.rs

use time;

pub fn say_something(word: &str) {
    let t = time::now();
    println!("{}, world at {}!", word, t.asctime());
}

So, the syntax for arguments is similar to ML, where the name comes first, followed by a colon and then the type. In Rust, statically-allocated strings have the type of &str, which is pronounced as "string slice". Heap-allocated strings have the type of String. This is a distinction you don't find in Clojure or other high-level languages. Read Strings to learn more.

Note that in this latest revision, we also moved the time object into a local variable using let, which should be familiar to a Clojure user. In Rust, you are required to specify the types of top-level functions, but almost never for local variables. Rust has type inference, so it can figure out the type of t on its own. It happens to be Tm.

As the Tm docs indicate, the asctime function returns a TmFmt. Although println! has no idea what that is, it doesn't matter. It implements a trait -- similar to a Clojure protocol -- called Display, which is all that println! needs. This mechanism is used pervasively in Rust. Read Traits to learn more.

References

The distinction in the previous section between stack and heap allocation is worth focusing on. In high-level languages, you can't control which is used, so you never think about it. In C and C++, you have complete control over it, but only at the price of being more error-prone. Rust promises to give you that control while being as safe as high-level languages.

When you have direct control over memory allocation, you also have control over how values are passed to functions. In high-level languages, you normally just pass a value to a function and the language will decide whether to only pass a reference to the value or to pass an entire copy of the value. In Rust you explicitly pass references to values.

That is what the & means in &str. Literal strings are automatically represented as a references, but under normal circumstances things will start their life as a value, and to pass them as a reference you will need to prepend them with &. For example, let's pass the Tm object to the say_something function:

// main.rs

extern crate time;

use utils::say_something;

mod utils;

fn main() {
    let t = time::now();
    say_something("Hello", &t);
    say_something("Goodbye", &t);
}
// utils.rs

use time;

pub fn say_something(word: &str, t: &time::Tm) {
    println!("{}, world at {}!", word, t.asctime());
}

What would happen if we just did say_something("Hello", t);, and change the argument's type to t: time::Tm? The value t will be "moved" into the function, and will no longer be available outside of it. Since say_something("Goodbye", t); is called after, it will throw an error. Read References and Borrowing to learn more.

Mutability

A Clojure programmer will be pleased to find that Rust shares a belief in data being immutable by default. The Tm object in the previous section cannot be mutated -- you'll get a compile error. For example, because it implements the Clone trait, it has a function called clone_from, which lets you replace it with a completely new Tm object. This is obviously a mutation, so if we want to use it, we must declare it with let mut:

// main.rs

extern crate time;

use utils::say_something;

mod utils;

fn main() {
    let mut t = time::now();
    t.clone_from(&time::now_utc());
    say_something("Hello", &t);
    say_something("Goodbye", &t);
}

In that example, the t object is being completely replaced by a new one that uses UTC time instead of local time. Interestingly, the say_something function still cannot mutate it, because references are immutable by default as well. If we wanted to run the clone_from function there, we would have to use a mutable reference:

// main.rs

extern crate time;

use utils::say_something;

mod utils;

fn main() {
    let mut t = time::now();
    say_something("Hello", &mut t);
    say_something("Goodbye", &mut t);
}
// utils.rs

use time;

pub fn say_something(word: &str, t: &mut time::Tm) {
    t.clone_from(&time::now_utc());
    println!("{}, world at {}!", word, t.asctime());
}

The neat thing about this is that you can tell when a function is mutating an argument by simply looking at its type signature. If you don't see &mut, it can't do so (unless it's internally mutable). It could still perform I/O like writing to the disk or requesting a network resource, so it's not necessarily pure in that sense, but at least we know that it's pure vis-à-vis its own arguments.

Nullability

In Clojure, we have the concept of nil to represent the lack of a value. It is convenient, but if we forget to check for it, we get the dreaded NullPointerException. Rust follows in the footsteps of languages like Haskell by doing the same thing it does with mutability: making it explicitly part of the type.

For example, let's say we want the say_something function to let you pass a Tm reference or nothing at all. If you do the latter, it will just create its own Tm object using time::now_utc(). To express this, we have to make it an optional type. That means changing the type to Option<&time::Tm> and changing the value we pass to it like this:

// main.rs

extern crate time;

use utils::say_something;

mod utils;

fn main() {
    let t = time::now();
    say_something("Hello", Some(&t));
    say_something("Goodbye", None);
}
// utils.rs

use time;

pub fn say_something(word: &str, t: Option<&time::Tm>) {
    if t.is_some() {
        println!("{}, world at {}!", word, t.unwrap().asctime());
    } else {
        println!("{}, world at {}!", word, time::now_utc().asctime());
    }
}

So, if we actually want to pass a value, we surround it with Some(...), and if we want to pass the equivalent of Clojure's nil, we pass in None. Then, in say_something, we can check if t contains a value using the is_some function, and if so, we call unwrap on it to get its value.

This may seem like a lot of work compared to just using nil, but the advantage is that NullPointerExceptions are impossible. We are forced by the compiler to check if it contains a value. Additionally, it has the same advantage that &mut has in the previous section; just by looking its type signature, we know which arguments allow no value to be passed.

Pattern Matching

In Clojure, we can get very powerful pattern matching capabilities using the core.match library. Rust has a similar mechanism baked into the language. This can be used to simplify complicated conditional statements using the match keyword. Read Match to learn more.

For our purposes, pattern matching can help us make our if statement safer. In the previous section, say_something is not very idiomatic, because it manually checks t.is_some() and calls t.unwrap(). It is much better to use the if let syntax like this:

// utils.rs

use time;

pub fn say_something(word: &str, t: Option<&time::Tm>) {
    if let Some(t_ptr) = t {
        println!("{}, world at {}!", word, t_ptr.asctime());
    } else {
        println!("{}, world at {}!", word, time::now_utc().asctime());
    }
}

Clojure, of course, has its own if-let, and the concept is very similar. The only difference is that we must use pattern matching to pull the value out of the option type. That's what Some(t_ptr) = t is doing. Pattern matching is used pervasively in Rust for everything from error handling to destructuring. Read Patterns to learn more.

Expressions

In Clojure, everything is an expression, which means we can embed code inside of code without any restriction. In Rust, it's not quite as pervasive, but nonetheless almost everything is an expression. The only things you've run into that can't be expressions are declarations, such as mod, use, fn, and let.

What about if statements? In the previous section, say_something is deliberately verbose. There is clearly no benefit to writing redundant code like the calls to println! beyond ensuring one's own job security. In Rust, if statements are expressions, so we can just embed it into a let statement like this:

// utils.rs

use time;

pub fn say_something(word: &str, t: Option<&time::Tm>) {
    let t_val = if let Some(t_ptr) = t {
        *t_ptr
    } else {
        time::now_utc()
    };
    println!("{}, world at {}!", word, t_val.asctime());
}

Here, we are making the local variable t_val, which will contain either the value inside t, or a new object if t is None. Notice the * before t_ptr. This is doing the opposite of & by grabbing the value that the reference is referring to. We need to do this because time::now_utc() returns a value, and we need to make sure both return the same type.

Also notice that neither expression in our if statement ends with a semicolon. Semicolons are used to demarcate statements. To return a value, we just write an expression without a semicolon. This is similar to what we do in Clojure. When we want to return a value, we simply put it at the end. Read Expressions vs. Statements to learn more.

Note that the same thing is done to return a value at the end of a function. If we wanted say_something to return our Tm object, all we need to do is indicate that in the type signature and then put t_val at the end of the function:

// utils.rs

use time;

pub fn say_something(word: &str, t: Option<&time::Tm>) -> time::Tm {
    let t_val = if let Some(t_ptr) = t {
        *t_ptr
    } else {
        time::now_utc()
    };
    println!("{}, world at {}!", word, t_val.asctime());
    t_val
}

Macros

You may have wondered this entire time why println! ends with a bang. In Clojure, it is idiomatic to do this for functions that are side-effecting. In Rust, it is the compiler-enforced syntax for macros. Users of Lisp dialects like Clojure are certainly fond of their macros, as there is a tremendous power, simplicity, and hubristic feeling of personal superiority they afford due to their homoiconic syntax.

Rust is not homoiconic, and unsurprisingly the macro system isn't as powerful. Their primary purpose is similar to that of C macros: to reduce code duplication through symbol replacement. Unlike C macros, however, they are hygenic. Read Macros to learn more. If you are looking for the ability to run arbitrary code at compile-time, you may need to write a compiler plugin instead.

Learn More

There is much more to learn about Rust from here. We haven't touched on lifetimes, the mechanism for achieving memory safety without garbage collection. We haven't looked at FFI, the mechanism for introducing segfaults and stack corruption into your program. The Rust Book, which I've been linking to all along, is a great next step for the reader.

oakmac commented Jan 9, 2015

Thank you for writing this :)

Thanks, interesting language features, and I appreciate the tone.

A nice intro for the Rust newbie with clear explanations and code.

I'd like to offer one improvement: is_some() and unwrap() are almost never used in idiomatic Rust because that is the type hole in the null-based languages that Rust discourages: unwrap() is type-unsafe and will panic if the value happens to be None (which it can't here because we just checked, but in practice there could be many levels of code between check and unwrapping). The best way to go is pattern-matching:

pub fn say_something(word: &str, t: Option<&time::Tm>) {
    match t {
        Some(t_actual) =>
            println!("{}, world at {}!", word, t_actual.asctime()),
        None =>
            println!("{}, world at {}!", word, time::now_utc().asctime())
    }
}
Owner
oakes commented Jan 9, 2015

Thanks Franklin, I agree completely. The reason I avoided pattern-matching is because I never introduced it in this guide. I may get around to adding a section for it.

mlusetti commented Jan 9, 2015

As a former C developer I really appreciate your definitions of Pointers and Stack vs Heap, it is by far the simplest and most accurate that I have ever seen.

masklinn commented Jan 9, 2015

Option also provides a whole host of useful method (which pattern-match under the hood but you don't have to know that), which is a nice property of it being an actual type. In this case, it should be possible to use unwrap_or_else to remove the conditional.

Also a nitpick:

In high-level languages, you normally just pass a value to a function and the language will decide whether to only pass a reference to the value or to pass an entire copy of the value. In Rust, as in C and C++, you use pointers for the former.

That's not quite correct for Rust, by default it will move values and the current scope will lose ownership (and not be able to use the value anymore). It will only copy values if they are marked copy. That used to be applied automatically (if all attributes were POD) but it's now opt-in even in trivial cases.

To give an example for @masklinn's point:

Running the following code (Time replaced with String, but the behavior is the same):

fn say_something(word: &str, t: String) {
    println!("{}, world at {}!", word, t);
}

fn main() {
    let t = "world".to_string();

    say_something("Hello", t);
    say_something("Hello", t);
}

The compiler will complain with:

<anon>:9:28: 9:29 error: use of moved value: `t`
<anon>:9     say_something("Hello", t);
                                    ^
<anon>:8:28: 8:29 note: `t` moved here because it has type `collections::string::String`, which is non-copyable
<anon>:8     say_something("Hello", t);
                                    ^
error: aborting due to previous error

Which is another pro for Rust: The error messages from the compiler are most of the time really, really good (and then you get an error with borrowed values... Good luck to you, then. 😄)

Owner
oakes commented Jan 9, 2015

Thanks masklinn and featureenvy. I changed the paragraph at the end of the Pointers section to reflect that point.

Rust takes the more traditional approach of being bundled with its build tool; it isn't a library and its compiler can't be embedded into programs for REPL-driven development.

That's not true, although it may appear so from looking at the installed binaries. Cargo ist just a program written in Rust (you can find it's sources here) that happens to be installed along rustc. And rustc is basically an interface for a whole lot of stuff around librustc (src). This can be used to build a REPL: rusti tries to be just that.

Owner
oakes commented Jan 9, 2015

I am not sure what you are referring to as "not true", killercup. Rust is not pulled from crates.io as a dependency, which is what I meant by "isn't a library". Also, rusti cannot be embedded inside a Rust program, which is what I meant by "can't be embedded into programs for REPL-driven development".

Kimundi commented Jan 9, 2015

Hi! I just read through this, nicely written :)

I have a few comments and nitpicks though:

  • In the "Why care about Rust", you say that Rusts concurrency story is weaker in comparision. That is certainly true in regard to what libraries currently exist, but there is nothing in the language that would prevent it from offering the same level of concurrency support (And it offers a great solution for enforcing memory safety in concurrent code). So, a note saying that this is not a language limitation would be nice :)
  • In the Pointers section, you claim that by implementing Copy, a value that moves would be copied instead, which might be imperformant. However, the truth is that a move and a copy are implemented exactly the same, both do a shallow copy. The reason for a type to not be Copy is not for performance, but because for some types its semantically wrong to logically copy the value. (Eg, by duplicating a handle to a heap allocation, you would get two handles that both try to manage the same memory)
  • In the mutability section, you link to Cell under the description of a "mutable container" as the exception to the immutability rules. While this is not wrong directly, its also not just containers that make use of this facility, and the general term for the feature is "internal mutability".
  • Rusts Mutability story in general is a bit more complicated than just "mutable vs immutable".
    For one, its not a property of a type in general, as is being implied in the Nullability section by comparing it to Option. Its actually based on the concepts of "shared access, which forbids unchecked mutation" and "unique access, which allows unchecked mutation", which both are first class entities in form of the &T and &mut T reference types. However, in practice this handles pretty similar to "mutable vs immutable", so I don't think its really necessary to change anything major.
  • In the Expressions section, you do *t.unwrap() on a Option<&Tm> value. However, this is in direct contradiction to the earlier section where you say that Tm is not copy, because copying out a value from behind a reference is only ever legal if that type is copy. I'm not sure what the current state of Tm actually is, but one the two wrong. :)
  • In the Macro section, you describe Rusts macros as hygienic C macros. However, they are more than that, capable of manipulating the AST and executing arbitrary code at compiletime, and don't do dumb text-only manipulation like C macros (Though most of the advanced features won't be stable for 1.0).
Owner
oakes commented Jan 9, 2015

Kimundi, thanks for the corrections; I removed the sentence about Copy and added the term "internally mutable". Regarding concurrency, I suppose anything can be implemented in any language, so I don't understand the distinction. Regarding macros, I'll adjust it as the language changes but I got the current description from discussions on IRC. I was told that I would need to write a compiler plugin to run arbitrary code at compile time.

@oakes, okay, with that definition you are right. I think I misunderstood that sentence. Let me clarify by saying that the rust compiler itself is a linkable library, and it can be used in a REPL implementation.

Kimundi commented Jan 9, 2015

I don't think you can implement the same kind of concurrency support in any language.

For example, Rust allows you to write a library that safely spawns a few threads that concurrently mutate some value on the parents threads stack. Its hard to define something like this in a language that does not have the concept of mutability, unboxed values, a stack, pointers, or a thread for that matter. (I'm not implying this is the case for Clojure, just as a general example)

Its true that in many languages that would have that problem, you can use FFI bindings to provide such libraries anyway, but then I'd argue that you haven't written the library in the language, but in C. ;)

Anyway, all I'm trying to say is that Rust is on the same layer as C++ and C in regard to the raw ability to interact with the hardware and OS threading system, and thus has no inherent restrictions there, but to me your text read as if it had. Sorry if I'm just misunderstanding what you actually meant, though :)


Regarding macros: The terminology gets mixed up a lot, sadly, but in principle a macro is just a special case of a compiler plugin and works with the same mechanism internally.

Its just that a macro_rules! macro is easier to support and implement, which is why its the only kind of user-definable macro available in 1.0.

But thats just defining one, for using one, the foo!() syntax might end up either invoking a declarative macro defined with macro_rules!, or a procedural macro defined as a compiler plugin.

In any case, I mostly just think that describing Rusts current level of macro support as "hygenic C macros" is selling them short :)

bayan commented Jan 10, 2015

The bit about comparing Rust to Go. Classic. 😆

xitep commented Jan 10, 2015

Very nice! Thank you for this writing!

NoamB commented Jan 10, 2015

Thanks. Great info.

Great intro! Now, anybody interested in writing a Lisp flavored Rust? ;)

thank you !

phiat commented Jan 20, 2015

Very nice!
Artifacts like this make me smile. Excellent bridge and comparison! ... har har on the Go analogy, but to reflect that, Lisp is like the game:
stones are the same (all s-expressions),
surround to capture (enclosing parens/delimiters),
safe stones never get removed (immutability),
stones strengthen when attached (power of composition),
stones are abstract (power of abstractions)

a bit of a stretch for a few, I know... (you can self implode and kill off your own safe, 2-eyed groups, but thats just terrible play)

This was actually very enjoyable to read (haven't finished yet though). Very funny!

hufeng commented Mar 23, 2015

petty cool

l1x commented Jun 20, 2015

Thanks for writing this. I think Clojure & Rust will co-exist in my projects, Clojure is simply too powerful and most of the time I can write code that is fast enough (mostly due to core.async and the simple concurrency primitives). Safety is an interesting question. I usually end up with a very concise codebase where I understand the inner intermediate steps of execution very well. Obviously, compile time type checks are great and you never catch these type bugs otherwise. Embeddability was never a problem.

I really like the concepts in Rust and now that 1.0 is out I can consider replacing OCaml as my default typed language of choice.

@ghost
ghost commented Feb 7, 2016

Safety is an interesting question. I usually end up with a very concise codebase where I understand the inner intermediate steps of execution very well. Obviously, compile time type checks are great and you never catch these type bugs otherwise. Embeddability was never a problem.

@ghost
ghost commented Feb 7, 2016

co-exist in my projects, Clojure is simply too powerful and most of the time I can write code that is fast enough (mostly due to core.async and the...

Under References, changing t to time::Tm as suggested compiles fine with latest Rust.

Lispre commented Apr 6, 2016

Thank you for your analysis

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment