↖️ Blog Archive

Humansort: A Tool for Sorting Items By Subjective Criteria

Bradley Gannon

2023-01-09

You can try the Web version of Humansort here, or you can try the CLI version and/or view the code here.

Motivating Problem

I have way more ideas than resources. At least a few times a day, I’ll think of something that “would be neat” if only I had the time and the right tools. Many years ago, I took the first step forward by writing my ideas down, but in a sense this was actually worse than nothing. Now I had a notebook full of many questionable ideas and probably a few good ones, which isn’t too different from a jar full of sand with a few delicious blueberries.

My main problem seemed to be that I couldn’t bring myself to get rid of the sand. By its nature, this list contained ideas that I at least thought were good enough to write down at some point in the past, so practically none of them were outright bad (according to me). Discarding any of them felt impossible, as if by eliminating them I was losing something. Maybe deep down I’m just unwilling to accept that there are plenty of things I’ll never get around to doing in my life. By choosing to do one thing, I’m also choosing not to do everything else. In any case, the ideas just pile up, and I didn’t know how to sift through them.

My favorite solution to a problem is to figure out why solving it isn’t necessary. (In rare cases, this is actually the worst solution, but it often works and is wonderful.) I applied that to my surplus of ideas, and the result is Humansort1. This application is the answer to the question: “What if I sorted my list of ideas according to my overall feelings about them?” If I could accomplish that, then I could pop the most interesting ideas off the top when I want to work on something new, and I wouldn’t have to worry about the ones at the bottom. The ones at the bottom are gone—for practical purposes—but they aren’t lost, which is apparently my issue. I can always grab them from the bottom, if I want to.

Elo Rating System

Humansort is different from a typical sorting algorithm. If you want to sort a list of numbers, you can do a lot of pairwise comparisons and work your way to an answer. Pairwise comparisons between two numbers will always have the same result. 2 < 3 is true today, and it’ll still be true tomorrow. But what if that property wasn’t available? If you’re trying to sort ideas for future projects (or baby names, or what book to read, or what restaurant to try, or what game to play, etc.), then your opinion might change over time. An idea that seemed uninteresting yesterday might become interesting to you tomorrow because of something you saw or read, or just because your mood is different. In this world of subjectivity, just because 2 < 3 was true yesterday doesn’t necessarily mean it will continue to be true tomorrow.

Breaking this assumption of stable comparisons also breaks most traditional sorting algorithms. You can use them, but they won’t return a perfectly sorted list2, and some will break in ways that are really unhelpful. For this problem, we need a sorting algorithm that’s fuzzy. It should bend and stretch under the shifting loads of human opinion, instead of brittly snapping at the first application of force. That’s where Elo comes in.

The Elo rating system provides a method for estimating a player’s relative strength in a two-player zero-sum game. Arpad Elo designed the system for use with chess, which is about as two-player and zero-sum as you can get. Elo assumed that a player’s relative strength could be described by a normal distribution, which captures the reality that some players have “good days” and “bad days”, but on average a master performs better than a novice.

By some clever math, Elo set the system up so that when two players meet, their ratings before the match give an estimate of the outcome. For example, if a master and novice play each other, it’s likely that the master will win, so their expected outcome is close to one, while the novice’s expected outcome is close to zero. Notably, the novice’s expected outcome is never exactly zero, because it’s always possible that they win in an upset.

After the match, the outcome updates the ratings of both players. If the master won, then their rating goes up only a little—after all, they were expected to win. The novice’s rating also goes down slightly. In fact, the rating shifts are zero-sum. That is, whatever the master gets must have come from the novice. If the novice somehow wins, then a very large rating transfer occurs. If the novice won against the master, then maybe the novice isn’t so inexperienced after all, and maybe the master isn’t as masterful as their rating would suggest.

A consequence of this approach is that the ratings are always a lagging indicator of actual performance. The hope is that an active player would play enough matches to keep their rating mostly accurate over time.

