[Pitch] Optional parameters in functions should default to `nil`, allow omission

pitch

Currently in Swift¹ if you create a function with an optional input:

func takesOptional(foo: Foo?)

You cannot omit the foo parameter and just call:

takesOptional()

You have to type takesOptional(foo: nil), which is wordy and doesn’t match with Swift’s behavior for variables, e.g.:

var foo: Foo?
let bar = foo // works fine, bar is set to nil

real-world use case

While I think it’s totally reasonable to assume nil for any optional function parameter (that doesn’t already have a value specified), it’s especially annoying that this isn’t the case in older or poorly-written APIs that haven’t been updated for Swift. For example NSImage has:

open func cgImage(forProposedRect proposedDestRect: UnsafeMutablePointer<NSRect>?,
                    context referenceContext: NSGraphicsContext?,
                    hints: [NSImageRep.HintKey : Any]?) -> CGImage?

Which (besides the cardinal sin of taking a CGRect by reference) doesn’t provide default = nil values and so requires:

let cgImage = image.cgImage(forProposedRect: nil,
                   context: nil, hints: nil)

Instead of the infinitely-nicer:

let cgImage = image.cgImage()

source incompatibility

The only source incompatibility I can think of with this proposal is that in an existing API if there already exists two functions / methods which do fundamentally the same thing but one takes fewer parameters, there could be confusion (for the compiler and programmer) on which version would be used, e.g. if an API had:

func draw(shape: Shape, bounds: CGRect?)
func draw(shape: Shape)

And the programmer wrote:

draw(shape: triangle)

Which version would get called?

My solution to this (and Swift’s solution to a similar ambiguity) is that the one with fewer parameters would always match first — with the justification that if the original API author went to the trouble to write a version with fewer parameters she might have addd some special sauce to it that it’d be good to have.

I also think this case should cause a soft warning when the API is compiled (but not used), but note that this ambiguity can be replicated in current Swift, if instead you add a default value to the first function:

func draw(shape: Shape, bounds: CGRect? = nil)
func draw(shape: Shape)

draw(shape: triangle)

Here the second one would be called (at least as of Swift 5.1).


feedback

This is my first pitch! Please let me know if I missed anything! And thank you for your feedback.

¹ Swift 5.1 at this writing

7 Likes

Similarly, I often forget that optional class instance properties are not implicitly set to nil.

2 Likes

I think this proposal will incorrectly subvert the intent of many API signatures. For instance, UIView.convertRect(_ rect: CGRect, toView: UIView?) can be called as view.convertRect(frame, toView: nil) to mean 'convert to the coordinate space of the window'. However, it is important that this parameter is typed out explicitly; allowing view.convertRect(frame) is clearly nonsensical and is a loss of clarity.

I guess what I am saying is, there are valid use cases for optional functional parameters being explicitly labelled and not always defaulting to nil.

If the image.cgImage(forProposed...) case is not one of those, as it appears not to be, then this can be fixed in the Swift CoreGraphics overlay or similar.

Finally, the behaviour of optional values being implicitly set to nil is generally considered 'bad' by several people on this forum and I think long-term it is likely to be deprecated and removed. This would resolve the inconsistency you bring up, but from the other direction.

20 Likes

Wouldn’t this be source breaking for the method

func someMethod(_ argument: Int? = 2) {
    print(argument == 2 ? “Found two” : “Keep trying”)
}

That’s a good counter-example, but also that’s an API that I don’t think fits in well with Swift. toView: nil is not something I’d use in a modern API.

In this case I think they should change the overlay. Something like

open func convertToWindow(_ rect: CGRect) -> CGRect

which would match the existing:

open func convertToBacking(_ rect: CGRect) -> CGRect
open func convertToLayer(_ rect: CGRect) -> CGRect
5 Likes

No, I’m not saying force all optionals to be nil, I’m saying if nothing is specified assume nil. (I just edited the proposal to make this clear, thank you.)

1 Like

An alternative approach would be to introduce Swift bridging annotations for C that are about to specify a default value expression. This would leave control of the API in the hands of the authors. It might not achieve the result your looking for with specific APIs but it is a more disciplined approach to solving this problem.

4 Likes

That would also work, but I also think it’s over-wordy to have to specify, like, CGFloat? = nil in my new Swift APIs.

Others may disagree. But, honestly, in every sane language I can think of, an unspecified value defaults to nil, and Swift was designed take the best ideas of all languages.

1 Like

Thanks for taking the time to write this up and congratulations on your first pitch!

Personally, there are times where I use nil semantically, and want the control to force myself to either choose nil or a value at the point of use. This proposal would remove that choice from an API developer's toolkit. It's not that hard to add a default value if that's the behavior that you want, or to write an extension if you want to clean up an API that does not have a default value.

