Swift is brand spanking new. How can we possibly be expected to write idiomatic code? On the other hand, Objective-C has been around for more than thirty years. We know what it looks like and feels like. The Objective-C of our iOS youth is very different than today’s Objective-C. Seen in this light, Swift is more evolutionary than you might think.
In this do {iOS} talk by Daniel Steinberg, we’ll look at Swift code in the light of the code of Objective-C and other languages that have come before it, and learn how to write code that is a pleasure for others to read. Join us in making lasagne with Daniel’s delicious, readable, and testable recipe.
Lasagne Coding (0:00)
I want to start with a recipe for lasagne. This lasagne is context-specific: it depends on what you know about each one of these steps. For a chef, each one of those steps means one thing; for a home cook, they mean something else. For example, bolognese sauce for a home cook might mean they know how to cook it, but it also might mean they go and get a jar of it to open.
Béchamel sauce is a different matter altogether. Béchamel is actually an important sauce to cooks, and it’s not really good from a jar. It’s what we call a “mother sauce.” (In classic French cooking, there are five mother sauces, and a lot of sauces stem from those.)
We might have a recipe for lasagne, and it might have to break out what we mean by a béchamel sauce. First, you make a roux out of butter and flour, you add milk to it, then you season it to taste. That gives you a pretty good sauce for lasagne.
Objective-C Context (3:02)
Despite what you’ve heard, Objective-C isn’t dead. There’s nothing wrong with Objective-C. It’s a lovely language, but you won’t find work if you stay with it. In an example from a class I teach, there’s just a simple timer. I want to show you a little bit of the model.
There’s the elapsedTime
method, and that’s what returns the time being displayed. That’s what gets the callback. Classically, we create a new instance of an NSDate
with alloc init
. Do you Swifters remember alloc init
? We will then calculate the elapsed time. One of the things in Swift that I really miss is an asterisk, because I can look at this code and tell that an NSDate
is a reference type and an NSTimeInterval
isn’t. In Swift, we don’t have that clue, which is kind of a shame.
- (NSTimeInterval)elapsedTime {
NSDate *now = [[NSDate alloc] init]
NSTimeInterval elapsedTime = [now timeIntervalSinceDate: self.startTime];
return elapsedTime;
}
We calculate the elapsed time and then we return it, and that’s what our elapsedTime
method does. If you’re aggressive, these three lines can be replaced with this one line.
return [self.startTime timeIntervalSinceNow];
There’s a timeIntervalSinceNow
, and this is wonderful, except the result is a negative time. Context.
We want to get rid of that negative. We can get rid of it in code by placing a negative out front of that, but then every time someone gets to that line of code, they have to stop and think, “Why is there a minus sign out front?”
return -[self.startTime timeIntervalSinceNow];
The context that the minus sign belongs to is missing. It’s not in your head when you’re looking at this code. The problem is that Cocoa has a method, timeIntervalSinceNow
, and what we really wish it had was a method called timeIntervalUntilNow
.
We know how to use categories, so let’s make one up. We won’t worry about name spacing for now.
@interface NSDate (TimeCalculations)
- (NSTimeInterval)timeIntervalUntilNow;
@end
This is our header file, which I miss from Objective-C too. It tells me exactly what I’m allowed to call. Other than that, I’m all in with Swift. Love it.
Here’s a category on top of NSDate
that I’m calling TimerCalculations
. I declared this method that I want, and then I jump over to the implementation file, and I implement the method. timeIntervalUntilNow
is what I get when I return the negative of timeIntervalSinceNow
.
@implementation NSDate (TimeCalculations)
- (NSTimeInterval)timeIntervalUntilNow {
return -[self timeIntervalSinceNow]
}
@end
You may say, “Well, Daniel, it’s got that same minus sign.” Yes, but now it’s in context. That minus sign is completely documented. I say that timeIntervalUntilNow
is the negative of timeIntervalSinceNow
, and it makes complete sense to me and to other people that come to my code. By providing context, you’ll write code that’s clean, clear, and like lasagne.
No more confusing things here; I’m just calling timeIntervalUntilNow
, and in the béchamel sauce is where I have my negative sign. That’s nice and clean.
Swift Timer Example (7:22)
Now, it would be cleaner if we didn’t need self
and square brackets and semicolons, but perhaps I get ahead of myself. Let’s look at the model in Swift.
struct Timer {
let startTime = NSDate()
var elapsedTime: NSTimeInterval {
return startTime.timeIntervalUntilNow
}
}
In Swift, the model is a little cleaner. I love properties in Swift. In Objective-C, I have to import a header file, declare a property, initialize it in viewDidLoad or somewhere else. I love that I could just create a new NSDate
instance like this.
In my Timer
struct and I create my elapsedTime
- I don’t know if you’ve noticed, but in Swift we have a tendency to prefer to implement simple methods as a computed property instead.
extension NSDate {
var timeIntervalUntilNow: NSTimeInterval {
return -timeIntervalSinceNow
}
}
We just call startTime
and timeIntervalUntilNow
, and we don’t implement that in a category anymore. We implement it in an extension in the same file so it doesn’t pollute our code base We extend NSDate
and just return the opposite of timeIntervalSinceNow.
To me, this is a description of the “sauce on the side”. This is one of the sauces. In the same file, I provide detail, so we’re creating a nice lasagne here, with code that is clean, compact, clear, and testable. It’s a bit of a cheat since we started with Objective-C and converted to Swift, so we’re not really doing anything hard or clever, yet you get pretty far.
When you’re first learning Swift coming from Objective-C, you think you’ll just write Objective-C things using a new syntax. But that’s not thinking in Swift; you’re just transliterating. To think in Swift, we have to do things differently.
App Sales: Swift & Dangerous (9:46)
Say I want to track app sales for a week. I want seven data points. I’m going to do it randomly, using GameplayKit, and I’m going to create a SequenceType
.
import GameplayKit
struct AppSales: SequenceType {
let numberOfDays: Int
let randomDistribution = GKGaussianDistribution(lowestValue: 0, highestValue: 10)
func generate() -> AnyGenerator<Int> {
var count = 0
return anyGenerator({
if count++ < self.numberOfDays {
return self.randomDistribution.nextInt()
} else {
return nil
}
})
}
}
I love this call to GameplayKit. This call says, “I’m creating a Gaussian distribution between zero and 10, and it means most days I’ll sell around five copies.” I’ll get the hump of my Gaussian distribution, with a little less on either side. So I’ll sell about five copys per day.
The generate
method is where we’re going to generate these things that make up our SequenceType
. So, if I haven’t gotten to the number of days that I’ve specified, then I’m going to go ahead and give you the next integer in my Gaussian distribution. Otherwise, if you’ve exhausted the days of the week, I’m going to return nil.
let lastWeeksSales = AppSales(numberOfDays: 7)
SequenceType
has become really important to Apple. Map, filter, reduce have all been moved into the protocol extension for SequenceType
.
Icreate an instance of last week’s sales that feels like an array, but it’s not. Remember, it is a SequenceType
, so there are things I can do.
Fast enumeration is implemented on SequenceType
in a really nice way; it just calls next, next, next, until it gets to nil, and then it stops. That’s how fast enumeration works. So for in
works really nicely. Once I’ve got last week’s sales, I can just fast enumerate through it.
let lastWeeksSales = AppSales(numberOfDays: 7)
for dailySales in lastWeeksSales {
print(dailySales)
}
-> 6, 5, 6, 4, 6, 4, 3
I print out my daily sales. Notice that they’re clustered around five. I’ve got my types.
The other thing is that because AppSales
is a SequenceType
, it supports all of map, filter, reduce, and flatMap, but if you feed it a SequenceType
, what it gives you back is an array. If you feed it a SequenceType
that is a sequence of Int
s, it’s going to give you back an array of Int
s by design.
Mapping With Context (12:04)
let lastWeeksSales = AppSales(numberOfDays: 7)
let lastWeeksRevenues = lastWeeksSales.map { dailySales in
Double(dailySales) * 1.99 * 0.70
}
We look at last week’s sales, and we want to generate the revenues from the sales. We start with that array of Int
s that isn’t really an array, but a SequenceType
, and we map it with a closure. We take today’s sales, we have to turn it into a Double
. (You don’t get automatic type promotion in Swift.)
I’m selling it for $1.99, and I get to keep 70%. When I look at the results, the Double
s that I get out have lots of decimals, which wouldn’t look great in a spreadsheet.
I think it’d be better if I specified what this $1.99 referred to, because $1.99 for me is in dollars, and 70 is in percents. That’s hard for me to remember, because there are two Double
s there. There’s no way to think of one as being different from the other because we aren’t carrying our context with us.
let lastWeeksSales = AppSales(numberOfDays: 7)
let unitPrice = 1.99
let sellersPercentage = 0.70
let lastWeeksRevenues = lastWeeksSales.map { dailySales in
Double(dailySales) * 1.99 * 0.70
}
I take out the $1.99, and instead I do this little explaining variable. The unit price is 1.99 dollars, so I’m calculating using the daily sales x the unit price.
By the way, you can’t just type “.70” in Swift and have it know that you mean a Double
. I need to explain what that is. All of a sudden, this calculation communicates better to me. I take my daily sales, I multiply it by the unit price and by the seller’s percentage, and that’s the money I’m going to keep.
I have an explaining variable for the unit price and for the seller’s percentage. Why don’t I do the same thing for the closure? Why don’t I pull up that closure, and make a function called revenuesForCopiesSold
? I calculate last week’s sales by mapping the daily sales and revenues for copies sold, and I pretty much explained everything I’m doing. I feel confident that when I come to this, I can dig in deep and find out what my béchamel sauce is. This tells me the big picture, and if I have to dig in deeper, I can.
let lastWeeksSales = AppSales(numberOfDays: 7)
let unitPrice = 1.99
let sellersPercentage = 0.70
func revenuesForCopiesSold(numberOfCopies: Int) -> Double {
return Double(numberOfCopies) * unitPrice * sellersPercentage
}
let lastWeeksRevenues = lastWeeksSales.map { dailySales in
revenuesForCopiesSold(dailySales)
}
Writing Readable Code (14:39)
You aren’t really a Swift developer if you don’t use $0
. Actually, I prefer this version, where I used the parentheses. I know it’s not a trailing closure, but I just put the method name there and I feel pretty good about that.
let lastWeeksRevenues = lastWeeksSales.map( revenuesForCopiesSold )
If one map is good, two maps is clearly better. I’m going to make the distribution wider. It’ll go from -5 to 15, so it still averages at five, but I’m going to get more values that are further from five.
let randomDistribution = GKGaussianDistribution(lowestValue: -5, highestValue: 15)
Negative sales don’t make me happy, so I’d like the negatives to count as zero. I could filter copies and only keep the ones greater than zero. I would just filter the elements that are greater than zero. Big win, I used $0
. I’m feeling like a programmer.
let lastWeeksRevenues = lastWeeksSales.filter{$0 > 0}
.map( revenuesForCopiesSold )
Solving Unreadable Pain (16:49)
I have a week’s worth of sales, then I filter it, and I might have fewer than seven values. That might matter, or that might not matter. In this case, it doesn’t (but it might!). I want to keep brackets of seven along the way. Since filter
can change the size of the array, I might instead want to map anything that is negative to zero.
I just change anything that’s a negative number to zero, and I reason to myself, “Doesn’t happen that much. It’s outside two standard deviations…” And I really did that so that I could show you this code.
let lastWeeksRevenues = lastWeeksSales.filter{$0 > 0 ? $0 : 0}
.map( revenuesForCopiesSold )
Because now, I really feel like a programmer. I’ve got $0
and 0
confused so much that I can’t remember what’s what, and for the win, I have the ternary operator in there. At least somebody on my team won’t understand this, and so I win one today.
func negativeNumbersToZero(number: Int) -> Int {
return max(0, number)
}
let lastWeeksRevenues = lastWeeksSales.map( negativeNumbersToZero )
.map( revenuesForCopiesSold )
Again, I’m going to pull out that calculation, the target of the map, into its own function. The function negativeNumbersToZero
returns whatever’s bigger, zero or the number, and now lastWeeksRevenues
is looking pretty good. I’m going to first map the negative numbers to zero, and then I’m going to figure out what the revenues are for copies sold.
I know this may look overly simplified, and I’m okay with that. This is feeling pretty readable to me, it’s feeling like the steps in lasagne. However, I’m not happy with the map there.
Why am I exposing that to you? Why do you need to know that that’s the process I’m using to find these things? Why don’t you put it into its own function? And so, I’m going to come back to the lasagne, and show you how to deal with that.
First, though, I want to return to the béchamel sauce (as I torture this metaphor). Here again we have this function, revenuesForCopiesSold
, which returned a Double
with an unclear representation. In Swift, we’ll create a new type for it, because it’s really cheap and easy compared to Objective-C.
typealias USDollars = Double
func revenuesForCopiesSold(numberOfCopies: Int) -> USDollars {
return Double(numberOfCopies) * unitPrice * sellersPercentage
}
I will just say, please use USDollars
for Double
, and now I know exactly what it represents. My revenuesForCopiesSold
returns USDollars
, so I know what it’s measured in.
func toTheNearestPenny(dollarAmount: USDollars) -> USDollars {
return round(dollarAmount * 100)/100
}
I probably want to round it off to the nearest penny.
Now, I can combine them into one function: revenuesInDollarsForCopiesSold
which takes the number of copies you sold and returns the US dollars, and it does it with this calculation on the bottom row which takes the numbers of copies, calculates the revenues for copies sold, and then uses that to round it to the nearest penny.
func revenuesInDollarsForCopiesSold(numberOfCopies: Int) -> USDollars {
return toTheNearestPenny(revenuesForCopiesSold(numberOfCopies))
}
As I read it, it seems backwards. We started on the inside and we worked our way out.
Responsible Custom Operators & Generics (21:58)
Now I will reluctantly show you a custom operator.
infix operator » {associativity left}
You may recognize this as the bullet from the lasagne recipe. I’m going to define an infix operator
, which goes between the two things that it operates on. If I’ve got a bunch of these in a row, I’ve got to tell you what order to do them in, so I’m going to associate it from the left. As you read left to right, I’ll do the first one, and then it gets piped into the next one, so they’re going to associate from the left.
func »<T, U>(input: T,
transform: T -> U) -> U {
return transform(input)
}
Ooh boy, there’s a generic. The infix operator
takes two pieces of input: some data and some function, and it does something with it. We can follow these types and see that the input is of type T, and the transform maps something of type T to type U.
This is generics used for good. It’s saying that whatever type of thing you input must match the parameter of the function. And, whatever the output of the function is, that’s going to be the return type of this operator.
At this point, people teaching functional programming with generics will say, “Well, clearly this function can only return one thing.” You’ve got to remember that “clearly” is in the eye of the beholder. Before you’ve seen this and gotten used to this, it’s not clear at all. Once someone explains it to you the first time and you’ve seen it, through familiarity you realize that you now have the context.
The only thing you can really do is apply the function to the input, and then you get something of type U. And so, if I’ve got some element and some function with this operator in between, I’m going to apply this function to that element, and that’s going to give me my output. In a way, I’m turning around the order in which I do things.
I feel dirty, because I’ve created a custom operator after telling people to not do that. But you’re going to feel good in a moment, because we’re going to use it. Like many things, using it might convince you that it’s a good thing.
func revenuesInDollarsForCopiesSold(numberOfCopies: Int) -> USDollars {
return numberOfCopies
» revenuesForCopiesSold
» toTheNearestPenny
}
Now, I start with my number of copies and I use my operator to pipe that into my revenues for copies sold, and that gives me something that I calculate, associative left, so that gives me something. Then I pipe that result into my toTheNearestPenny
. Now it feels nice.
Back to Lasagne, the Team Player (25:35)
I calculated lastWeeksRevenues
, and I had these two maps, one to the next. I want to turn this into something that looks like piping one to the other. I have to think about what map does, why map applies here, and so I’m going to move map. I’ll pull these two maps into functions.
func replaceNagiveSalesWithZeroSales(sales: [Int]) -> [Int] {
return sales.map(negativeNumbersToZero)
}
func calculateRevenuesFromSales(sales: [Int]) -> [USDollars] {
return sales.map(revenuesInDollarsForCopiesSold)
}
Notice that it’s still doing the same map, but I can reason through what it does. I give you an array of Int
s, and I’m going to map the negative numbers to zero. Not only is that small and clear, but I can write unit tests for that.
The second method also takes an array of Int
s and calculates the revenues in dollars for the copies sold. Small methods that do one thing. Small methods that I can test with input, and see how they do. Except… the types don’t match.
That’s why I used a SequenceType
earlier, because lastWeeksSales
is a SequenceType
, not an array of Int
s. My first method is expecting an array of Int
s. If two maps are good, three maps are better.
func anArrayOfDailySales(rawSales: AppSales) -> [Int] {
return rawSales.map{$0}
}
It just outputs an array, and it takes whichever element you put in as the next element. So it’s just going to convert.
I can chain these guys together, and that’s looking like lasagne:
let lastWeeksRevenues = lastWeeksSales
» anArrayOfDailySales
» replaceNagiveSalesWithZeroSales
» calculateRevenuesFromSales
I take lastWeeksSales
, and from it I calculate an array of daily sales, I replace the negative sales with zero sales, and then I calculate the revenues from those sales, and I’m feeling that’s just like lasagne. It feels just like this to me. It’s readable code, and I know that makes you nervous. But you should learn to favor this.
Someone can come to your code and figure out what you’re doing. I know you don’t keep your job that way, so instead, you do this to your recipe. I don’t know which ingredients do you mean by this:
let lastWeeksRevenues = lastWeeksSales
.map{$0 > 0 ? $0 : 0}
.map{round(Double($0) * unitPrice * sellersPercentage * 100) / 100}
This is the code that you write because this makes you feel better. We write this stuff in place, and we figure that if you can’t understand it, you really shouldn’t be on my team. In our hearts, some of us believe this.
I don’t want to work on your team. I want to work on a team that writes code that they want to read later, and that they want me to read. I’d rather take my code, and when I need to know more about one of these guys, I zoom in on it:
let lastWeeksRevenues = lastWeeksSales
» anArrayOfDailySales
» replaceNagiveSalesWithZeroSales
» calculateRevenuesFromSales
func calculateRevenuesFromSales(sales: [Int]) -> [USDollars] {
return sales.map(revenuesInDollarsForCopiesSold)
}
func revenuesInDollarsForCopiesSold(numberOfCopies: Int) -> USDollars {
return numberOfCopies
» revenuesForCopiesSold
» toTheNearestPenny
}
func revenuesForCopiesSold(numberOfCopies: Int) -> USDollars {
return Double(numberOfCopies) * unitPrice * sellersPercentage
}
Each piece communicates in place. Each piece is clear to me while I’m thinking about it. I can hold it in my head, but I don’t need to hold the whole thing in my head. If something goes wrong, I can trace where that happened by looking at which test failed. We can zoom in until it’s completely clear to us.
Our code is now clean, clear, and testable. We feel good about ourselves, and it provides the context for each piece of information.
For a discussion of how to possibly remove the custom operator with transducers, check out the video at the top at this timestamp: (29:48)
Receive news and updates from Realm straight to your inbox