Modifier to make a `func` on a type a free function

Quite often, I encounter the following use case:

class MyClass {
  func firstFunc() {
    callSomeTask(arg: "", completion: {
      // must use `self` here, causes capture
      self.anotherFunc()
    })

    callSomeTask(arg: "", completion: { [weak self] in
      // another way, with `weak self`, too verbose
      self?.anotherFunc()
    })

    callSomeTask(arg: "", completion: { [weak self] in
      // another way, using a `static` func, verbose `Self`
      Self.anotherStaticFunc()
    })
  }

  func anotherFunc() {
    // doesn't reference `self`
  }

  static func anotherStaticFunc() {
    // doesn't reference `self`
  }
}

I'm wondering whether a modifier like free for a function could be useful for these cases to reduce self capture, like so:

class MyClass {
  func firstFunc() {
    callSomeTask(arg: "", completion: {
      // no `self`
      anotherFunc()
    })
  }

  free func anotherFunc() {
    // doesn't reference `self`, checked by compiler
  }
}

Notes: I'm aware that for structs, self capture is implicit now. But even in that case, it causes unnecessary memory pressure by copying/retaining for longer.

1 Like

Isn't free function just a static/global function? :thinking:

7 Likes

That is another solution, however, this causes the function locality to be somewhere else - it's considered good code style to locate local functions together if they're used together.

I'm wondering whether a modifier like free for a function could be useful

Such a modifier does exist. It's called static (or class).

What's verbose about Self? Also, there is no retain cycle in this last example because a static method doesn't have access to self.

1 Like

extensions to the rescue!

There is no reason for all your methods to need to be inside the type declaration itself. The standard library's practice is to declare a type with the minimum (stored properties and basic initializers) and then to close the declaration, and do the rest via extensions.

Once you do this, your local free functions can just be above your method declarations.

8 Likes

There are use cases where this doesn't work, for example if you're already in an extension:

extension SomeViewController {
   struct SomeModel {
       // ....
   }
   static func processSomething(_ input: String) -> String {
       // how do I call this from `SomeModel` now? 
   }
}

There's definitely a way to solve this with extensions, but the side effect is fragmenting a source file into many tiny islands with their own declaration boilerplate, thus actually reducing co-locality on average.

Not to mention that extensions are a semantic construct, and not just a syntax feature.

I'm gonna try to redefine the problem:

  • There's currently a way to define an instance method within a type
  • There's currently a way to define a type method within a type
  • There's no way to declare a free function within a type

You can call it from anywhere in the module using SomeViewController.processSomething

Why can't you just use a static method? By "free" are you referring to a global function? Why would you define a global function within a type? That just doesn't make any sense.

This is precisely the over-verbosity I was referring to - since the call site is within SomeViewController - if it was a free function, the prefixing would have been unnecessary.

A free function is a function that isn't a method. It doesn't have any self. A global function would be a free function in the module scope, which isn't what I want.

I'm still unclear about one thing. In the following example, do I need an instance of MyType to call myFunc on? Or can I just call myFunc as if it was a global function?

struct MyType {
    free func myFunc() { }
}

myFunc()  // is this valid?

At first, I was skeptical of the idea, but I find the rationale to be reasonable. With such functions, we get the following:

  • no capture of self
  • no pollution of the type's enclosing namespace
  • no need for awkward or verbose identifiers to describe scope (MyType.myFunc(...))

It would allow the type definition to act as a namespace for otherwise free (read: global) functions. I don't believe there's a way to accomplish all 3 points today.

1 Like

I think a new declaration syntax is overkill. I would prefer to be able to use static functions in instance scope without explicit namespacing.

(For compatibility, if this leads to ambiguity between instance and static methods, the instance method would be used with a warning and a fixit to add self. to disambiguate.)

7 Likes

Always found strange to use Self.staticMethod when there is no conflict. Most notably when passing method references like .map(Self.transform) being able to remove the Self when there is no conflict would be nice.

3 Likes

If you have a free function defined in MyType, then code located outside of MyType still has to prefix the call with either the type name or instance variable name, right? How is this any less verbose or awkward?

For code located within MyType, the only difference between a free and static method is the Self prefix. I think it would be a huge stretch to call this verbose or awkward. It's only five extra characters.

For these reasons, I think that adding a free function is completely unnecessary.

4 Likes

If that is the last brick in the wall, I would suggest using this strategy and then, outside of the extension, writing a free function that just calls through to this method. Then, You have the association and the free function, both.

As an aside, is what you want a pure annotation that can be used on methods? I admit that I am… fond of the idea so maybe I'm just seeing what I want to see but I think if that is what you want, you might have a different, if not less, frustrating time arguing for it with that phrasing.

What is "this" strategy? Could you write some example code?

A free function is still not necessarily a pure function. Such a function could still read/write global state.

Sorry, it was a response to the suggestion of using a static function.

extension SomeViewController {
   struct SomeModel {
        // can now call SomeViewController.processSomething("hello")
        // or moreElegantName("hello")
        // for the same effect
   }
   static func processSomething(_ input: String) -> String {
       // ...
   }
}

func moreElegantName(_ input: String) -> String {
    SomeViewController.processSomething(input)
}

I too have often come across this problem of wanting to limit visibility of a function to within a type, but have the function not be instance bound.

I think we should keep the static func but extend symbol lookup rules to accept the “naked” function name when calling a static function from a non-static context.

If a static and non-static function share names, it should cause an ambiguity error, and the proposed fixit should be to prepend with either self. or Self.

5 Likes

For sure, because reaching out beyond Self to enclosing types already works that way.

enum ReallyOutThere {
  static func outerStaticMethod() { }
  static func reallyOutThereMethod() { }

  enum Outer {
    static func outerStaticMethod() { }

    struct Inner {
      static func innerStaticMethod() { }

      func instanceMethod() {
        innerStaticMethod() // does not compile; requires `Self.`
        outerStaticMethod() // compiles! Calls `Outer.outerStaticMethod`.
        reallyOutThereMethod() // compiles!
      }
    }
  }
}
11 Likes

That would be a breaking change. Instead, the instance method should take priority over a static method. Furthermore, in the current version of Swift, instance methods already take priority over global methods.

6 Likes