sminez / penrose

A library for writing an X11 tiling window manager
https://sminez.github.io/penrose/
MIT License
1.26k stars 88 forks source link

Adding meta queries to support combinations of queries as queries. #291

Closed favilo closed 4 months ago

favilo commented 7 months ago

Adding structs:

And combinator methods in the Query trait in order to make these usable.

sminez commented 6 months ago

I'm not entirely sure I'm convinced by the ergonomics of this or why this being raised as a PR without first raising an issue to discuss the feature request.

favilo commented 6 months ago

I'm personally using these with an extension trait to make it more ergonomic.

Apologies for not raising an issue first. I've raise #294 just now.

sminez commented 6 months ago

It looks like the structs you are defining aren't being given a consistent set of derives? They should all really be deriving the same common set of Debug, Copy, Clone, PartialEq and Eq.

The compiler issue you're showing here is down to the structs you've defined not having any trait bounds I suspect? Taking the NotQuery as an example:

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NotQuery<Q>(pub Q);

Here Q can be anything at all. Nothing is constraining it to be only something that implements Query. I've had a quick play around with seeing what might work without resorting to impl Query<X> everywhere and I think something like this works?

/// A query to be run against client windows for identifying specific windows
/// or programs.
pub trait Query<X: XConn> {
    /// Run this query for a given window ID.
    fn run(&self, id: Xid, x: &X) -> Result<bool>;

    /// The negation of this query
    fn not(self) -> NotQuery<X, Self>
    where
        Self: Sized,
    {
        NotQuery {
            inner: self,
            _phantom: std::marker::PhantomData,
        }
    }
}

/// The logical negation of `Q`.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NotQuery<X: XConn, Q: Query<X>> {
    inner: Q,
    _phantom: std::marker::PhantomData<X>,
}

impl<X, Q> Query<X> for NotQuery<X, Q>
where
    X: XConn,
    Q: Query<X>,
{
    fn run(&self, id: Xid, x: &X) -> Result<bool> {
        Ok(!self.inner.run(id, x)?)
    }
}

Exposing the inner Query as public for these structs isn't really that valuable when they should be getting constructed by calling the combinator methods (and becomes actively annoying to do once we need PhantomData). Also, if this is going to be added as a feature from the main Penrose crate itself these should just be methods on the Query trait directly rather than adding an extension trait that then needs to be imported.

All that said, I'm still not convinced that I fully see the utility of the All and Any structs? I can see the logical operator structs being useful for simple combinations, but for anything sufficiently complicated that you start to use a lot of these I think it is probably going to be easier to write explicit queries instead.

favilo commented 4 months ago

Thanks for the pointers. Not sure why I'd not thought of PhantomData. Of course we'd need the types to ensure it is the same XConn

This looks much simpler, and I like the concrete types. Though I was wondering if returning Box<dyn Query<X>> might be good for the combinator methods. Not that dynamic dispatch is that great, but you do use it a lot in this crate. And it would allow for future updates to the underlying types without changing the public interface of these combinator methods.

EDIT: In defense of my prior implementation, I'd added the methods to an extension trait because it didn't make sense to allow people to override them just by implementing Query, since they are pretty standard logical constructs. But if you'd prefer they were here, that's all good to me. This way you don't need to use a new trait just to combine queries.

sminez commented 4 months ago

Box<dyn Query<X>> could work potentially but I quite like that the and method must return an AndQuery rather than an arbitrary query, and the behaviour of these combinator structs is a standard logical construct like you say. An extension trait does make sense as a way of ensure that the implementation can't be overwritten (something that I've done in the codebase already with the XConnExt trait) but here I think this feels more like the situation you have in the standard library with Iterator where there are a whole bunch of methods in the trait that return types you can't directly construct, making the methods effectively non-overwritable anyway.

Once thing I'm not sure about is whether using Box<dyn Query<X>> inside of the combinator structs would be worth it? So that we have AndQuery<X> rather than AndQuery<X, Q1, Q2> where Q1: Query<X>, Q2: Query<X>. The dynamic dispatch side of things is another lookup but compared to the interactions with the X server to actually run the queries I very much doubt that it will be having any noticeable performance impact, and the resulting API would be a lot nicer to work with.

@favilo do you have any preference on that side of things?

favilo commented 4 months ago

Hmm. The idea of just AndQuery<X> is pretty nice. I can get behind internal dynamic dispatch. That would make the public API slightly less confusing for people new to rust.

And that's a great point about not being able to construct the types outside of this crate. You've convinced me that we don't need an extension trait.

I'll go ahead and make that change tomorrow morning.