Reusable function parameter lists

I had a syntax idea that I'd like to hear thoughts on - I'm not currently leaning towards it as a genuinely good idea because I know enough to know that I'm not seeing all of the potentially problematic consequences. It is intriguing however and perhaps with some other minds at the table it could be brought to a fruitful state. The idea is essentially to be able to define reusable function parameter lists in the form of structs.

Here's a simple struct:

struct Foo {
    let bar: Bool
    let baz: Double
    let bop: String
}

Here's a function that requires a Foo value:

func doSomething (_ foo: Foo) {
    
}

I can currently call it like this:

doSomething(
    Foo(
        bar: true,
        baz: 10.1
        bop: "hello"
    )
)

What I'd like is to be able to call it like this:

doSomething(
    bar: true,
    baz: 10.1,
    bop: "hello"
)

The rule I have in mind is that in the case where a function takes only one argument then that function automatically acquires an overload for each initializer of the argument type. In my example I have no external argument label for foo in the parent function, but I suppose that any external argument label should be ignored in the case that the parameter list of a Foo.init is being used.

In theory this should apply recursively. Here's the example reasoning that I'm imagining the compiler would do:

  1. When resolving a function call, obviously check first for an explicitly defined overload that matches the provided arguments and argument labels.
  2. If no match is found find all overloads which accept only one argument.
  3. For each unique type from these only-child arguments, find all accessible initializers for the argument.
  4. Synthesize an overload for the parent function using the parameter list from each of these initializers from step 3.
  5. Attempt to resolve the function call in the context of all these newly synthesized overloads.
  6. During this process, we may have synthesized new overloads which themselves only take one argument. If after the first round we have still not been able to resolve the call to the parent function successfully then we can repeat the process on these newly generated one-argument initializers.

To clarify the possibility of the recursive aspect:

struct Foo {
    let bar: Bool
    let baz: Double
    let bop: String
}

struct Bar {
    let foo: Foo
}

func doSomethingElse (_ bar: Bar) {
    
}

func demo () {

    /// This overload of `doSomethingElse` is recursively generated
    /// by finding firstly that a `Bar` can be initialized with only a `Foo`
    /// and secondly that a `Foo` can be initialized with `bar:baz:bop:`
    doSomethingElse(
        bar: true,
        baz: 10.2,
        bop: "hello"
    )
}

The closest syntax alternative you can achieve today is inferring the initializer of the type:

doSomething(.init(
    bar: true,
    baz: 10.1,
    bop: "hello"
))
1 Like

I imagine that introducing implicit overloads for every single-arg function would have pretty abysmal effects on type checker performance. I think it would be a better direction to allow some sort of lightweight record initialization syntax like:

doSomethingElse(
  { 
    bar: true,
    baz: 10.2,
    bop: "hello"
  }
)

So that the structuring/destructuring is always explicit. I don’t necessarily think that this would be an improvement over the current .init solution, but introducing implicit overloads is going to need some pretty substantial justification to offset the performance cost.

1 Like

I think the record syntax is much more clear, and could also be used where there is more than one parameter without introducing ambiguity. I feel like it's also more clear than using .init(...), which always feels a bit odd. Code completion might be a bit of a nightmare, though.

An alternative might be something like ExpressibleByTupleLiteral (e.g. as discussed in this thread).

As that thread points out, the benefit is rather small - we could drop the .init you have to write today, but that’s pretty much it.

The other option is - if you have a lot of methods which all take a Foo, write those as member functions on Foo. Since this is all about “reusable function parameter lists in the form of structs”, I assume there are lots of functions which take a single argument of type Foo.

3 Likes

I don't like it. Imagine someone wants to create an array that contains the answer to the life, universe and everything, but used too much kotlin lately, so they forgot about the array literals

let arr = Array(42)

Today it gives you an error message. In your future it compiles. But is it correct? Can you guess how many items it contains? What is the type?

It's possible that it desugared into let arr = Array(String(123))
Did you guess two elements?
Did you guess an array of Character?

What if someone adds an unrelated type with an unrelated init? It might make it ambigous and stop compiling.

Could you explain the error messages when they try to insert another Int into this array, and start getting error messages containing types that are totally unrelated to all the code they wrote?

1 Like