For testability purposes (and to make things easier to reason about), sometimes it's nice if methods in a class are pure functions, that is, they don't contain any explicit or implicit references to self
, or really anything other than the arguments that are passed in.
In the painfully contrived example below, method
is a method (because it references both a property and another method
using self
) and isEven
is a pure function (because it doesn't).
class MyClass {
let number: Int
init(number: Int) {
self.number = number
}
func method() -> Bool {
return self.isEven(number: self.number)
}
func isEven(number: Int) -> Bool {
return number % 2 == 0
}
}
But with this setup you have to create an instance of MyClass
to be able to test isEven
, even though it's
not actually dependent on any instance properties or methods of the class:
let foo = MyClass(number: 42)
XCTAssertFalse(foo.isEven(number: 5))
One option would be ditch object-orientation and pull isEven
out of the class entirely, but then it will be polluting the global namespace, when its functionality might be fairly specific to MyClass
.
Now you're probably thinking: "If only there were some way to associate a function with a class, but make the compiler ensure that it doesn't reference any instance properties or methods." Or maybe you're thinking, "come on, bozo, that's called a static method", in which case pat yourself on the back because that's where I'm going with this.
As a recovering Objective-C developer, static methods are something that's kind of buried at the bottom of my metaphorical toolbox, partly because they don't see a lot of use by Apple (outside of a few very specific use cases like convenience initializers and singletons), and partly because the syntax is a little awkward ([[self class] isEven:self.number]
).
Up until recently the syntax has been even more awkward in Swift, something like MyClass.isEven(number: self.number)
, which means that if you move the method you have to remember to change MyClass
to whatever class you're now inside of.
But as of a minor version or three ago, you can now use capitalized Self
in instance methods of classes, so the code above becomes:
class MyClass {
// ...
func method() -> Bool {
return Self.isEven(number: self.number)
}
static func isEven(number: Int) -> Bool {
return number % 2 == 0
}
}
And now you can test isEven
without creating an instance of MyClass
:
XCTAssertFalse(MyClass.isEven(number: 5)
The potential downside here is you can still reference self
inside of a static method, but it refers to the class rather than an instance, so you're likely to notice when you're doing this.
Side note: that also brings to mind the possibility of a pure
decorator for function declarations, that would ensure that it doesn't have any side effects and doesn't depend on anything other than its arguments, maybe something like:
pure func isEven(number: Int) -> Bool {
// can't use anything but the arguments to compute the result, and can't modify anything global either.
return number % 2 == 0
}
That would be nice, but I'm not entirely sure that it's within the compiler's abilities to enforce that (but if it is, feel free to steal this for a Swift Evolution proposal). The case above is trivial, but consider this:
pure func makeRequest(_ request: URLRequest, using session: URLSession, completion: @escaping (Bool) -> ()) {
let task = session.dataTask(with: request) { _ in
completion(result)
}
task.resume()
}
Obviously this isn't actually pure, because running it multiple times (with, say, a disconnected network some of the time) may have different results.
Anyway, this is the part where I run up against the limits of my lack of a formal computer science education and have to defer to those who know what they're talking about.