[Pre-Pitch] Inferred default parameter values

(Seems like this is something that ought to have been pitched before? But I found no thread...)

For many years I've been wanting to be able to have a convenient way of "inferring" / "inheriting" / "following" / "reverse-forwarding" the default value of a parameter, either to init or other function.

Imagine we have syntax for conveniently working with Date, like so 30.years.ago, look at this simple struct Person:

public struct Person {
    public let firstName: String
    public let lastName: String
    public let dateOfBirth: Date
    
    public init(
        firstName: String,
        lastName: String,
        dateOfBirth: Date = 30.years.ago
    ) {
        self.firstName = firstName
        self.lastName = lastName
        self.dateOfBirth = dateOfBirth
    }
    
    public static func unknown(
        dateOfBirth: Date = 30.years.ago
    ) -> Self {
        Self.init(firstName: "Jane", lastName: "Doe", dateOfBirth: dateOfBirth)
    }
}

See how we are duplicating the value 30, it is specified twice. But imagine we semantically mean to use the same value. We solve this ofc either by doing this:

public struct Person1 {
    
    ...
    
    public static let defaultDateOfBirth: Date = 30.years.ago
    
    public init(
        firstName: String,
        lastName: String,
        dateOfBirth: Date = Self.defaultDateOfBirth
    ) {
        ...
    }
    
    public static func unknown(
        dateOfBirth: Date = Self.defaultDateOfBirth
    ) -> Self {
        Self.init(firstName: "Jane", lastName: "Doe", dateOfBirth: dateOfBirth)
    }
}

Or by doing this:

extension Date {
    public static let defaultDateOfBirth: Self = 30.years.ago
}
public struct Person2 {

    ...

    public init(
        firstName: String,
        lastName: String,
        dateOfBirth: Date = .defaultDateOfBirth
    ) {
        ...
    }
    
    public static func unknown(
        dateOfBirth: Date = .defaultDateOfBirth
    ) -> Self {
        Self.init(firstName: "Jane", lastName: "Doe", dateOfBirth: dateOfBirth)
    }
}

This is ofc a very very simple scenario with just one default value for a parameters, but imagine we have 3 or 4 or more even. Both version struct Person1 and struct Person2 solution have their own drawback.

The problem with struct Person1 is that we are "polluting" the struct with static lets for the default values. The problem with struct Person2 is that we are "polluting" the type Date with domain specific values. And since both init and static func unknown are public the extension inside Date need also be public (otherwise this solution would not have been that bad).

Default values for parameters are awesome when used in just one place, because the do not have any of struct Person1 nor struct Person2s problem, the default value is just declare with = .defaultValue which is perfect, exactly what we want, but as soon as we need to reference semantically this default value in more than one place, we must retort to any of the two bad solutions.

But what if, we could with some compiler magic get this syntax possible?

public struct Person3 {

    ...

    public init(
        firstName: String,
        lastName: String,
        dateOfBirth: Date = 30.years.ago
    ) {
        ...
    }
    
    public static func unknown(
        dateOfBirth: Date = .inferred // not possible today, and this is what I pitch!
    ) -> Self {
        Self.init(firstName: "Jane", lastName: "Doe", dateOfBirth: dateOfBirth)
    }
}

In struct Person3's func unknown we spell out the default argument as .inferred, which tells Swift that most likely during compilation (I'm not a compiler engineer, but this seams reasonable?), replace .inferred with = 30.years.ago from the function (init) called!

And this would also work in several steps/hops, so this would also work:

public struct Person3 {

    ...

    public init(
        firstName: String,
        lastName: String,
        dateOfBirth: Date = 30.years.ago
    ) {
        ...
    }
    
    public static func doe(
        firstName: String,
        dateOfBirth: Date = .inferred
    ) -> Self {
        Self.init(firstName: firstName, lastName: "Doe", dateOfBirth: dateOfBirth)
    }
    
    public func unknown(
        isMale: Bool,
        dateOfBirth: Date = .inferred
    ) -> Self {
        Self.doe(firstName: isMale ? "John" : "Jane", dateOfBirth: dateOfBirth)
    }
}

What do you think? Have you found yourself in need of this ever?

A tedious way getting the behaviour I want without this new compiler magic - without polluting neither struct Person nor Date (or Int) is to used PointFree's Tagged package to create a domain specific Date type:

extension Person { 
    public typealias DateOfBirth = Tagged<Self, Date>
}

and then do:

extension Person.DateOfBirth { 
    public static let `default`: Self = .init(rawValue: 30.years.ago)
} 

It is not terrible, but tedious for non trivial situations where I have multiple parameters for which I wanna use shared default value.

I don't agree with your characterisation of that as "polluting". In fact, it seems to be a self documenting default and therefore a Good Thing.

2 Likes

static let defaultDateOfBirth = ... in Person1 is not so bad.


let me add a couple of more ways for completeness:

  • two "unknown" functions, one with date parameter and another without. This method is not good if you have more than one default parameter.

  • have default parameter optional (in both "init" and "unknown"). When used inside init it is naturally:

    self.dateOfBirth = dateOfBirth ?? .....

and that's the only place where you specify the value. in both "init" and "unknown" it is:

init(firstName: String, lastName: String, dateOfBirth: Date? = nil) ...
static func unknown(dateOfBirth: Date? = nil) ...

Having said that, I like the train of thought you presented and an ability to specify default parameter like so:

static func unknown(dateOfBirth: Date = default)

and also potentially use it like so when needed:

Person.unknown(dateOfBirth: default)

would be useful. Beside other things it would refine the language in this other place:

func foo(_ a: Int = 283746287364, _ b: Int = 12652636273, _ c: Int = 476873728368)
...
foo(a, b)       // can do this
foo(b)          // but not this
foo(default, b) // now I can
1 Like

Another advantage to this is that you can do this in the caller:

// only override the default if a condition is met
let person = Person.unknown(dateOfBirth: condition ? myDate : nil)

You actually can already do this if you don't delete the argument labels:

func foo(a: Int = 1, b: Int = 2) { }
foo(a: 3, b: 4)
foo(a: 3)
foo(b: 4)

Sure thing. I was specifically talking about a function that does't use argument labels. "default" parameter would cover that case. Optional parameter works there as well, but it is not as nice and looks like a hack.

1 Like