Private default arguments in public functions

Is this intended behaviour or a bug?

private let baz = 4
func foo(bar: Int = baz) {} // this compiles
public func foo(bar: Int = baz) {} // this doesn't

Apparently, the first of these two cases was fixed a while ago, but I'm not sure if the second one was just an oversight or if there is some deeper reason why it shouldn't work.

1 Like

Afaik, foo is considered to be a single function (not an overload without parameter), and so the mental model is that you always supply the argument at the call site (thus it has to be accessible at the call site).
Imho some relaxation of the visibility rules would be nice, but that would arguably increase complexity.

I'm sorry, but I did not understand what you just wrote. Would it be possible for you to reformulate that in more concrete terms? What I find puzzling is that it works if foo is internal, but not if it is public.

My interpretation is that each call to foo() is implicitly transformed to foo(bar: baz), so as long as baz is visible at the call site, foo can be visible as well.
I have to agree that this would also break the first case, so the actual model must be more complex… maybe it's just possible because the rules don't have to be as strict as when you cross module borders (but I guess you need an answer from someone with enough authority to declare either case to be buggy ;-)

The fact that you can write

func log(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
  // ...
}

… and have file, line be interpreted at call-site, gives me this impression as well. However …

func addAction(_ action: Selector, target: Any = self) {
  // ...
}

… does not work like that.

1 Like

I would expect that not to work because there might not even be a self at the call site.

1 Like

This is intended. Up until Swift 5 we had a default keyword printed on imported public API. With Swift 5 you should now see the real default arguments printed, as in public functions all default arguments must also be public.

https://bugs.swift.org/browse/SR-2608

Default arguments are now printed in SourceKit-generated interfaces for Swift modules, instead of just using a placeholder default .

If on the other hand we would allow default arguments with an access level lower than public than this feature won't be possible. Sure you can argue that printing the current default placeholder would be enough, but for public API this is simply not always enough from the perspective of the API user.

4 Likes

What you are observing is intended behavior. There is specific code in the compiler to perform these checks and emit the diagnostic text.

The restriction only applies to public default argument expressions because when you call a public function from outside the module, a copy of the expression is emitted inside the calling module. (The bug you referenced was that when -enable-testing was passed to the compiler, the compiler erroneously enforced these restrictions for internal functions too. This was a bug because we do not want -enable-testing to actually change language semantics in any way; it should be possible to build any Swift module with -enable-testing).

An internal function's default argument can still reference internal or private declarations because only one copy of the default argument expression is emitted, and it is emitted inside the defining module.

This restriction was introduced in Swift 4.0 and the implementation was changed to take advantage of the restriction in Swift 4.2. The reason we wanted to make this change is to not have to emit a public symbol for each default argument expression, with the associated overhead the call entails, when in fact most default arguments are very simple expressions such as literals. The usability improvement of being able to print the default argument expression in a manner that allows the user to copy and paste it into their own code and have it work was another motivating factor.

Swift is lexically scoped, so if it were to work, a default argument expression of self would refer to the self value of the callee, not the caller. That is,

class C {
  func f(_ x: Any = self) {}
}

let c = C()
c.f()

Would be equivalent to c.f(c), not c.f(self). However it doesn't work yet because default argument expressions are not implemented as full closures yet. It would be nice if self was supported, as well as the local function case:

func outer(x: Int) {
  func inner(y: Int = x) {} // capturing 'x' from outer scope here
}

The #line, #file, etc, forms are special syntax that behave completely differently from other default argument expressions. They're not really expressions at all, and take a different path in the compiler, since you cannot use them anywhere other than the top level of a default argument.

If you need to do non-public things you can always wrap your default argument expression in a function or computed property. Then it will still print as a reference to the function or computed property in the generated interface, so the user can call it directly if need be, but the implementation can do whatever you want it to do (and change resiliently).

3 Likes

Thanks for the reply. I think it's a bit of a shame (for encapsulation reasons) and it's also not very intuitive to me, but I can see the reasoning behind it.

Terms of Service

Privacy Policy

Cookie Policy