Open regexident opened 1 month ago
The proposed API would be extensible as well:
If at some point ascent was to get proper support for re-entrant, differential programs, then AscentProgram
could be extended like so:
impl AscentProgram {
fn facts(&self) -> AscentProgramFacts {
AscentProgramFacts {
edge: HashSet::from_iter(self.runtime.edge),
path: HashSet::from_iter(self.runtime.path),
}
}
/// Extends the existing facts with the contents of `facts`, avoiding duplicates.
fn extend_facts(&mut self, mut facts: AscentProgramFacts) {
self.extend_edge_facts(facts.edge);
self.extend_path_facts(facts.path);
}
/// Extends the existing `edge` facts with the contents of `facts`, avoiding duplicates.
fn extend_edge_facts(&mut self, mut facts: HashSet<(i32, i32)>) {
for edge in &self.runtime.edge {
facts.remove(edge);
}
self.runtime.edge.extend(facts.edge);
}
/// Extends the existing `path` facts with the contents of `facts`, avoiding duplicates.
fn extend_path_facts(&mut self, mut facts: HashSet<(i32, i32)>) {
for path in &self.runtime.path {
facts.remove(path);
}
self.runtime.path.extend(facts.path);
}
/// Runs the program to completion, retaining its state.
pub fn run_mut(&mut self) {
self.runtime.run();
}
}
Let say for some reason your program's edge
relations were coming in
one by one, and you wanted to query the paths at each epoch:
fn main() {
let mut prog = AscentProgramBuilder::default()
.build();
let edges = vec![(1,2), (3,4), (2,4), (2,3)];
for edge in edges {
prog.extend_edge_facts(HashSet::from_iter([edge]));
prog.run_mut();
let facts = prog.facts();
println!("{}", facts.path);
}
}
Again, the API prevents —the user from accidents by only allowing actions that keep the program valid, while also allowing for re-entrancy.
Having just stumbled upon this comment https://github.com/s-arash/ascent/issues/30#issuecomment-1986937702, according to which appears that the leaky abstraction of the current API is intentional:
I would thus instead like to propose introducing an additional higher-level and fail-safe abstraction around the existing API that would serve the needs of 80% of users, while still providing access to the current lower-level and more error-prone API as an escape hatch for the remaining 20% of users who really need full control over the program and its internal state:
Steps
ascent!
/AscentProgram
toascent_runtime!
/AscentProgramRuntime
.ascent!
/AscentProgram
.AscentProgramFacts
ascent_run!
be based on the newascent!
, rather thanascent_runtime!
, as it is currently.API Example
Name bike-shedding aside the new fail-safe wrapper API generated from an
ascent! { … }
program with relationsedge
andpath
…Program's full code snippet
```rust use ascent::ascent; ascent! { relation edge(i32, i32); relation path(i32, i32); path(x, y) <-- edge(x, y); path(x, z) <-- edge(x, y), path(y, z); } ```… would look and work like this:
The above would then be used like this:
By either initializing the initial facts step by step:
Or by passing fully initialized initial facts all at once:
(btw, the literal initialization of relations above is something that the current API doesn't allow due to the private fields in
AscentProgram
)One could even go a step further and provide a builder API that would make things even shorter, while also improving ergonomics by accepting not just actual sets but arbitrary
T: IntoIterator
, which the actual sets then get collected from internally, thus preserving proper set semantics:(Keep in mind that if this simplified 80% API doesn't cut it and you need the full control of
AscentProgramRuntime
, then you can still just useascent_runtime!
instead ofascent!
.)Usability/Safety benefits of the high-level API
The "high-level" API provides a fail-safe and straight-forward API that's effectively impossible to use incorrectly, as there is …
::from_facts(…)
, passing in a set of initial facts..run()
, consuming the program, returning a set of final facts.There are no other methods, nor do there need to be any.
Comparison against the existing API
Now, looking at what's currently necessary to initialize and run an equivalent program … ```rust fn main() { let mut prog = AscentProgram::default(); prog.edge = vec![(1, 2), (2, 3)]; prog.run(); println!("path: {:?}", prog.path); } ``` … one might wonder "fine, but what did we gain by all this? What's wrong about the last snippet? It's short, it's clean." But it's also misleading. Why? Because unlike the "new" API it allows for all kinds of incorrect usages and has a massive API surface with no clear guidance as to which parts of it are actually intended for user access. ## Usability/Safety issues of the low-level API The current "low-level" API can easily be used incorrectly by … - … passing in relations with duplicates, violating Datalog's set semantics. - … messing with the program's internal state, possibly corrupting it. - … removing or mutating facts between repeated runs of the program. The new API does not suffer from any of these.