It turns out that the Elo rating system is applicable to all kinds of games, even with more than two players. You don’t even have to use it for traditional games. In the case of Humansort, I assumed that subjective sorting items had some unobservable “true” relative rating for the particular person doing the sorting, and that this rating can change over time and be influenced by all kinds of stuff that probably can’t be modelled all that well. Humansort presents the user with a small number of items from the larger set and asks for the user’s top preference. This preferred item is considered to be the “winner” against the other items in the subset, and the system then performs Elo rating updates between the winner and each of the other items. Repeat this over an over, and it doesn’t take long before the list is mostly sorted. Later, the user can run the sorting system again, and their updated preferences will be incorporated into the list. All of the subjective reasoning behind the preferences are wrapped up in a single rating value, and the system accepts the subjectivity in all its fuzzy harmony.

I don’t know whether or not the assumptions behind the Elo system are actually met by the Humansort problem. It’s not clear to me, for example, that idea ratings are inherently zero-sum. In practice, though, it seems to work, and that’s good enough for me. Maybe there’s a another system that could perform better, but “better” here really only means “converges faster”, and the current system already converges pretty quickly. I also tweaked the selection of items for comparison such that it’s not random. Instead, there’s a bias towards items that are already highly-rated. My reasoning here is that users probably want to get the highest sort fidelity for the items that they care the most about—you probably don’t care about the particular sort order of the items at the bottom of the list. This improves convergence somewhat for the highest-rated items, which for large lists can save time and limited user attention.

Comments on Yew and Rust/WASM

I didn’t really need a Web version of this program, but I thought it might be fun to use it as a motivator for learning some basic Rust/WASM. Yew seems to be the de facto standard in this space. The setup process is pretty simple, mostly because Rust’s toolchain and devtool management are so smooth. It wasn’t long before I was hello-worlding in the browser.

Having gone through the process now, though, I’m less enchanted with the whole idea. First though, here are some of the unequivocal advantages of Yew:

The mapping of React concepts really is amazing. The homepage isn’t lying when it says:

Developers who have experience with frameworks like React and Elm should feel quite at home when using Yew.

I felt almost no friction coming from React. They’ve got function components, hooks, props—the whole deal, only in Rust.

But a few issues and choices made the experience uncomfortable for me. Some of this is definitely my inexperience with Yew—there’s no question about that—but also I struggled in ways that I don’t with Typescript and React.

So Much Cloning

In Yew, as in React, callbacks are everywhere. Callbacks are little functions that you pass to a component or element that handle some event, like a keypress or a click. They usually take the event as an argument and operate on the app state somehow. In a garbage-collected language like JavaScript, you can use any variable as long as it’s available in the callback’s scope. Of course, Rust doesn’t have the burden of a garbage collector, but that freedom comes with some responsibilities for the programmer. In this case, it’s necessary to move the variables that you need to use in the callback into the callback itself. If this were the whole story, it would just be a matter of using the move keyword and a bit of transparent compiler bookkeeping.

But that’s not the whole story. Since the callback outlives its parent function, its variables need to as well, so you have to clone them into new local variables beforehand.3

Anyway, what you get is something like this:

let onkeypress = {
    let state = state.clone();
    let old_value = value.clone();
    let editing = editing.clone();
    let editing_value = editing_value.clone();
    move |e: KeyboardEvent| {
        let target: HtmlInputElement = e.target_unchecked_into();
        let new_value = target.value();
        editing_value.set(new_value.clone());
        if e.key() == "Enter" {
            state.dispatch(Action::RenameItem {
                old_name: old_value.to_string(),
                new_name: new_value,
            });
            editing.set(false);
        }
    }
};

This is the handler for keypress events when the user edits an item in Humansort. I’m cloning four variables here because I need access to all of them in the closure. Of course, with some clever refactoring there’s some chance that I could reduce that number a bit, but that’s not the point. The point is also not to improve performance by avoiding cloning, since I would guess that the effect would be negligible. It’s purely a boilerplate-avoidance issue for me. It would be cool to not have to clone stuff all the time because it clutters the code.

