Daniel Duan's blog

On the Subject of Interface Hygiene

In a purly reactive world, your entire program merge into a single stream. Now, close your eyes, and envision: your project as one, beautiful, stream.

Now open your eyes. Yeah, it’s not. Your project is a Mac or iOS app. It’s full of your memories, sweat, and blood. And you are ready to sweat and bleed some more by putting some Combine and SwiftUI into it. You watched the WWDC19 sessions and learned that “Subjcets are super powerful”. You looked into your code and realized you can’t really do anything with Combine without Subjects at the current state of the project.

Well…

Here are a few habits that help keeping your project that prevasively uses Combine.Subject sane. They should seem obvious to anyone who understands Murphy’s law and the value of minialism in interfaces. If you already are using some reactive stream implementation, substitute the types with their counterparts in your framework and these rules should seem down right basic.

Vend Subjects as Publishers

Subjects help bridge from the imperitive to the reactive world. Somewhat paradoxically, sharing them is not very “RX-y”. This is akin to prefering lets over var.

Most of the time, what you want to share is the values pumped into the stream, not the privilage to mutate it. Because Subjects conform to Publisher, it’s easy to hide from the users the fact that your stream is backed by them.

// Bad: now anyone who get a hold of it can mess with your stream!
public enum GreatInts {
    public var updates = CurrentValueSubject<Int, Never>(0)
}

With Combine this conversion happens via type-erasure:

// Better: all your users care is the stream (publisher), so give them that!
public enum GreatInts {
    // Internally, it's backed by a subject.
    var subject = CurrentValueSubject<Int, Never>(0)
    // Externally, it's just a Publisher. 
    public var subject: AnyPublisher<Int, Never> {
        subject.eraseToAnyPublisher()
    }
}

CurrentValueSubject natually caches the latest value

RX theorists will hate this: sometimes it’s just practical to expose a synchronous interface to the latest vaule in the stream!

Two things.

  1. It might be tempting to expose the subject and let your user use its .value. Well, you shouldn’t (as explained in the previous section). A separate interface dedicated to the latest value prevents people from polluting your stream.
// (Still) bad
public final class GreatInts {
    public var updates = CurrentValueSubject<Int, Never>(0)
}
  1. Remember CurrentValueSubject has that .value property! It may seem surprising, but I’ve seen folks transitioning to RX clinging to the old ways:
public final class GreatInts {
    // well, at least it's not a public subject...
    var subject = CurrentValueSubject<Int, Never>(0) // <- initial value 0
    public var updates: AnyPublisher<Int, Never> {
        subject.eraseToAnyPublisher()
    }

    // Wait, there's that 0 again
    public var latest: Int = 0 {
        didSet {
            subject.send(latest) // ?
        }
    }
}

First, you’ll notice that 0, the initial value, is duplicated as both the subject’s initial value, as well as the value of a stored property. And these duplicated sources of truth persist throughout the parent’s life time. Weird, right?

Here’s a slightly better version:

public final class GreatInts {
    var subject = PassthroughSubject<Int, Never>()
    public var updates: AnyPublisher<Int, Never> {
        subject.eraseToAnyPublisher()
    }

    public var latest: Int = 0 {
        didSet {
            subject.send(latest) // ?
        }
    }
}

Now there’s no two copy of the latest value in memory anymore. But in my opinion it does not embrace the full power of Combine. Here’s the most natual way to do this:

public final class GreatInts {
    /// This is a CurrentValueSubject again.
    var subject = CurrentValueSubject<Int, Never>(0)
    public var updates: AnyPublisher<Int, Never> {
        subject.eraseToAnyPublisher()
    }

    public var latest: Int {
        subject.value
    }
}

Essentially, you create separate public interface, each vends a little piece of CurrentValueSubject’s power.

No really, don’t use Subjects

Even a well-scoped Subject (properly being private or internal, depending on your tool of choice for access control) still has a mutable state that you probably don’t want: its stream can go from “alive” to “complete”. And, again, anyone with access can make this state transition happen, leaving you in the undefensible position of … hoping everyone on your team to not misuse your stuff?

Lucky for you (and me), a “incompletable” subject is a established concept – a “Relay”. I’ve put together a repo for you to look and/or use:

https://github.com/dduan/relay

Yeah, ban all Subjects in your project with a linter. Seriously.

Fin