2 Likes

I understand this viewpoint but mine is that the number of times I want to allow nil but require the user to type it is minuscule, and the number of times I don’t want to have to type or look at = nil is “every time I see it”.

Plus I really REALLY don’t want to be in the business of cleaning up Apple’s APIs with my own overlays. This proposal would clean up a BUNCH of bad overlays in one swell foop.

I am -1 on this.
There is, currently, no way to opt out of a default argument and I wouldn't want to add it to the language to accommodate this.

7 Likes

I always though that implicit nil optional initialization was too magical. Does any other type of enum have implicit initialization?

AFAIK no other type has that behavior.

It would be more consistent if this behavior is part of ExpressibleByNilLiteral, not Optional.
(But not worth changing I think, though)

Congrats on your first pitch! :slight_smile:

While I understand wanting code to be as clean as possible...you can already do this. It just requires the API author to do it. If optional parameters defaulted to nil, you would have to introduce something to say that parameter is required (for whatever reason). That seems like the opposite direction.

This would be a magic transformation that IMO sacrifices clarity for brevity, and takes control away from API Authors.

1 Like

True. But there’s also not much reason to opt out of the default nil case, if nil is allowed. Any syntactic sugar involves giving up a tiny bit of control in exchange for doing what the user would expect.

I’m not sure what you mean here. Already in Objective-C the APIs are annotated for whether they accept nil or not. Are you talking about importing pure C APIs that take pointers?

Hi Will, congrats on your first pitch!

Question:
I acknowledge that you're probably looking to solve multiple things, but are you primarily interested sugaring Swift code to avoid having to write = nil in a function definition, or are you primarily interested in making existing Objective-C APIs (which didn't have default args in the first place) easier to use?

If it's the second case, I think it would be very interesting to consider introducing a new attribute (similar to the existing nullability attributes) that could be used in ObjC headers to indicate that a nullable argument should have a default init of nil. This approach would put the API author in charge.

-Chris

9 Likes

Thanks! My primary interest is in fixing the Objective-C APIs. BUT with the knowledge that a lot of the ones in question just aren’t being maintained by the teams in charge of them.

For example, this uniform type API still exists after five years:

extern __nullable CFStringRef
UTTypeCreatePreferredIdentifierForTag(
  CFStringRef              inTagClass,
  CFStringRef              inTag,
  __nullable CFStringRef   inConformingToUTI)                        API_AVAILABLE( ios(3.0), macos(10.3), tvos(9.0), watchos(1.0) );

And I feel like if there were anyone in charge of this API they’d have fixed the CFStringRef issue by now, so it’s unlikely they’ll add other attributes? (It’s true someone went though and marked __nullable, so that was nice.)

I have a secondary interest in not having to type / read = nil in the APIs I create / use, but that’s not as important to me.

-W

1 Like

Fair point. Here’s a stronger example; setter functions. Something like UIButton.setImage(_ image: UIImage?, for state: UIControl.State)- this is a perfectly reasonable signature where nil means no image. With the proposed change, it would be possible to write button.setImage(for: .normal), which wouldn’t set an image at all, it would remove any image that was set. SwiftUI includes methods like lineLimit(_ Int?) where passing nil represents no constraint on number of lines; allowing people to write lineLimit() would be less than ideal.

I’m all for improving the Objective-C import of neglected APIs but I don’t think a rule can be applied unilaterally without causing a lot of ambiguity (not for the type checker, for humans).

8 Likes

Congrats on your first proposal!

This is not true of Python:

def my_func(foo, bar=None):
    pass

In the above, calling my_func() is not equivalent to calling my_func(None). The former results in an exception, while foo is set to None explicitly in the latter.

If this proposal passes, I wonder how you’d handle and document the case where someone writes a function whose signature looks like this:

func myFunc(foo:Foo, bar:Bar?, baz:Baz)

In this case, the Optional is interleaved with non-Optionals.

The docs are prescriptive for defaults in this case, but not terribly descriptive about what happens if they’re disobeyed:

Place parameters that don’t have default values at the beginning of a function’s parameter list, before the parameters that have default values. Parameters that don’t have default values are usually more important to the function’s meaning—writing them first makes it easier to recognize that the same function is being called, regardless of whether any default parameters are omitted.

(https://docs.swift.org/swift-book/LanguageGuide/Functions.html)

In this case, what should happen if someone tries to call myFunc(foo, baz:myBaz)? I would expect it to fail, but I would also expect this to be confusing to developers, and complicated to communicate.

Given that your stated primary goal is to deal with legacy ObjC code, my inclination is that this would be the proper fix:

1 Like