assert-rs / predicates-rs

Boolean-valued predicate functions in Rust
docs.rs/predicates
Apache License 2.0
175 stars 29 forks source link

Add `map` combinator to map input values to a predicate #142

Open holly-hacker opened 1 year ago

holly-hacker commented 1 year ago

I am working on a todo app and I use predicates-rs to filter tasks before displaying them to the user. My Task struct contains a id: TaskId field and I want to use predicate::in_hash to filter out tasks that have an id that is within a certain list.

I currently have code that looks roughly like this:

// base predicate that lets everything through
let predicate = predicate::always().boxed(); // BoxPredicate<Task>

if filter_unactionable {
    let tasks_with_uncompleted_dependencies: Vec<TaskId> = task_database.iter_tasks().filter(|| ...).map(|t| t.id).collect();
    let has_uncompleted_dependencies = predicate::in_hash(tasks_with_uncompleted_dependencies); // HashableInPredicate<TaskId>
    predicate = predicate.and(has_uncompleted_dependencies.not()).boxed(); // error, differing types!
}

// other filters here

// `predicate` is returned and later used to filter which tasks to display

This code does not work because my in_hash predicate expects a value of type TaskId while my main predicate expects Task.

To solve this, I'd like a way to "map" values before a predicate evaluates them. This could look like this (API is just a suggestion):

let predicate = predicate::always().boxed(); // BoxPredicate<Task>

if filter_unactionable {
    let tasks_with_uncompleted_dependencies: Vec<TaskId> = task_database.iter_tasks().filter(|| ...).map(|t| t.id).collect();
    let has_uncompleted_dependencies = predicate::in_hash(tasks_with_uncompleted_dependencies); // HashableInPredicate<TaskId>

    // map from Predicate<Task> to PredicateTaskId
    let has_uncompleted_dependencies_mapped = has_uncompleted_dependencies.map(|task: &Task| task.task_id);

    predicate = predicate.and(has_uncompleted_dependencies_mapped.not()).boxed();
}
epage commented 1 year ago

Would you be up for prototyping this within your project, creating extension traits to provide the syntax you want?

This will

holly-hacker commented 1 year ago

I've already created my own predicate in my own project here: https://github.com/holly-hacker/td/commit/4f71543393cb662b01759128028bf3e76447f044#diff-ea57b46e48fccc26585162b2fa81cc63b45aa766280a22afa43c881a356b23cdR197

Usage looks roughly like this

let tasks_with_uncompleted_dependencies = self.database.get_all_tasks()
    .filter(|t| ...)
    .map(|t| t.id().clone())
    .collect::<HashSet<_>>();

let has_uncompleted_dependencies = predicate::in_hash(tasks_with_uncompleted_dependencies);

let has_uncompleted_dependencies = MapPredicate::new(has_uncompleted_dependencies, |task: &Task| task.id());

predicate = predicate.and(has_uncompleted_dependencies.not()).boxed();

I opted to not write an extension trait for my current solution, but an ideal api would look something like this instead:

let has_uncompleted_dependencies = has_uncompleted_dependencies.map(|task: &Task| task.id()));