Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

About

Hi, I’m Charles! I like programming, 3D printing, electronics, and video games. I care a lot about community, robust and maintainable software, and developer tooling. (Obviously, those two lists are not exhaustive.)

I award myself the title of “yak shaving domain expert” because many of my projects were created to solve problems I encountered in other projects of mine, and because the projects I created this way have been my most successful ones.

Notable projects

DerailWebsite

Derail provides an alternate error trait for Rust with support for multiple causes and strongly-typed attached details.

The core of this project is pretty much done until Rust makes traits with lifetime-generic associated types dyn-compatible.

I created this initially because I needed its feature set for Engage.

EngageWebsite

Engage is a command-line process composer with ordering and parallelism based on directed acyclic graphs.

This project is quite usable, though there are some planned changes and improvements that may happen in the future.

I created this initially because I wanted its feature set for basically every other project I work on.

SprinklesWebsite

Sprinkles is a convention for the Nix ecosystem for structuring dependencies, exposing items, allowing self-references, and enabling overridability.

This project is quite usable, though there are some planned changes and improvements that may happen in the future.

I created this initially because I wanted the minimum viable escape vector from Flakes for my NixOS configurations and my other projects that use Nix.

GrapevineWebsite

Grapevine is a Matrix homeserver forked from Conduit 0.7.0.

This project has largely ceased development, as its maintainers grew exhausted with the Matrix ecosystem.

SliceWorx KP3S Pro S1

Written 2023-02-14.


This is a living document that serves as notes on and a review of this 3D printer.

stock

The SliceWorx KP3S Pro S1 is a version of the Kingroon KP3S Pro, which itself is a version of the Kingroon KP3S. The selling point of the SliceWorx KP3S Pro S1 over the Kingroon KP3S Pro is that it ships with linear rails on all axes. Kingroon does sell an upgrade kit to convert the Y axis to linear rails, but their kit will cost you ~1cm of build height, whereas SliceWorx redesigned the Y axis a bit so you can have your cake and eat it too.

In my opinion, this printer has great value for the 250 USD I paid for it. As far as I can tell, it’s only possible to buy this version of the printer through SliceWorx themselves, since they make some modifications before sending it to you. If you’re looking for a relatively well-designed first printer, I’d say this fits the bill. Of course, there’s always room for tinkering if you’re into that, but the out-of-the-box experience is fine for typical use-cases.

Mainboard

mainboard

As you can see, this uses a GD32F303VET6, which is unsupported by Marlin.

Marlin

I’m currently running Marlin bugfix-2.1.x on it anyway using my configuration here. I’m going to be doing a lot of rebasing/force-pushing so be aware of that if you’re going to be using or basing off of my configuration. Don’t expect anything to work properly until you cherry-pick this patch into your tree, reproduced here just in case:

From 1940418bbe89d07863ed05c6cddb1edf285a5a31 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilenko <jmz52@users.noreply.github.com>
Date: Tue, 24 Jan 2023 20:37:03 +0300
Subject: [PATCH] Update tft_fsmc.cpp

---
 Marlin/src/HAL/STM32/tft/tft_fsmc.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Marlin/src/HAL/STM32/tft/tft_fsmc.cpp b/Marlin/src/HAL/STM32/tft/tft_fsmc.cpp
