Skip to content

Instantly share code, notes, and snippets.

@frankus
Last active April 29, 2021 16:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frankus/37724113e837f0c2bd6425141a468f1d to your computer and use it in GitHub Desktop.
Save frankus/37724113e837f0c2bd6425141a468f1d to your computer and use it in GitHub Desktop.
Extracting Pure Functions in Swift Without Polluting the Global Namespace

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment