When last time I lamented the unoriginality of my tool nfp I also entertained the idea of salvaging some value from it by extracting the event debouncing part into a stand-alone tool. So that's what I did.

Meet debounce, a Rust library and a prototype command-line tool.

Rust

In all honesty, I didn't really need debounce as a library. nfp was already working fine. But it felt like the Right Thing™ to do, and gave me an excellent opportunity to play with Rust's synchronization primitives.

Blocking and waiting

For a clean solution (that is, one without busy polling I employed before) I needed two threads: the main one to block and wait forever for external events, and a worker to wait out timeouts and perform specified actions. The worker would also have a mode where there are no events and it should block and wait for the main thread to supply one.

This sounds like the job for a conditional variable, and I had hoped Rust would have some idiomatic higher-level wrapper around them. Turned out, Rust had three :-)

Somehow it's very on-brand for a language that gives you 5 kinds of pointers and 4 kinds of strings :-) But this is also what makes it fun! Anyway, I ended up using parking, as it didn't need any extra code and worked well for my use case where I don't mind the worker thread being occasionally randomly woken up out of turn.

Traits intricacies

Another purely Rustian puzzle I stumbled upon had to do with polymorphism. I have two kinds of event buffer types whith identical interface for getting values out of them. In Rust you express this with a trait which concrete types then implement in their own way:

pub trait Get<T> {
    fn get(&mut self) -> State<T>;
}

T is the type of data stored in the buffer.

Here seasonal Rustians are probably already asking their screens something along the lines of "wait, if your return type strictly depends on what's in the buffer, it doesn't really make sense for it to be a parameter of the trait…" And they are totally correct, but I didn't know that at that point.

So far so good. Then I thought that, having a .get(), it should be pretty natural for the buffer to implement a standard Iterator that would call .get() as long as there are items in the buffer in the ready state.

So I wrote the obvious:

impl<T, B> Iterator for B
where
    B: Get<T>,
{
    fn next(&mut self) -> Option<T> {
        todo!();
    }
}

Which says "this is an implementation of the standard Iterator trait for any type B which implements Get".

This however produced a compiler error which proved too intricate for me to understand. So, long story short, I went on Rust user forum where nice people imparted me over a couple of days with deep knowledge about traits, blanket implementations and associated types (which I think I finally get). Now my buffers are also iterators and I don't need to repeatedly call .get() in my tests :-)

Here's a couple of things I had a chance to reflect on, following this story:

CLI tool

Rust's packaging tool, Cargo, has a built-in notion of "examples", where you can implement something working without affecting your library dependencies and have it automatically built alongside the main code.

So I implemented a CLI tool which works exactly in the way I described in the previous post, by removing sequential duplicates from stdin that happen within a specified grace period:

inotifywait -m . | debounce -t 200

It's very bare-bones, as much as you would expect from a working example. I encourage anyone who needs additional options and features to write their own solution. (Here's a free idea: let the user specify by which part of the string to test equality, either with a regex or a field index, or something.)

Open source

So this technically makes me an open source maintainer. Again. But this time, having 10+ years of experience maintaining highlight.js I think I'm going to do things differently.

I don't like the default assumptions about what FLOSS maintainers are supposed to do these days. You're supposed to write code, do regular releases (lest your project will be pronounced dead), react on issues, review PRs from random people and be extra energized when dealing with anything that has the word "security" attached to it. And as a bonus for particularly good work you'll be rewarded with a Community™, whose self-proclaimed leaders would harass you for being a dictator who should feel guilty about not having the Community's interests, as formulated by the "leaders", in mind every single second of your life.

This is all bullshit, of course. But this is also reality. And I used to bitch about it before, there's nothing new here.

So here's what I'm going to do:

In this light, my choice of pijul as a version control system plays well into this, as I expect to be somewhat shielded from Github's crowd where that sense of needy entitlement is especially strong.

A random recent example is this thread where people with Opinions™ have been harassing maintainers of black about a minor issue for three years, and not a single one of them thought of volunteering to maintain a fork with the stability guarantees they ostensibly require so hard. Such work is not much fun of course, but they assume the maintainers owe it to them.

P.S. I think I should write more about pijul, it's an interesting project!

P.P.S. By the way, check out highlight.js! Since I transferred it to more motivated people it became such a powerhouse!

Add comment