index cf9e569336bc..aad796231436 100644
--- a/Marlin/src/HAL/STM32/tft/tft_fsmc.cpp
+++ b/Marlin/src/HAL/STM32/tft/tft_fsmc.cpp
@@ -100,11 +100,11 @@ void TFT_FSMC::Init() {

   HAL_SRAM_Init(&SRAMx, &Timing, &ExtTiming);

-  __HAL_RCC_DMA2_CLK_ENABLE();
-
   #ifdef STM32F1xx
-    DMAtx.Instance = DMA2_Channel1;
+    __HAL_RCC_DMA1_CLK_ENABLE();
+    DMAtx.Instance = DMA1_Channel1;
   #elif defined(STM32F4xx)
+    __HAL_RCC_DMA2_CLK_ENABLE();
     DMAtx.Instance = DMA2_Stream0;
     DMAtx.Init.Channel = DMA_CHANNEL_0;
     DMAtx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;

In order to get a basic config going, you’ll want to find and cherry-pick the configure electronics and copy mechanical configuration from manufacturer commits into your tree.

Klipper

I hear it’s supposed to be possible to put Klipper on this, but I have yet to try. Currently stuck in decision fatigue for which SBC to get.

Python has sum types

Written 2022-05-25.


Python doesn’t have sum types

One of my biggest gripes with Python is that it doesn’t have sum types, or a way to emulate them. For example:

>>> from typing import List
>>> x = []
>>> isinstance(x, List[int])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/nix/store/nvxp3xmlrxj9sw66dk7l0grz9m4889jn-python3-3.9.12/lib/python3.9/typing.py", line 720, in __instancecheck__
    return self.__subclasscheck__(type(obj))
  File "/nix/store/nvxp3xmlrxj9sw66dk7l0grz9m4889jn-python3-3.9.12/lib/python3.9/typing.py", line 723, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

This means that Python has no way to check if a variable is an instance of a specific type1

Unless it does

I present to you: typing.Literal. “How is that relevant?” you ask. Excellent question. First, let’s remember that sum types are also known as “tagged unions”. Python has unions in the form of typing.Union (or the | syntax in newer versions). Given this, we can create the union half of a sum type like this:

from typing import Union, List

StrOrIntList = Union[str, List[int]]
#                    ^^^  ^^^^^^^^^ A **variant**
#                    |
#                    Another **variant**

The next problem is figuring out how to detect which variant we have. The obvious strategy is to use isinstance, but as established, isinstance is not flexible enough. I also looked around to see if there’s a way to get type annotation information at runtime so one could check against that, but this doesn’t seem to be possible. Even if you could, it would also not cover the case where you want multiple variants with the same data type: Union[int, int, str] is the same as Union[int, str] to the typechecker2, so there’s no way to tell the two ints apart.

Next, we need some mechanism to associate each variant with a tag that’s accessible at both runtime and typecheck-time, so that we can do control flow at runtime and allow the typechecker to assert that we’re checking the right things before even running the code. For the association portion, we know that types can be paired together in Python by using typing.Tuple like this:

from typing import Tuple, List

StrAndInt = Tuple[str, List[int]]
#                 ^^^  ^^^^^^^^^ We'd like this to be the **variant's** data
#                 |
#                 We'd like to use this as our **tag**

This has a major problem, which is that the typechecker has no insight into the possible values of our tag, the str. This practically defeats the entire purpose, since it robs us of assertions that we’re checking against valid tags, and that we’re checking against all valid tags. After all, the goal is to ensure more things can be checked before runtime, and having to run the code to make sure you have no typos in strings where you’re checking against a tag is pretty self-defeating. For a week or so after coming up with what I’ve discussed so far, I had no solution to this problem, and I thought most hope was lost. But then I had a realization, which led directly to this blog post.

For some reason3, Python allows you to use value literals as types. Importantly, value literals can be used not only as a type, but also as a value. Using this slightly odd behavior4, we can create a tag that’s accessible at both runtime and at typecheck-time. For example:

from typing import List, Tuple, Literal

NamedInt = Tuple[Literal["a tag"], List[int]]
#                ^^^^^^^^^^^^^^^^ A statically-analyzable
#                                 *and* runtime-accessible **tag**!

On its own, this construct is completely and utterly useless. But, if we combine our tags with a union, we get…

Tagged unions

Which are also known as…

Sum types

In Python, sum types are constructed as a union of tuples of a tag and the variant’s data. Here’s an example:

from typing import List, Tuple, Literal, Union

MySumType = Union[
#           ^^^^^ The union
    Tuple[Literal["string"], str],
    Tuple[Literal["list of ints"], List[int]],
#   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A variant
#         ^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^ The variant's data
#         |
#         The variant's tag
]

Now that we have our sum type, I’ll demonstrate how it’s used. The type of x[1] where x is of type MySumType can be either str or List[int]. We can determine the type of x[1] we have by looking at the tag, which is x[0]. Then, if we do things right, our typechecker will be able to automatically narrow the type of x[1] to either str or List[int] based on the type and value of x[0] in our control flow. Let’s give it a try:

def gives_sum_type() -> MySumType:
    # Just return one of the two "variants"
    return ("list of ints", [1, 2, 2, 3])

    # We could also do this, for example:
    # return ("string", "foo")


def uses_sum_type():
    match gives_sum_type():
        case ("string", x):
            print("the variant contains a string:", x)
        case ("list of ints", x):
            print("the variant contains a list of ints:", x)

The typechecker narrows the type of x in each branch to either str or List[int] based on the tag matched in the case arm. Values in the tag position that do not correspond to any tag in the sum type’s definition will cause the typechecker to emit an error; typos and the removal of variants from the sum type’s definition are both covered by this case. Exhaustiveness is also checked in match statements, so if you later add variants to MySumType, the typechecker will emit an error since not all variants are covered.

If you’re not on Python 3.10 or later (which was when match was introduced), then you can use the following hack to get the same guarantees:

from typing import NoReturn

def unreachable(_: NoReturn) -> NoReturn:
    raise AssertionError("unreachable")


def uses_sum_type():
    x = gives_sum_type(False)

    if x[0] == "string":
        y = x[1]
        print("the variant contains a string:", y)
    elif x[0] == "list of ints":
        y = x[1]
        print("the variant contains an list of ints:", y)
    else:
        unreachable(x)

Avoiding a typesafety footgun

Given the following:

from typing import List, Tuple, Literal, Union

MySumType = Union[
    Tuple[Literal["string"], str],
    Tuple[Literal["list of ints"], List[int]],
]

MyOtherSumType = Union[
    Tuple[Literal["string"], str],
    Tuple[Literal["list of strings"], List[str]],
]

The code ("string", "foo") will pass the typechecker as either MySumType or MyOtherSumType because they both have a variant with the same name, which is not great. One way to get around this would be to create a wrapper class and instantiate your sum type inside its constructor. For example:

from typing import List, Tuple, Literal, Union

class MySumType:
    def __init__(
        self,
        adt: Union[
            Tuple[Literal["string"], str],
            Tuple[Literal["list of ints"], List[int]],
        ],
    ) -> None:
        self.adt = adt

Now you’d write MySumType(("string", "foo")) instead, which will never typecheck as another sum type that happens to have variants with the same name. In order to use match with this type, you’d simply use match.adt to get access to the inner type. This has the added bonus that you can now add class methods to sum types, which is pretty cool.

Adding documentation

You might also want to add documentation to your sum type variants. We can accomplish this by moving away from Literal["..."]s and creating new types:

from typing import List, Tuple, Literal, Union

class String:
    """
    A variant containing a string
    """


class ListOfInts:
    """
    A variant containing a list of ints
    """


class MySumType:
    def __init__(
        self,
        adt: Union[
            Tuple[String, str],
            Tuple[ListOfInts, List[int]],
        ],
    ) -> None:
        self.adt = adt

This is slightly more awkward to write, as MySumType((String(), "foo")), but the benefit of documentation outweighs it in my opinion. Matching still works as expected as well.

Danger zone

One famously successful use-case for sum types is error handling: their exhaustive and explicit properties make it easy to determine which failures are possible, and from there, which failures you can handle, and then to handle those in a type-safe, high-confidence manner. We can now accomplish this in Python, by replacing the use of exceptions with the following foundation5:

from typing import TypeVar, Union, Tuple, Literal, Generic

class Ok:
    """
    A variant containing a success value
    """


class Err:
    """
    A variant containing an error value
    """


T = TypeVar("T")
E = TypeVar("E")


class Result(Generic[T, E]):
    def __init__(
        self,
        adt: Union[
            Tuple[Ok, T],
            Tuple[Err, E],
        ],
    ) -> None:
        self.adt = adt

    # More methods...

This is what scientists call “carcinisation”, which is the phenomenon wherein, given a programming language and enough time, it will eventually become Rust.



  1. If the types have generic parameters. Obviously, this works for “regular” types, because otherwise isinstance() would be completely useless.

  2. Even further, Union[T, T, T] is the same as T; note how the Union is dropped entirely.

  3. I am indifferent to the rationale that caused this behavior to exist, but I am very happy that it does because it is incidentally core to making proper sum types in Python.

  4. Hmm, maybe it’s not that odd, I guess you could use it to do const generics? I bet that’s why it exists.

  5. I would also like to have an ABC for sum types that provide a single adt() method that performs a conversion like Result -> ResultRaw so that this sort of interface can be identical for all sum types, which would make writing manual match statements more consistent. I can’t think of an easy way to statically type such an ABC right now though, and the .adt convention is good enough for my immediate purposes.

Psyonix’ Epic Fail

Written 2019-05-01, updated 2022-03-13.


So Epic Games is buying Psyonix. Everyone’s upset for a multitude of reasons, a lot of which aren’t the right ones. A few very special people are even content with this news for the wrong reasons, which is even worse. Now, imagine the next three paragraphs as panels in the expanding brain meme.

  1. Lots of people are just upset that Rocket League is going to show up on the Epic Games Store (EGS) and aren’t reading any further into the issue. This opinion is not so bad because at least they’re still doing the right thing, just for the wrong reasons. Also, it seems nearly universally agreed that the EGS is… not very good.

  2. A lot of people are indifferent and just want everyone to shut up and move on. Obviously, this accomplishes nothing other than making the people who know what’s going on talk more (see: this blog post).

  3. Some people are actually supportive of this, their reasoning being that they “can still play it on Steam since they already own it there, and this will just bring the game to a wider audience”. The last part may be true (summary: ~40% of EGS users come from consoles rather than being an existing PC/Steam player), but this still misses the actual issue at hand. However, I say “may” because Rocket League’s playerbase is already mostly console players.

Very few people are seeing the actual issue at hand, however, which is that by removing the game from Steam, Epic/Psyonix (I don’t know which is actually in charge of this decision, but if I had to guess I would pick Epic) would be engaging in extremely anticonsumer and anticompetitive behavior.

It’s true that neither party has yet to confirm or deny that Rocket League will be removed from Steam after it appears on the EGS. However, if Epic/Psyonix wanted everyone to calm down, you’d think they’d just announce that Rocket League will remain purchasable from the Steam store and fully supported as it has been, and just also be available on the EGS. There would be no issue if this were the case, as then users would be able to choose whether to play on Steam or on the EGS, which is the ideal situation. The lack of such an announcement says to me that they’ve already decided internally to remove it from the Steam store and they just don’t want to deal with the backlash from the public yet.

Some other issues that have not been addressed is whether current owners of the game on Steam will be able to buy Keys, Rocket Passes, or DLC cars once the game appears on the EGS. Whether Psyonix will continue to support Rocket League on Linux is also up in the air; since the EGS has no Linux version, it seems reasonable (from a business perspective) to me that Psyonix would stop ensuring that updates worked on Linux if Rocket League were made unpurchasable on Steam. Valve’s Proton is very good, so that might be an option, but I’d bet a lot of people who care about this issue will completely drop the game regardless.

UPDATE: As expected, in late September of 2020, Rocket League was removed from the Steam store. Linux support was also dropped/disabled

How single-user Git-over-SSH works

Written in like 2018 probably.


Have you ever wondered how GitHub, GitLab, Gogs, Gitea, and so on allow multiple users to push and pull data from repos with only one unix user? Perhaps you want to know how this is done so you can write your own version of the aforementioned type of software that doesn’t suck. That’s how I got here. Anyway, this is how it’s done:

OpenSSH gives you an option (ForceCommand in /etc/ssh/sshd_config) to force the use of a particular command on connection, overriding the one the client actually wants to run. When this option is set, the client’s “intended command” gets stored in an environment variable called $SSH_ORIGINAL_COMMAND. This can be used to force the execution of your own script (or binary) that allows and disallows “intended commands” each time anyone tries to do anything with this account over SSH.

Additionally, the ~/.ssh/authorized_keys file allows you to specify a per-key forced command. This means that you can set an option (say, --with-key) for the per-key override that isn’t present in the global override in /etc/ssh/sshd_config. Now, in your forced command, you can disallow write access to connections that are missing the --with-key argument, since it’s only present if a user has uploaded an SSH key.

Even further, you can provide the --with-key argument a key_id value that allows your software to know exactly which key was used for this connection. With that information, you can associate the SSH key with a user account and, for example, allow them extra read/write access to their private repositories and extra write access to their public ones. It’s up to your software to keep a database of key_id <-> SSH key <-> user associations, however.

While you’re messing with the sshd_config, you’ll need to add PermitEmptyPasswords yes and you’ll probably also want to add DisableForwarding yes. Thankfully, you can restrict the effects of those two options to apply only to your software’s user and not to everyone on the server by using a Match section. All together, your sshd_config will have a new section like this:

Match User your_software_user
    PermitEmptyPasswords yes
    DisableForwarding yes
    ForceCommand /path/to/your/software

References

  1. man 5 sshd_config for /etc/ssh/sshd_config options
  2. man 8 sshd for ~/.ssh/authorized_keys file format and options
  3. Both of the above for the uses of the $SSH_ORIGINAL_COMMAND environment variable
  4. The Gogs codebase for reverse-engineering all of the above

Extreme minimalism considered harmful