"methods and func's are closures. no difference, just different syntax": is this correct?

I said this on twitter to John Sundell, who I learn a whole lot from.

I said that because I thought that's what the language reference say about closure:

Global and nested functions, as introduced in Functions, are actually special cases of closures. Closures take one of three forms:

  • Global functions are closures that have a name and don’t capture any values.
  • Nested functions are closures that have a name and can capture values from their enclosing function.
  • Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.

I'm confused because he said:

... you can pass any method as if it was a closure, but that doesn't make all methods closures. Think of it this way: you can convert any method into a closure, just like how you can convert any Substring into a String. Doesn't make them the same, just interoperable.

I don't know what to make of this: if you can pass any method as closure, then methods are closure.

Anyway, I need final judgement on this so I can unconfused myself.

1 Like

I think it's just that the terminology is fuzzy here. Formally, the "close" part of "closure" is that it closes over its environment, i.e. it captures variables that are in scope. But in practice we tend to use to it to refer to any kind of function being treated as a value (i.e. assigned to a variable or passed as an argument), regardless of whether it captures something.

I suppose you could also argue that methods are in fact closures formally because they capture the instance variables of self.

Practically speaking: you can pass any kind of function, including methods and closures, as an argument.

7 Likes

I would say you are both right.

In the context of a running program, “methods”, “functions”, and “closures” are the same. They operate in the same way. They serve the same purpose. They are interchangeable. I suspect they are even indistinguishable (although I’m not an assembly expert). This is likely what the language reference had in mind.

In the context of a programmer writing a .swift file, “methods”, “functions”, and “closures” are all different. They are used in different ways. They serve different purposes. They are not interchangeable. They are easily distinguishable. This is likely what Sundell had in mind.

You could compare it to the difference or lack thereof between a fraction and a decimal.

Both fractions and decimals represent a rational numbers, so as far as the operations of the natural world are concerned, they are the same. The rain doesn’t care whether it is falling at 9.8 m/s/s or 9 8⁄10 m/s/s.

But when you use fractions or decimals in writing to communicate information, they have very different properties from one another. On one hand, it is far easier to compare decimals than fractions. (Which is greater, 5⁄7 or 8⁄11? 0.714 or 0.727?). On the other hand, 23⁄43 is precise, whereas writing it as a decimal inevitably incurs a loss of precision: 0.534 883... There are things fractions can do that decimals can’t, and vice versa. And so fractions and decimals can also be thought of as inherently different things which serve inherently different purposes.

3 Likes

He actually does have a point.

Their main difference (besides the way they are declared) is that they have different calling conventions. I'm not intimately acquainted with all of this, so take my explanation with a grain of salt, but here's a simple example that demonstrates one of the implications of these differences:


func foo() {
    let i = 123
    
    // OK, compiles with no problem
    func bar() -> Int {
        return i
    }
    
    class Klass {
        // Error: Class declaration cannot close over value 'i' defined in outer scope
        func baz() -> Int {
            return i
        }
    }
}

— normally, methods do not carry around their context, unlike closures and other functions that can do so if they reference a value outside of them. If you will, methods are just sugared "normal" functions that receive the self parameter behind the scenes: in the following example, foo1() and foo2() are equivalent:

class Klass {
    let i = 10
    func foo1() -> Int {
        return  i
    }
}

func foo2(_ `self`: Klass) -> Int {
    return `self`.i
}

— so any invocation of foo1() really translates to into foo2(instanceOfKlass).

Closures do the same for their captured values: you can actually imagine that all closures are really structs that look like this:

struct Closure<T: AnyObject> {
    let context: T
    let actualFunction: (context: T, params: Parameters) -> ReturnType
}

The thing is that you can't have both: methods expect a parameter of their enclosing type, while closures expect a parameter T that's basically a class containing all those captured values — this is (probably) why he draws the distinction that "closures capture their surrounding context while methods can reference their enclosing type".

But how can you pass a method as a parameter or store it in a variable, you may ask? The compiler will actually wrap your method in an implicit closure, for which this T will contain (among other captured values, if present) the object to act upon.

All this talk, but funnily enough: this mostly doesn't matter (unless you are trying to write something like my first example, which doesn't compile).

2 Likes

I hope someone can explain the exact reason of this error. Maybe this is a bug?

All class/struct methods are closures, thus far, I have not seen any exception to this. If you can define it, you can use it as closure

That has nothing to do with methods and everything to do with local classes:

func foo() {
  let i = 123

  func bar() -> Int {
    return i
  }
  let barClosure: () -> Int = {
    return i
  }

  class Klass {
    // Error: Class declaration cannot close over value ‘i’ defined in outer scope
    func baz() -> Int {
      return i
    }
    // Error: Class declaration cannot close over value ‘i’ defined in outer scope
    let bazClosure: () -> Int = {
      return i
    }
  }
}
1 Like

It's a bug. Function-nested declarations aren't used nearly as much as others, so this kind of thing doesn't get much visibility.

Compiles:

let i = 123

