[Draft] Allow default value for parameters in generic clause

Hello!

Check draft proposal, please.
Proposal

UPD:

Previous pitch discussion.


[Proposal] Allow default values for parameters in generic clause

During the review process, add the following fields as needed:

Introduction

Allow type parameters in generics clauses to have default values in class definition. Could be expanded to generic functions also.

Motivation

It is common in practise to have some generic class with several parameters. However, sometimes you need only specific subset of this class with several parameters set to concrete values.

Consider priority_queue from C++ stl.

template <class T, class Container = vector<T>,
  class Compare = less<typename Container::value_type> > class priority_queue;

It has several template parameters.

  1. Type of element T.
  2. Container type with elements matched Type T.
  3. Compare - binary function which will order elements in priority queue.

Lets try to implement similar priority queue in swift.

First, Consider comparison protocol.

protocol BinaryCompareFunction {
    associatedtype T
    static func compare(lhs: T, rhs: T) -> Bool
}

struct BinaryCompareFunctions {
    struct Less<E>: BinaryCompareFunction where E: Comparable {
        typealias T = E
        static func compare(lhs: E, rhs: E) -> Bool {
            return lhs < rhs
        }
    }
}

Next, define interface of priority queue. It will be nested in PriorityQueues structure for convenience.

// MARK: Definition
struct PriorityQueues {
    class PriorityQueue<Container: Collection, Comparison: BinaryCompareFunction> where Container.Element == Comparison.T {
        typealias Element = Container.Element
        typealias ComparisonFunction = Comparison
        var container: Container?
        init(container: Container?) {
            self.container = container
        }
    }
    struct test {

    }
}

Question.

How to specify default values for type parameters for this queue?

Proposed solution

Prososed solution could solve problem in shorter and elegant way.

Proposed solution style

struct PriorityQueues {
	class PriorityQueue<Container: Collection = Array<Int>, Comparison: BinaryCompareFunction = BinaryCompareFunctions.Less<Container.Element>> {}
}

Detailed design

There are several possible solutions for this functionality.

  1. Rule of last
  2. Naming parameters opposite to function style
  3. Naming parameters in function style

Rule of last

It may not affect current functionality.
Consider simple class.

class Foo<A, B = Y, C = Z> {}

Rule of last could be splitted into statements.

  1. All parameters with default values are at the end of parameter list. ( At the rightside )
  2. There is no parameter without default value to the right of parameter with default value.

Consider invalid example.

class Bar<A, B = Y, C> {}

Thus, you could specify parameters only from left to right.

Foo<Int> // Foo<X, Y, Z>
Foo<Int, String>  // Foo<Int, String, Z>
Foo<Int, String, Int> //Foo<Int, String, Int> 

As you see there is no chance to save default value of second parameter (B = Y). It should be also specified to provide non-default value to third parameter (C = Z).

Naming parameters.

We could also add names for type parameters. It is the same functionality that functions have.
However, it should be splitted into two possible solutions.

Functions have two types of parameters names: Inner parameter name and Outer parameter name.

func create(id: Int) // Inner parameter name == Outer parameter name
func mutate(_ id: Int) //underscored behavior. Parameter name is omitted in invocation
func destroy(byId id: Int) // Inner parameter name != Outer parameter name

// Usage:
create(id: 0)
mutate(0)
destroy(byId: 0)

Underscored behavior

Outer parameter is optional for generic clause. It will not break any existing functionality.

class Foo<A, B = Y, C = Z> // valid
class Foo<A, Second B = Y, Third C = Z> // valid

This solution is opposite to function behavior.

Function behavior

Outer parameter is required for generic clause. It will break all existing functionality.

class Foo<A, B = Y, C = Z> // invalid
class Foo<A, Second B = Y, Third C = Z> // valid
class Foo<A, _ B = Y, Third C = Z> // valid

This solution is similar to function behavior.

Mixing parameters examples.

For naming parameters solutions we could mix generic parameters and names by preserving parameter list order as function does with parameters list.

Underscored behavior example

class Foo<A, Second B = Y, C = Z> // valid

Foo<Int, Second: String, Int> // valid
Foo<Int, String, Int> // invalid or valid, tbd

