mirror of
https://github.com/salsa-rs/salsa.git
synced 2025-01-12 08:30:51 +00:00
improve docs
This commit is contained in:
parent
2ddc8032ee
commit
0a2a871d98
4 changed files with 305 additions and 64 deletions
32
FAQ.md
Normal file
32
FAQ.md
Normal 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.
|
||||
|
80
README.md
80
README.md
|
@ -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.
|
||||
|
|
172
examples/hello_world/README.md
Normal file
172
examples/hello_world/README.md
Normal 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.
|
||||
|
85
examples/hello_world/main.rs
Normal file
85
examples/hello_world/main.rs
Normal 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(()));
|
||||
}
|
Loading…
Reference in a new issue