class Klass {
  func baz() -> Int { i }
}

Compiles:

enum Outer {
  static let i = 123

  struct Inner {
    func baz() -> Int { i }
  }
}
1 Like

These values do not get captured (i.e., they only get referenced):

// Local values get captured:
func makeFunc(i: Int) -> () -> Int {
    return { i }
}

let func1 = makeFunc(i: 1)
let func2 = makeFunc(i: 2)
print(func1()) // Prints "1" — does not get affected by the second call

// Global values do not get captured:
var global = 1
class Klass {
  func baz() -> Int { global }
}

let klass = Klass()
let baz1 = klass.baz
print(baz1()) // Prints "1"
global = 2
print(baz1()) // Prints "2" — if `global` were captured, would've printed "1"

— and because static properties are just namespaced globals, the same applies to them.

1 Like

These are really all very different beasts. The last example is the most notable, as even when we try declaring it top-level (i.e., not locally in a function), we can still miscompile:

class Klass {
    var i = 1

    // error: cannot use instance member 'i' within property initializer; property initializers run before 'self' is available
    let bazClosure: () -> Int = {
      return i
    }
}

— if it wasn't about closures vs. methods, you wouldn't see this error. But because the compiler tries to treat bazClosure as an actual closure and capture self, you can only instantiate it after Klass gets initialized. Changing let bazClosure to lazy var bazClosure solves the problem.

An even better way to see what's going on is to try declaring it in a struct instead of a class:

struct Strukt {
    let i = 1

    // Error: Escaping closure captures mutating 'self' parameter
    lazy var bazClosure: () -> Int = {
      return i
    }
}

bazClosure is clearly being treated as a closure, not as a method.

As to why it is so — it is a different question, and I personally don't see where it gets a "mutating self parameter" (probably it's the lazy initializer itself, since it ought to mutate the struct) — but hopefully you can clearly see that this behaviour is quite different from how methods usually operate.

I would also not be surprized if the bazClosure in the class caused a reference cycle, which is obviously not what normal methods would do.


A yet another piece of "evidence":

struct Strukt1 {
    let closure: () -> Int = { return 1 }
}

print(MemoryLayout<Strukt1>.size) // prints "16"

struct Strukt2 {
    func method() -> Int { return 1 }
}

print(MemoryLayout<Strukt2>.size) // prints "0"

— 16 bytes from the first example being two words, exactly the size of a thick function pointer that is required to store a closure.

1 Like

In case it got swept by the wayside. It is absolutely not a bug.

Class declarations cannot capture local contexts. They only capture global context as needed (instance methods technically also capture self). Allowing such capturing requires a lot more overhead than you'd think, and the resulting behaviour likely won't make sense.

Normally, you shouldn't need to capture variables from instance methods since you can simply store it in the object.

To note, this also applies to essentially everything, not just methods,

func foo() {
  let i = 0

  class A {
    var a = { i } // error
    var b = i // error
    func c() {
      i // error
    }
  }
}
2 Likes

On the terminology: I always thought that "capturing" specifically means copying a certain value and storing it alongside whatever entity that captured it (this is why I said here that global variables only get referenced instead of captured).

The reason for this bit of pedantry is that global values referenced in instance methods will not be protected from race conditions (as they are shared), unlike values that would've been copied. If the word "capturing" also refers to those cases, how could we disambiguate?

We can probably define it like that. Though it doesn't help us much in presence of @convention(c) and the likes. We could say that they can neither capture nor reference, but then it's just another concept that one needs to learn.

Not that it's a bad thing. Implicit capturing is already different from explicit one (with capture list), but they both constitute the context that are captured by the closures.

The act of copying itself can be raced too. It's not a property of referencing per se. If anything, since the language has no concept of concurrency (until recently), I'd be surprised if we can speak about such things using pure in-language terminologies.

1 Like

That's not generally how the term is used in Swift as I understand it. Capturing by default is by reference, and can be made to be by value by sticking the captured value in an explicit capture list ([foo]).

Also, this from above:

Is not quite accurate. Yes, each invocation of makeFunc creates a different copy of i, but it's not the case that this means that i gets copied by the capture (as is implied by saying it gets "captured" instead of "referenced"). E.g.,

func makeFuncs(_ i: Int) -> (increment: () -> Void, print: () -> Void) {
    var i = i
    return ({ i += 1 }, { print(i) })
}

let funcs = makeFuncs(0)
funcs.print() // 0
funcs.increment()
funcs.print() // 1

If we instead capture i in the second closure by value:

return ({ i += 1 }, { [i] in print(i) })

then the output becomes:

0
0
2 Likes

Oh alright, then it's indeeed quite difficult to talk about this stuff succinctly! I meant precisely that "each invocation of makeFunc creates a different copy of i" into a new context — in contrast to instant methods which just plain reference a single global value at a specific address.

1 Like

Right, makes sense. I just wanted to point out that this is a property of how parameter declarations work in Swift versus global variables that is independent of the semantics of capturing.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy