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 :-)
- Condvar, which is exactly the Rusty wrapper around the idea of a conditional var
- channel, a higher-level interface for consumers to wait on data supplied by producers (which I suspect is built on top of
Condvar
) - parking, a built-in lightweight ability for a thread to suspend ("park") until the other thread wakes it up.
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:
-
Having such a go-to place as users.rust-lang.org is exactly what I'm missing while developing for Android. To my knowledge, there just isn't anything like this for that ecosystem, and everyone just shouts in the abyss of Stack Overflow and tries to sort out random pieces of code coming from there.
-
This type system wrangling is one of the things that makes dynamically typed languages more productive. And yes, I'm aware of the downsides, so no need to repeat the mantra of "typed languages remove a whole class of bugs" in the comments. Better think of the whole new class of code structures you need to learn and maintain to do it :-)
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:
-
I won't develop the code past what I need from it myself. If someone needs more features, they should write their own solution and maintain it (or not maintain it!) in the way they want. The license explicitly allows it.
-
I am interested in what other people would make of it, but I make no promise about accepting all derivative work into my code. As long as you don't forcefully insist on having your PR merged, I remain a nice person and encourage sharing of ideas!
-
I am especially interested in suggestions (in any form) on improving my Rust. This is, after all, what I wrote the thing for!
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!