Daniel Duan's blog

Magical Interactions

I want to talk about this little function:

func interact(_ process: (String) -> String) {
    var input = ""
    while let line = readLine() {
        input += line
    }

    print(process(input))
}

Brief explanation: it reads all the input from stdin as a String, feeds it into a closure process, which it takes in as its only argument, and prints process’s output.

Here’s how one might use it:

// counts characters from stdin and prints result to stdout
interact { String($0.count) }

Got that? Well, now I’m going to rewrite it in a slightly less Swift-y way:

let count: (String) -> String {
    return String($0.count)
}

interact(count)

The argument for interact got defined with a name and an explicit type signature.

So, what’s so special about this interact function? Two words: side effects. More precisely, it took away the concern of side-effects from the user. count belongs in the realm of pure functions. It has no worries about file handles or operating systems. It’s (String) -> String. I wanted to emphasize this with the rewrite. Look at that empty line. Now you see a boundary between 2 worlds.

This may all seem contrived. But when I learned about this function in Haskell, I was blown away.

It’s like a great magic trick: you are presented a scenario, say, writing a little script. Maybe you need to process some CLI output and print out a CSV or JSON string (literally 90% of the script I write). A Haskell programmer would jump into the bottom level of the problem and start writing these little pure functions: one to split the string, one to convert some numbers, one to manipulate a list, one to match some patterns…gradually the broken-down absractions get built back up via function composition. You can see the light at the end of the tunnel, yes, yes! If you feed this list into that function that returns a string you’ll have the right value to print out! Okay, now the problem is solved in the pure functional world! The only thing left to do is…

Now, the setup of the magic is complete. Now, you are onboard with the solution, you thought the problem through with the magician…you are distracted. The ending came so…quickly, but unexpected. What? You just feed your solution to this interact function and…that’s it? I was expecting some readLines or prints (okay, at least 1 print statement)!

That’s the thing: interact deals with two side effects, the input and the output. But its user deals with zero. It’s as if the two effects “cancel” each other out! It’s a neat trick, really. Small, low-key, easy to miss. But I’m glad I noticed it and come to appreciate its power and simplicity.


🔗 [Accepted with Modifications] SE-0225

This announcement comes with some answers to “what should be in the Swift standard library?”

Notably:

Almost all library features can be composed from existing features with enough effort. To be considered for addition to the library, a proposed feature must satisfy two conditions: it must provide functionality that is useful to a substantial population of Swift programmers, and it must provide substantial advantages over the alternative ways of accomplishing that functionality. Potential advantages include:

It may be complex, challenging, or error-prone for users to implement themselves. It may have substantial performance advantages over a user implementation, either because it has access to library internals or just because the library implementation will likely be more carefully tuned. It may be substantially easier to work with because it composes better with other language or library features. It may be substantially more “fluent”: that is, more natural to discover, use, and read in code. The implementation may involve composing primitives in a subtle or tricky way, or the primitives may be unfamiliar to many programmers. This is a more subjective criterion than the others, and people may reasonably differ about how to apply it. For example, consider adding an method to rotate the elements of an array. This operation can be performed using a relatively simple composition of slicing and concatenation:

array = array[amount...] + array[..<amount]

However, a method to rotate an array in-place would still be welcome: it would have substantial performance advantages, it would avoid several potential off-by-one errors, and it would be significantly easier for a reader to recognize as a rotation.

In contrast, a property like isNotEmpty offers no substantial advantages over !isEmpty, and adding it would open the door to adding trivial negations of essentially every predicate.


A Tale of Two Dates

Recently, I discovered a curious thing about Dates in two large projects I work on. Simply put, both projects receives, from various HTTP endpoints, the same object component: a timestamp, and a duration. Combining these two pieces, both projects derives two Foundation.Dates to represent a time range. So far, so good.

However, project A uses Fonudation.DateInterval to represent this concept, while project B uses Range<Date>. But why? Why represent the same component differently? What a gigantic waste of brain power for everyone on both projects!

So I set out to unify this thing. Wherever a Range literal is used, I swap in DateInterval.init(start:end:); Range.lowerBound becomes DateInterval.start; Range.upperBound becomes DateInterval.end, etc. It didn’t take long to complete the conversion to DateInterval in project B, now it builds and runs!

Oh, wait, why are some tests failing in project B? Shouldn’t this just be an mechanical change?

I spent time investigating. The failing tests are for some very specific business logic that I’m not familiar with. So things took a while to become clear. What felt like a long time later, I realized my mistake.

(I’m sorry if this has been obvious to you. You are a better Swift programmer!)

Somewhere in project B is the following:

struct Item {
    let range: Range<Date>
}

struct Container {
    let items: [Item]
}

func containerFactory(range: Range<Date>, items: [Item]) -> Container {
    /// pretend there's more code here

    return Conatiner(items: items.filter { $0.range.contains(range.lowerBound) })
}

And of course, after my “refactor”, it became

struct Item {
    let range: DateInterval
}

struct Container {
    let items: [Item]
}

func containerFactory(range: DateInterval, items: [Item]) -> Container {
    /// pretend there's more code here

    return Conatiner(items: items.filter { $0.range.contains(range.start) })
}

Tests for containerFactory failed. And here’s why: DateInterval.contains is inclusive for its upper bound (.end), whereas Range.contains isn’t! You can see it plainly by running the following

import Foundation

let sooner = Date(timeIntervalSince1970: 0)
let later = sooner.addingTimeInterval(1000)

DateInterval(start: sooner, end: later).contains(later) // true
(sooner..<later).contains(later)                        // false

So here’s what stumped me:

  1. The 2 projects chose to interpret the same component differently, which I didn’t not expect.

  2. I didn’t know how Foundation.DateInterval works.

Well, today I learned.