improve docs

This commit is contained in:
Niko Matsakis 2018-10-02 05:50:38 -04:00
parent 2ddc8032ee
commit 0a2a871d98
4 changed files with 305 additions and 64 deletions

32
FAQ.md Normal file
View file

@ -0,0 +1,32 @@
# Frequently asked questions
## Why is it called salsa?
I like salsa! Don't you?! Well, ok, there's a bit more to it. The
underlying algorithm for figuring out which bits of code need to be
re-executed after any given change is based on the algorithm used in
rustc. Michael Woerister and I first described the rustc algorithm in
terms of two colors, red and green, and hence we called it the
"red-green algorithm". This made me think of the New Mexico State
Question --- "Red or green?" --- which refers to salsa. Although this
version no longer uses colors (we borrowed revision counters from
Glimmer, instead), I still like the name.
## What is the relationship between salsa and an Entity-Component System (ECS)?
You may have noticed that Salsa "feels" a lot like an ECS in some
ways. That's true -- Salsa's queries are a bit like *components* (and
the keys to the queries are a bit like *entitites*). But there is one
big difference: **ECS is -- at its heart -- a mutable system**. You
can get or set a component of some entity whenever you like. In
contrast, salsa's queries define **define "derived values" via pure
computations**.
Partly as a consequence, ECS doesn't handle incremental updates for
you. When you update some component of some entity, you have to ensure
that other entities' components are upated appropriately.
Finally, ECS offers interesting metadata and "aspect-like" facilities,
such as iterating over all entities that share certain components.
Salsa has no analogue to that.

View file

@ -15,15 +15,16 @@ Yehuda Katz, and Michael Woerister.
## Key idea
The key idea of `salsa` is that you define two things:
The key idea of `salsa` is that you define your program as a set
of **queries**. Queries come in two basic varieties:
- **Inputs**: the base inputs to your system. You can change these
whenever you like.
- **Queries**: values derived from those inputs. These are defined via
"pure functions" (no side effects). The results of queries can be
memoized to avoid recomputing them a lot. When you make changes to
the inputs, we'll figure out (fairly intelligently) when we can
re-use these memoized values and when we have to recompute them.
- **Functions**: pure functions (no side effects) that transform your
inputs into other values. The results of queries is memoized to
avoid recomputing them a lot. When you make changes to the inputs,
we'll figure out (fairly intelligently) when we can re-use these
memoized values and when we have to recompute them.
## How to use Salsa in three easy steps
@ -34,63 +35,14 @@ Using salsa is as easy as 1, 2, 3...
later on you can use more than one to break up your system into
components (or spread your code across crates).
2. **Implement the queries** using the `query_definition!` macro.
3. Create the **query context implementation**, which contains a full
listing of all the inputs/queries you will be using. The query
content implementation will contain the storage for all of the
inputs/queries and may also contain anything else that your code
needs (e.g., configuration data).
3. **Implement the query context trait** for your query context
struct, which contains a full listing of all the inputs/queries you
will be using. The query struct will contain the storage for all of
the inputs/queries and may also contain anything else that your
code needs (e.g., configuration data).
Let's walk through an example! This is [the `hello_world`
example](examples/hello_world) from the repository.
To see an example of this in action, check out [the `hello_world`
example](examples/hello_world/main.rs), which has a number of comments
explaining how things work. The [`hello_world`
README](examples/hello_world/README.md) has a more detailed writeup.
### Step 1: Define a query context trait
The "query context" is the central struct that holds all the state for
your application. It has the current values of all your inputs, the
values of any memoized queries you have executed thus far, and
dependency information between them.
```rust
pub trait HelloWorldContext: salsa::QueryContext {
salsa::query_prototype! {
/// The fundamental **input** to the system: contains a
/// complete list of files.
fn all_files() for AllFiles;
/// A **derived value**: filtered list of paths representing
/// jpegs.
fn jpegs() for Jpegs;
/// A **derived value**: the size of the biggest image. To
/// avoid doing actual image manipulating, we'll use the silly
/// metric of the longest file name. =)
fn largest() for Largest;
}
}
```
###
Let's make a very simple, hello-world sort of example. We'll make two inputs,
each of whihc is
## Goals
It tries to hit a few goals:
- No need for a base crate that declares the "complete set of queries"
- Each query can define its own storage and doesn't have to be memoized
- Each module only has to know about the queries that it depends on
and that it provides (but no others)
- Compiles to fast code, with no allocation, dynamic dispatch, etc on
the "memoized hit" fast path
- Can recover from cycles gracefully (though I didn't really show
that)
- Should support arenas and other lifetime-based things without requiring
lifetimes everywhere when you're not using them (untested)
## Example
There is a working `hello_world` example which is probably the best documentation.
More to come when I expand out a few more patterns.