This issue is not unique to Yew, and the Rust community is aware of it (i.e., there is a tracking issue). There doesn’t seem to have been much progress towards a solution yet, although there has been a lot of discussion, which probably has value.

No Code Formatting in the html! Macro

Perhaps the most impressive feature of Yew with respect to developer experience is the html! macro. If you feed the macro some DOM-like tokens, it’ll convert that representation into an Html struct. This makes it possible to write something that looks a lot like JSX inside Rust source files. You can even nest them and use braces to escape further Rust code, which gives you all kinds of options for emitting virtual DOM elements according to arbitrarily complicated logic. As long as the function component ultimately returns the Html type, you’re good to go.

Here’s an example from Humansort. This is from the InputItem component, which is the component that appears when you type in a new item to sort and press enter.

html! {
    <tr>
        {
            if *editing {
                html! {
                    <td colspan="2">
                        <input
                            type="text"
                            {onkeypress}
                            value={editing_value.to_string()}
                        />
                    </td>
                }
            }
            else {
                html! {
                    <>
                        <td>
                            { value }
                        </td>
                        <td>
                            <button onclick={onedit}>
                                { "edit" }
                            </button>
                        </td>
                    </>
                }
            }
        }
        <td>
            <button onclick={onremove}>{ "remove" }</button>
        </td>
    </tr>
}

Note that I can mix pseudo-HTML like the <tr> tag with Rust code to get conditional output. Amazing.

My only gripe here is that rustfmt doesn’t know how to handle whatever goes inside the html! macro, so I have to format that code by hand. 😭 Poor me, right? It seems like rustfmt actually doesn’t handle macro invocations in general, which is reasonable but still disappointing. What if Yew shipped some tool called e.g. yewfmt that cleaned up html! invocations? Or, another more radical option would be to ditch html! and use a different pattern, although I can’t imagine what would be better. Building Html structs by hand would be a lot less elegant.

The Name (“yew” vs. “you”)

The project’s name sounds identical to the word “you”, which makes it difficult for me to discuss it with people in verbal conversation. I had particular trouble while trying to explain the benefits of using Yew to a friend. For example, I probably said something like:

Yew is a cool Rust library that lets you write frontend code for the Web.

but I felt weird as I was saying it because I felt the need to somehow indicate that I was talking about Yew, the Rust library, and not “you” the person I was speaking to. I haven’t found a good workaround for this issue. Maybe I could start calling it “yew rs” or “yew lib” in verbal conversation?

No Built-in Solution for Styling (Yet)

If you want to style your components, Yew doesn’t have a standard solution. For my simple project, I just wrote CSS in my index.html, but for a more complicated app you’d probably want your styles to live in your components, with some global style definitions to draw from. There seem to be several decent solutions, but I haven’t tried any of them. Hopefully one becomes popular enough to be officially adopted into the larger Yew project.

Conclusion

Yew is a lovely tool, and I recommend learning how to use it. It definitely works for small projects, and some of the examples in the repo show that its performance is impressive. (Come on, it’s Rust, we knew it would be fast.) I suspect that large Yew projects are just as comfortable to navigate and hack on as other Rust projects. If you really want to stick with Rust throughout your Web stack, this is your best option, and it’s a good one. I don’t think it’s the superior choice in all cases, though. The normal transition cost math still applies. React is probably more sensible if you need to use lots of npm packages, have a lot of knowledge in that ecosystem already, or you don’t need to care about performance, memory safety, or street cred.


  1. In case it isn’t clear, Humansort is a system for sorting items in a way that allows the human to provide the comparator function, rather than silicon. Its name is a continuation of the pattern among sorting algorithms that are all named *sort. It is not a system for sorting humans—although you could use it for that, I guess.↩︎

  2. In the case of a subjective sorting metric, “perfectly sorted” probably doesn’t have any meaning anyway.↩︎

  3. I’m actually not certain why this is the case, but it’s all over Yew and apparently all over other Rust code too.)↩︎