inc_complete/
lib.rs

1//! # Inc-Complete
2//!
3//! inc-complete is a library for incremental compilation supporting serialization from the ground
4//! up.
5//!
6//! In inc-complete, a central `Db` object is used to query and cache the result of _pure_
7//! functions. The functions being pure is key. If there are side-effects performed then
8//! they will not be re-performed when the computation's result is later cached and returned again.
9//!
10//! Before we create the `Db` object however, we need to define the storage type to store all
11//! the computations we want to cache. In inc-complete, each computation is its own type and is
12//! either an input (if it has no dependencies) or an intermediate computation. For this example
13//! we're going to model the following spreadsheet:
14//!
15//! ```text
16//!       [  A  ] [     B    ]
17//! [ 1 ] [ 12  ] [ =A1 + 8  ]
18//! [ 2 ] [  4  ] [ =B1 + A2 ]
19//! ```
20//!
21//! We will have two inputs: `A1` and `A2`, and two intermediates: `B1` and `B2` where
22//! `B1` depends on `A1` and `B2` depends on `B1` and `A2` directly, and `A1` transitively.
23//! All computations in inc-complete correspond to a struct representing that computation.
24//! This struct often holds the arguments for the corresponding function to perform but in
25//! this case none of our cells need any parameters besides the values of other cells to query.
26//!
27//! For each input type we'll derive `Input` and give it a unique id, along with 
28//! the output type of the function retrieving the input value, and the storage type we'll store
29//! all the metadata in - we'll define this type later on.
30//! 
31//! Let's start by defining our input types:
32//!
33//! ```
34//! # struct MySpreadsheet;
35//! use inc_complete::{ Input, define_input };
36//!
37//! // All computation types must implement Debug and Clone.
38//! // Each input type must additionally implement `Input`, which requires an additional
39//! // `inc_complete` attribute to specify the unique id of the computation, its output type,
40//! // and the storage type to store it in.
41//! ##[derive(Input, Debug, Clone)]
42//! ##[inc_complete(id = 0, output = i32, storage = MySpreadsheet)]
43//! struct A1;
44//!
45//! ##[derive(Input, Debug, Clone)]
46//! ##[inc_complete(id = 1, output = i32, storage = MySpreadsheet)]
47//! struct A2;
48//! ```
49//!
50//! Next, let's define the intermediate computations and the functions to compute them.
51//! For these we just need to define the computation type and use the `intermediate` macro 
52//! on a function to perform that computation. Note that this function should have
53//! exactly two arguments: the computation type itself (which may be a struct containing
54//! more arguments to use if needed), and a `DbHandle` containing your storage type:
55//!
56//! ```
57//! # use inc_complete::{ Input, define_input, Storage, impl_storage };
58//! # #[derive(Debug, Input, Clone)]
59//! # #[inc_complete(id = 0, output = i32, storage = MySpreadsheet)]
60//! # struct A1;
61//! # #[derive(Debug, Input, Clone)]
62//! # #[inc_complete(id = 1, output = i32, storage = MySpreadsheet)]
63//! # struct A2;
64//! # use inc_complete::storage::SingletonStorage;
65//! # #[derive(Storage, Default)]
66//! # struct MySpreadsheet {
67//! #     a1: SingletonStorage<A1>,
68//! #     a2: SingletonStorage<A2>,
69//! #     b1: SingletonStorage<B1>,
70//! #     b2: SingletonStorage<B2>,
71//! #     #[inc_complete(skip)]
72//! #     my_metadata: String,
73//! # }
74//! use inc_complete::{ intermediate, define_intermediate };
75//!
76//! // All Computation types must implement Debug & Clone
77//! # #[derive(Debug, Clone)]
78//! # struct B1;
79//!
80//! ##[derive(Debug, Clone)]
81//! struct B2;
82//!
83//! ##[intermediate(id = 2)]
84//! fn compute_b1(_ctx: &B1, db: &inc_complete::DbHandle<MySpreadsheet>) -> i32 {
85//!     // These functions should be pure but we're going to cheat here to
86//!     // make it obvious when a function is recomputed
87//!     println!("Computing B1!");
88//!     db.get(A1) + 8
89//! }
90//!
91//! ##[intermediate(id = 3)]
92//! fn compute_b2(_ctx: &B2, db: &inc_complete::DbHandle<MySpreadsheet>) -> i32 {
93//!     println!("Computing B2!");
94//!     db.get(B1) + db.get(A2)
95//! }
96//! ```
97//!
98//! Ceremony aside - this code should be relatively straight-forward. We `get` the value of
99//! any sub-computations we need and the `DbHandle` object automatically gives us the most
100//! up to date version of those computations - we'll examine this claim a bit closer later.
101//!
102//! Now we can define the actual storage type to hold all our computations. 
103//!
104//! ```
105//! # #[derive(Debug, Clone)]
106//! # struct A1;
107//! # #[derive(Debug, Clone)]
108//! # struct A2;
109//! # #[derive(Debug, Clone, PartialEq, Eq, Hash)]
110//! # struct B1;
111//! # #[derive(Debug, Clone, PartialEq, Eq, Hash)]
112//! # struct B2;
113//! # use inc_complete::storage::SingletonStorage;
114//! # use inc_complete::define_input;
115//! # define_input!(0, A1 -> i64, MySpreadsheet);
116//! # define_input!(1, A2 -> i64, MySpreadsheet);
117//! # use inc_complete::define_intermediate;
118//! # define_intermediate!(2, B1 -> i64, MySpreadsheet, |_b1, db| {
119//! #     // These functions should be pure but we're going to cheat here to
120//! #     // make it obvious when a function is recomputed
121//! #     println!("Computing B1!");
122//! #     db.get(A1) + 8
123//! # });
124//! # // Larger programs may wish to pass an existing function instead of a closure
125//! # define_intermediate!(3, B2 -> i64, MySpreadsheet, |_b2, db| {
126//! #     println!("Computing B2!");
127//! #     db.get(B1) + db.get(A2)
128//! # });
129//! use inc_complete::{ Storage, impl_storage };
130//!
131//! ##[derive(Storage, Default)]
132//! struct MySpreadsheet {
133//!     a1: SingletonStorage<A1>,
134//!     a2: SingletonStorage<A2>,
135//!     b1: SingletonStorage<B1>,
136//!     b2: SingletonStorage<B2>,
137//!
138//!     // If you need to store non-computation data you can use the `skip` attribute.
139//!     // Just be careful since changes to this data will not be tracked by inc-complete,
140//!     // it is possible to break incrementality! To avoid this, avoid mutating any `skip`
141//!     // fields in the middle of an incremental computation.
142//!     #[inc_complete(skip)]
143//!     my_metadata: String,
144//! }
145//! ```
146//!
147//! In this example, we're using `SingletonStorage` for all of our
148//! computations because all of `A1`, `A2`, `B1`, and `B2` are singleton values like `()` with
149//! only a single value in their type. This lets us store them with an `Option<T>` instead of a
150//! `HashMap<K, V>`. If you are unsure which storage type to choose, `HashMapStorage<T>`
151//! is a good default. Even if used on singletons it will give you correct
152//! behavior, just with slightly worse performance than `SingletonStorage<T>`.
153//!
154//! With that out of the way though, we can finally create our `Db`, set the initial values for our
155//! inputs, and run our program:
156//!
157//! ```
158//! # #[derive(Debug, Clone)]
159//! # struct A1;
160//! # #[derive(Debug, Clone)]
161//! # struct A2;
162//! # #[derive(Debug, Clone, PartialEq, Eq, Hash)]
163//! # struct B1;
164//! # #[derive(Debug, Clone, PartialEq, Eq, Hash)]
165//! # struct B2;
166//! # use inc_complete::storage::SingletonStorage;
167//! # #[derive(Default)]
168//! # struct MySpreadsheet {
169//! #     a1: SingletonStorage<A1>,
170//! #     a2: SingletonStorage<A2>,
171//! #     b1: SingletonStorage<B1>,
172//! #     b2: SingletonStorage<B2>,
173//! # }
174//! # use inc_complete::{ define_input, impl_storage, define_intermediate };
175//! # // This macro may be replaced with a derive proc macro in the future
176//! # impl_storage!(MySpreadsheet,
177//! #     a1: A1,
178//! #     a2: A2,
179//! #     b1: B1,
180//! #     b2: B2,
181//! # );
182//! # define_input!(0, A1 -> i64, MySpreadsheet);
183//! # define_input!(1, A2 -> i64, MySpreadsheet);
184//! # define_intermediate!(2, B1 -> i64, MySpreadsheet, |_b1, db| {
185//! #     // These functions should be pure but we're going to cheat here to
186//! #     // make it obvious when a function is recomputed
187//! #     println!("Computing B1!");
188//! #     db.get(A1) + 8
189//! # });
190//! # // Larger programs may wish to pass an existing function instead of a closure
191//! # define_intermediate!(3, B2 -> i64, MySpreadsheet, |_b2, db| {
192//! #     println!("Computing B2!");
193//! #     db.get(B1) + db.get(A2)
194//! # });
195//! type SpreadsheetDb = inc_complete::Db<MySpreadsheet>;
196//!
197//! fn main() {
198//!     let mut db = SpreadsheetDb::new();
199//!     db.update_input(A1, 12);
200//!     db.update_input(A2, 4);
201//!
202//!     // Output:
203//!     // Computing B2!
204//!     // Computing B1!
205//!     let b2 = db.get(B2);
206//!     assert_eq!(b2, 24);
207//!
208//!     // No output, result of B2 is cached
209//!     let b2 = db.get(B2);
210//!     assert_eq!(b2, 24);
211//!
212//!     // Now lets update an input
213//!     db.update_input(A2, 10);
214//!
215//!     // B2 is now stale and gets recomputed, but crucially B1
216//!     // does not depend on A2 and does not get recomputed.
217//!     // Output:
218//!     // Computing B2!
219//!     let b2 = db.get(B2);
220//!     assert_eq!(b2, 30);
221//! }
222//! ```
223//!
224//! ...And that's it for basic usage! If you want to delve deeper you can manually implement
225//! `Storage` for your storage type or `StorageFor` to define a new storage type for a single input
226//! (like `SingletonStorage` or `HashMapStorage` which inc-complete defines).
227//!
228//! This example did not show it but you can also use structs with fields in your computations, e.g:
229//!
230//! ```
231//! use inc_complete::{ storage::HashMapStorage, impl_storage, define_intermediate };
232//!
233//! #[derive(Default)]
234//! struct MyStorageType {
235//!     fibs: HashMapStorage<Fibonacci>,
236//! }
237//!
238//! // The trailing `,` is required after each field type
239//! impl_storage!(MyStorageType, fibs: Fibonacci,);
240//!
241//! // a fibonacci function with cached sub-results
242//! #[derive(Debug, Clone, PartialEq, Eq, Hash)]
243//! struct Fibonacci(u32);
244//!
245//! define_intermediate!(0, Fibonacci -> u32, MyStorageType, |fib, db| {
246//!     let x = fib.0;
247//!     if x <= 1 {
248//!         x
249//!     } else {
250//!         // Not exponential time since each sub-computation will be cached!
251//!         // In addition to `db.get(mytype)` we can also do `mytype.get(db)`
252//!         Fibonacci(x - 1).get(db) + Fibonacci(x - 2).get(db)
253//!     }
254//! });
255//! ```
256//!
257//! These fields often correspond to parameters of the function being modeled, in
258//! this case the integer input to `fibonacci`.
259//!
260//! ## Serialization
261//!
262//! Serialization can be done by serializing the `Db<S>` object. All cached computations
263//! are stored in the provided storage type `S` so it is up to users to decide how they
264//! want to serialize this storage. As a starting point, it is recommended to tag a field
265//! with `#[serde(default)]` when a new field is added to keep serialization backwards-compatible
266//! when deserializing previous versions of `S`. See the source file `tests/serialize.rs`
267//! as an example of this.
268mod cell;
269#[macro_use]
270pub mod storage;
271pub mod accumulate;
272mod db;
273mod interned;
274
275pub use cell::Cell;
276pub use db::{Db, DbGet, DbHandle};
277pub use storage::{Computation, Run, Storage, StorageFor};
278
279pub use inc_complete_derive::{Input, Storage, intermediate};