View file

@ -0,0 +1,172 @@
The `hello_world` example is intended to walk through the very basics
of a salsa setup. Here is a more detailed writeup.
### Step 1: Define the query context trait
The **query context** is the central struct that holds all the state
for your application. It has the current values of all your inputs,
the values of any memoized queries you have executed thus far, and
dependency information between them.
In your program, however, you rarely interact with the **actual**
query context struct. Instead, you interact with query context
**traits** that you define. These traits define the set of queries
that you need for any given piece of code. You define them using
the `salsa::query_prototype!` macro.
Here is a simple example of a query context trait from the
`hello_world` example. It defines exactly two queries: `input_string`
and `length`. You see that the `query_prototype!` macro just lists out
the names of the queries as methods (e.g., `input_string()`) and also a
path to a type that will define the query (`InputString`). It doesn't
give many other details: those are specified in the query definition
that comes later.
```rust
salsa::query_prototype! {
trait HelloWorldContext: salsa::QueryContext {
fn input_string() for InputString;
fn length() for Length;
}
}
```
### Step 2: Define the queries
The actual query definitions are made using the
`salsa::query_definition` macro. For an **input query**, such as
`input_string`, these resemble a variable definition:
```rust
salsa::query_definition! {
InputString: Map<(), Arc<String>>;
}
```
Here, the `Map` is actually a keyword -- you have to write it. The
idea is that each query isn't defining a single value: they are always
a mapping from some **key** to some **value** -- in this case, though,
the type of the key is just the unit type `()` (so in a sense this
*is* a single value). The value type would be `Arc<String>`.
Note that both keys and values are cloned with relative frequency, so
it's a good idea to pick types that can be cheaply cloned. Also, for
the incremental system to work, keys and value types must not employ
"interior mutability" (no `Mutex` or `AtomicUsize` etc).
Next let's define the `length` query, which is a function query:
```rust
salsa::query_definition! {
Length(context: &impl HelloWorldContext, _key: ()) -> usize {
// Read the input string:
let input_string = context.input_string().get(());
// Return its length:
input_string.len()
}
}
```
Like the `InputString` query, `Length` has a **key** and a **value**
-- but this time the type of the key is specified as the type of the
second argument (`_key`), and the type of the value is specified from
the return type (`usize`).
You can also see that functions take a first argument, the `context`,
which always has the form `&impl <SomeContextTrait>`. This `context`
value gives access to all the other queries that are listed in the
context trait that you specify.
In the first line of the function we see how we invoke a query:
```rust
let input_string = context.input_string().get(());
```
When you invoke `context.input_string()`, what you get back is called
a `QueryTable` -- it offers a few methods that let you interact with
the query. The main method you will use though is `get(key)` which --
given a `key` -- computes and returns the up-to-date value. In the
case of an input query like `input_string`, this just returns whatever
value has been set by the user (if no value has been set yet, it
returns the `Default::default()` value; all query inputs must
implement `Default`).
### Step 3: Implement the query context trait
The final step is to create the **query context struct** which will
implement your query context trait(s). This struct combines all the
parts of your system into one whole; it can also add custom state of
your own (such as an interner or configuration). In our simple example
though we won't do any of that. The only field that you **actually**
need is a reference to the **salsa runtime**; then you must also
implement the `QueryContext` trait to tell salsa where to find this
runtime:
```rust
#[derive(Default)]
struct QueryContextStruct {
runtime: salsa::runtime::Runtime<QueryContextStruct>,
}
impl salsa::QueryContext for QueryContextImpl {
fn salsa_runtime(&self) -> &salsa::runtime::Runtime<QueryContextStruct> {
&self.runtime
}
}
```
Next, you must use the `query_context_storage!` to define the "storage
struct" for your type. This storage struct contains all the hashmaps
and other things that salsa uses to store the values for your
queries. You won't need to interact with it directly. To use the
macro, you basically list out all the traits and each of the queries
within those traits:
```rust
query_context_storage! {
struct QueryContextStorage for QueryContextStruct {
// ^^^^^^^^^^^^^^^^^^^ ------------------
// name of the type the name of your context type
// we will make
impl HelloWorldContext {
fn input_string() for InputString;
fn length() for Length;
}
}
}
```
The `query_context_storage` macro will also implement the
`HelloWorldContext` trait for your query context type.
Now that we've defined our query context, we can start using it:
```rust
fn main() {
let context = QueryContextStruct::default();
println!("Initially, the length is {}.", context.length().get(()));
context
.input_string()
.set((), Arc::new(format!("Hello, world")));
println!("Now, the length is {}.", context.length().get(()));
}
```
And if we run this code:
```bash
> cargo run --example hello_world
Compiling salsa v0.2.0 (/Users/nmatsakis/versioned/salsa)
Finished dev [unoptimized + debuginfo] target(s) in 0.94s
Running `target/debug/examples/hello_world`
Initially, the length is 0.
Now, the length is 12.
```
Amazing.