Function behavior example

class Foo<A, _ B = Y, Third C = Z> // valid

Foo<Int, String, Third: Int> // valid
Foo<Int, String, Int> // invalid or valid, tbd

Source compatibility

Depends on solution.

Effect on ABI stability

Depends on solution.

Effect on API resilience

Depends on solution.

Alternatives considered

There are several workarounds only, there is no convenient solution.
Consider previous PriorityQueue. We could specify type parameters in several ways.

  1. Naive
  2. Class method
  3. Subclass
  4. Class Object

Naive

// MARK: Naive
extension PriorityQueues.test {
    static func naive() {
        let _ = PriorityQueues.PriorityQueue<Array<Int>, BinaryCompareFunctions.Less<Int>>(container: [])
    }
}

Class method

// MARK: Class method
extension PriorityQueues.PriorityQueue {
    class func defaultQueue<T: Comparable>(t: T) -> PriorityQueues.PriorityQueue<Array<T>, BinaryCompareFunctions.Less<T>> {
        return PriorityQueues.PriorityQueue<Array<T>, BinaryCompareFunctions.Less<T>>(container: [])
    }
}

extension PriorityQueues.test {
    static func classMethod() {
        let i: Int = 0
        let _ = PriorityQueues.PriorityQueue<Array<Int>, BinaryCompareFunctions.Less<Int>>.defaultQueue(t: i)
    }
}

Subclass

// MARK: Subclass
extension PriorityQueues {
    class PriorityQueue_DefaultLess<T: Comparable>: PriorityQueue<Array<T>, BinaryCompareFunctions.Less<T>> {
        convenience init() {
            self.init(container: [])
        }
    }
}

extension PriorityQueues.test {
    static func subclass() {
        let _ = PriorityQueues.PriorityQueue_DefaultLess<Int>()
    }
}

extension PriorityQueues.test {
    static func easy() {
        let _ = PriorityQueues.PriorityQueue_DefaultLess(container: [0])
    }
}

Class object

Object companion if I remember. Similar item in Scala.

// MARK: Class Object
extension PriorityQueues {
    class PriorityQueueObject<T: Comparable> {
        class func defaultQueue() -> PriorityQueue<Array<T>, BinaryCompareFunctions.Less<T>> {
            return PriorityQueue<Array<T>, BinaryCompareFunctions.Less<T>>(container: [])
        }
    }
}

extension PriorityQueues.test {
    static func classObject() {
        let _ = PriorityQueues.PriorityQueueObject<Int>.defaultQueue()
    }
}

Not sure this is worth while - do you have a better motivating example?

If it were introduced I would favour mimicking the func syntax with named parameters and requiring an underscore for no name; even though this is a breaking change. Exactly this change was made for init and this was worthwhile overall. The reason for this opinion is that I value consistency in the language.

1 Like

Would it be non-ABI-breaking to add a generic parameter with a default?

This is described in the Generics Manifesto: swift/GenericsManifesto.md at main · apple/swift · GitHub

It makes sense in all generic signatures, not just generic types.

2 Likes

I think in any reasonable implementation of default arguments, it is ABI-breaking to add a new generic argument with a default. Just like default arguments for functions, default arguments for generic parameters would be a compile-time fiction only.

2 Likes

That’s disappointing. If it wasn’t ABI-breaking, it would allow us to evolve APIs in interesting ways, such as starting with Resuit<T> and changing it to Result<T, E = Error> if we ever get typed throws.

I've met a similar situation and I wanted the @ViewBuilder closure parameters in the initializer of my generic type to have default values. I've posted the question onto StackOverflow, could some one here please take a look and give me some advice? Thanks a lot.

You question one thing but speaking about a different thing. You asked for adding a new generic parameter which is ABI breaking as @Slava_Pestov described, however it's still unclear if it's possible to retroactively extend existing types with a default type on already existing generic type parameters (potentially with the help of a special attribute).

Edit: Sorry just noticed how old this thread is! :upside_down_face:

1 Like

It depends on the design, but based on Slava’s response, I think that adding a default to an existing generic argument would not be ABI breaking.

2 Likes