Swift: subscript vs. callAsFunction

Viewed 205

What's the difference between subscript and callAsFunction? From my point of view, they behave almost the same, they both can have argument labels, default values, etc.

Is there any real difference between the two, except that subscript uses square brackets instance[index] but callAsFunction uses parentheses instance(index)?

BTW, we know that subscript can have a setter, how about callAsFunction? Can it have a setter too?

3 Answers

Subscripts can be set to read-write or read-only via the getter and setter methods. A callAsFunction() is a method that allows you to have parameters to perform some steps or functions.

Subscripts

Taking a look at the swift documentation on subscript we have something like this.

Source: https://docs.swift.org/swift-book/LanguageGuide/Subscripts.html

subscript(index: Int) -> Int {
    get {
        // Return an appropriate subscript value here.
    }
    set(newValue) {
        // Perform a suitable setting action here.
    }
}

callAsFunction()

Calling a callAsFunction() does NOT have getters and setters. You might have something like this that returns a computed value.

Source: https://www.hackingwithswift.com/articles/212/whats-new-in-swift-5-2

struct StepCounter {
    var steps = 0

    mutating func callAsFunction(count: Int) -> Bool {
        steps += count
        print(steps)
        return steps > 10_000
    }
}

var steps = StepCounter()
let targetReached = steps(count: 10)

we know that subscript can have a setter, how about callAsFunction? Can it have a setter too?

It can't, because callAsFunction is just a regular method declared in your class/struct. Being able to call it without its name is just syntactic sugar achieved with compiler magic.

And that's pretty much the only practical difference between subscripts and callAsFunction.

But IMO the more important difference between them is not functionality-wise, but style-wise. Subscripts and function calls express different semantics. Subscripts express a "access/set something" semantic, and function call are more like "do something".

This idea is supported by Apple's Swift API Design Guidelines:

Describe what a function or method does and what it returns

Describe what a subscript accesses

Clearly, functions are supposed to do stuff, and subscripts are used to access stuff.

Let's use the example from here:

struct Dice {
    var lowerBound: Int
    var upperBound: Int

    func callAsFunction() -> Int {
        (lowerBound...upperBound).randomElement()!
    }
}

let d6 = Dice(lowerBound: 1, upperBound: 6)
let roll1 = d6()
print(roll1)

Imagine how this would look if it were written with a subscript:

subscript() -> Int {
    (lowerBound...upperBound).randomElement()!
}

let roll1 = d6[]

It works, but d6[] just doesn't express the meaning of "roll the dice" as much as d6() does. It feels like I am accessing something in d6.

On the contrary, imagine dictionary accesses were instead like function calls:

let studentScores = ["Tom": 100, "Jack": 80, "Harry", 70]
let tomsScore = studentScores("Tom")

It feels like I am doing an action called studentScores. It would have been better if I renamed the dictionary to studentScoresFor, but that's a weird name for a dictionary...

Methods and subscripts currently have different capabilities. But they're converging. (e.g. Subscripts gained generics/static, and callAsFunction uses the same syntax as a subscript.)

There is no reason for both to exist anymore. They came from a before-modern-Swift time. Anyone who thinks they have inherent different meaning only has that idea because they've learned it and now find it "natural". Beware the dogma.

As it is, you need to use subscripts if you want to use a value after an equals sign. If this feature ever makes it into Swift, it will probably be in the form of a "named subscript". Once people get used to that, you'll see more people, like yourself, start to ask your question.

Then, either one of the syntaxes will be deprecated, or we'll wait until Swift's successor to see that.

Here's what named subscripts look like:

final class Class {
  var bools: ObjectSubscript<Class, String, Bool?> {
    .init(
      self,
      get: { object in
        { object._bools[$0]
          ?? Bool(binaryString: $0)
        }
      },
      set: { $0._bools[$1] = $2 }
    )
  }

  private var _bools: [String: Bool] = [:]
}

let object = Class()
let ghoul = ""
XCTAssertEqual(object.bools["1"], true)

XCTAssertNil(object.bools[ghoul])
object.bools[ghoul] = false
XCTAssertEqual(object.bools[ghoul], false)
/// An emulation of the missing Swift feature of named subscripts.
/// - Note: Argument labels are not supported.
public struct ObjectSubscript<Object: AnyObject, Index, Value> {
  public typealias Get = (Object) -> (Index) -> Value
  public typealias Set = (Object, Index, Value) -> Void

  public unowned var object: Object
  public var get: Get
  public var set: Set
}

public extension ObjectSubscript {
  init(
    _ object: Object,
    get: @escaping Get,
    set: @escaping Set
  ) {
    self.object = object
    self.get = get
    self.set = set
  }

  subscript(index: Index) -> Value {
    get { get(object)(index) }
    nonmutating set { set(object, index, newValue) }
  }
}
var set: Set = [1, 2, 3]

XCTAssert(set.contains[3])

set.contains[1] = false
XCTAssertEqual(set, [2, 3])

var contains = set.contains

contains[3].toggle()
XCTAssertEqual(set, [2])

contains.set = { _, _, _ in }
contains[2].toggle()
XCTAssertEqual(set, [2])

var four: Set = [4]
withUnsafeMutablePointer(to: &four) {
  contains.pointer = $0
  XCTAssert(contains[4])
}
public extension SetAlgebra {
  var contains: ValueSubscript<Self, Element, Bool> {
    mutating get {
      .init(
        &self,
        get: Self.contains,
        set: { set, element, newValue in
          if newValue {
            set.insert(element)
          } else {
            set.remove(element)
          }
        }
      )
    }
  }
}
/// An emulation of the missing Swift feature of named subscripts.
/// - Note: Argument labels are not supported.
public struct ValueSubscript<Root, Index, Value> {
  public typealias Pointer = UnsafeMutablePointer<Root>
  public typealias Get = (Root) -> (Index) -> Value
  public typealias Set = (inout Root, Index, Value) -> Void

  public var pointer: Pointer
  public var get: Get
  public var set: Set
}

public extension ValueSubscript {
  init(
    _ pointer: Pointer,
    get: @escaping Get,
    set: @escaping Set
  ) {
    self.pointer = pointer
    self.get = get
    self.set = set
  }

  subscript(index: Index) -> Value {
    get { get(pointer.pointee)(index) }
    nonmutating set { set(&pointer.pointee, index, newValue) }
  }
}
Related