View file

@ -0,0 +1,85 @@
use std::sync::Arc;
///////////////////////////////////////////////////////////////////////////
// Step 1. Define the query context trait
// Define a **query context trait** listing out all the prototypes
// that are defined in this section of the code (in real applications
// you would have many of these). For each query, we just give the
// name of the accessor method (`input_string`) and link that to a
// query type (`InputString`) that will be defined later.
salsa::query_prototype! {
trait HelloWorldContext: salsa::QueryContext {
fn input_string() for InputString;
fn length() for Length;
}
}
///////////////////////////////////////////////////////////////////////////
// Step 2. Define the queries.
// Define an **input query**. Like all queries, it is a map from a key
// (of type `()`) to a value (of type `Arc<String>`). All values begin
// as `Default::default` but you can assign them new values.
salsa::query_definition! {
InputString: Map<(), Arc<String>>;
}
// Define a **function query**. It too has a key and value type, but
// it is defined with a function that -- given the key -- computes the
// value. This function is supplied with a context (an `&impl
// HelloWorldContext`) that gives access to other queries. The runtime
// will track which queries you use so that we can incrementally
// update memoized results.
salsa::query_definition! {
Length(context: &impl HelloWorldContext, _key: ()) -> usize {
// Read the input string:
let input_string = context.input_string().get(());
// Return its length:
input_string.len()
}
}
///////////////////////////////////////////////////////////////////////////
// Step 3. Implement the query context trait.
// Define the actual query context struct. This must contain a salsa
// runtime but can also contain anything else you need.
#[derive(Default)]
struct QueryContextStruct {
runtime: salsa::runtime::Runtime<QueryContextStruct>,
}
// Tell salsa where to find the runtime in your context.
impl salsa::QueryContext for QueryContextStruct {
fn salsa_runtime(&self) -> &salsa::runtime::Runtime<QueryContextStruct> {
&self.runtime
}
}
// Define the full set of queries that your context needs. This would
// in general combine (and implement) all the query context traits in
// your application into one place, allocating storage for all of
// them.
salsa::query_context_storage! {
pub struct QueryContextStorage for QueryContextStruct {
impl HelloWorldContext {
fn input_string() for InputString;
fn length() for Length;
}
}
}
// This shows how to use a query.
fn main() {
let context = QueryContextStruct::default();
println!("Initially, the length is {}.", context.length().get(()));
context
.input_string()
.set((), Arc::new(format!("Hello, world")));
println!("Now, the length is {}.", context.length().get(()));
}