That’s all for now. I’m not an expert with RX myself. Hopefully these perspective can help you avoid some nasties.


TOML Decoder Playlist

I enjoyed making TOMLDeserializer and TOMLDecoder recently. Let’s hope some projects will pick them up and TOML as a configuration format will start to spead in Swift communities.

What’s outstanding about these projects is that I started working on them while streaming. Personally, I consume a lot of content like this. So now people can watch me writing Swift, too.

I’ve been archiving recordings of these streams as much as I can. Here are the links to these videos, each is around 1-1.5 hours long:

The TOMLDecoder projects would’ve been capture on camera entirely if I weren’t such a streaming n00b and messed up a few streams. Hilarious.


Kick-ass CLI Tools In Swift

As someone who lives in a terminal simulator, I’m pleasantly surprised by the new toys we get in recent years such as fzf, ripgrep, fd, etc. A great number of these are written in relatively young programming languages such as Go and Rust. But, noticibly, none of them are written in Swift.

In this post, I’ll try to explain why that is.

POSIX Ergonomics

Unix-like virtual file systems has been around for decades. API that manupulates such systems has standardized a long time ago and exists in most computers running Linux/BSD/macOS today (and, to a large extend, smart phones). To Swift users, Using these APIs is straight-forward (rmdir("path/to/dir")).

So Swift programmers are all happy campers (re-)inventeing all sorts of file system utilities, right?

Well, not quite.

Okay, I lied about POSIX APIs being “straight-forward” in Swift. Or rather, this is very subjective.

Continuing with the rmdir example, we must first import it from either Glibc or Darwin, depending on your OS. To know whether the operation is successful, we need to see whether it returned integer 0. To learn why 0 was not returned, we need to read the “magical” variable errno. errno could be written to by other APIs so we’d better capture it in time…

And that’s one of the simpler APIs in POSIX calls!

Programmers whine about ergonomics partially because we are previlidged and spoiled. But mostly because our attention is a limited resources. Mixing API conventions distracts us from solving the problem at hand. Bad ergonomics, therefore, drives away a good potion of users who cares about quality of their tools.

Culture and History

As of this writing, the release of Swift 5 is imminent. The vast majority of existing Swift code is written to run on iOS. The concept of a file, or the traditional virtal file system, is hidden to iOS users, and sandboxed for developers. I bet most Swift users rarely think about the fact that there’s a entire set of POSIX API at their disposal.

Foundation alleviates the need to deal with files and directories: Bundle locates the files; CoreData, UserDefaults or the keychain is your primary way to persist data; Data, String or NSCoding has methods to read and write to files. And finally, if you really need to deal with files, NSFileManager has everything you’ll ever need.

Why would a productive Swift programmer think about POSIX in this environment? Why would a tutor teach POSIX over the useful/practical/”native” alternatives?

We can trace “riding on the Apple platform” mentality back to the pre-iPhone days, where a very small Mac developer community labors on on a niche platform (compared to iOS today) and they loved it. However, I’m sure they used more POSIX stuff back then than the average iOS developers today.

Having a great library such as Foundation on the most popular developer platform where the language thrives means it’ll take longer for “subcultures” to emerge, if they do at all.

The Standard Library And Its Influence on New Users

File system APIs being in Foundation as opposed to the standard library is probably a temporary condition. Nevertheless, it has at least the following implications:

  1. Its quality of implementation is not held on the same standard that those APIs in the standard library. This is especially true for the separate, open-source Foundation implementation. Getting consistent and correct behaviors across macOS and Linux is hard.

  2. A person learning Swift won’t explore the language with a file system API. This I suspect, is the most important reason many of these great CLI utilites are written in other programming languages. Programmers seek instant gratification when they learn. And they usually stay in a limited domain (like iOS) at first. This is where the built-in library is special: no matter which domain is chosen, it’s always available. Languages such as Go and Rust include things like paths and files in their built-in library. Playing with these APIs while learning the lanugage plants a seed for future, serious, projects. There are less users of these languages compared to Swift, but there are more people thinking about projects that involves file systems in thoes communities. (Note I don’t have statistics here, just a guess.)

Conclusion

The next killer CLI tool is still more likely to be written in Go or Rust, than in Swift. Hopefully, somewhere in these speculations is a true cause of this phenomena. Maybe someone reading this will be inspired to accelerate change that will eventually revert the condition. (I’m trying).