I posted this on the "Improving the UI of generics" thread, but I wanted to also post it separately to hear some independent discussion on the matter without either bumping or clogging that thread:
I have an idea that I quite like so far but I could be convinced otherwise - I would love to hear what people think about it. I know that the idea is inspired by many ideas of others that I've read here on the forum, but I don't remember seeing exactly this as I'm proposing it. However, it is also possible that without knowing it I'm proudly presenting someone else's idea as if it were my own. Apologies if so.
The Idea
What if we expand the use of the generic <T: Constraint>
syntax to be usable in every (or maybe almost every) situation where a normal type name can be used? The syntax would have the same meaning in the new context as it does in its current usage, namely that the type in question will be chosen by the caller (subject to certain constraints).
Simplest example:
// Current syntax
func doNothing <Value> (with value: Value)
// New syntax
func doNothing (with value: <Value>)
The Value
type is introduced at the same time as being used in the type expression.
Details
The placeholder type names that are introduced in this way are accessible in the whole function signature and within the body of the function just like with the current generic syntax:
// Current syntax
func first <C: Collection> (of collection: C) -> C.Element
// New syntax
func first (of collection: <C: Collection>) -> C.Element
Any type names wrapped in angle brackets must be unique within the scope. This, for example, is an error:
// Wrong
func assign (_ newValue: <Value>, to destination: inout <Value>) // Error - invalid redeclaration of `Value`
Exactly one usage of Value
must be wrapped in angle brackets, and everywhere else it is referenced by name like any other type. Generic constraints can be applied either within the angle brackets or by way of a where
clause.
Thus, the correct ways to write that function are:
func assign (_ newValue: <Value>, to destination: inout Value)
func assign (_ newValue: Value, to destination: inout <Value>)
func assign <Value> (_ newValue: Value, to destination: inout Value)
If an angle bracket type declaration appears in the return type position that does not mean that it is a reverse generic. It is still a regular generic type, in the sense that the caller chooses the return type.
All of these signatures are equivalent:
// Current syntax
func echo <Value> (_ value: Value) -> Value
// New syntax
func echo (_ value: Value) -> <Value>
func echo (_ value: <Value>) -> Value
The order in which the types are declared within the function signature doesn't matter, in the sense that the declared types can be referenced in earlier parameters:
// Old syntax
func feed <Recipient: Eater> (_ food: Recipient.Food, to recipient: Recipient) -> Recipient.FormOfThanks
// New syntax
func feed (_ food: Recipient.Food, to recipient: <Recipient: Eater>) -> Recipient.FormOfThanks
I find the reduction of angle-bracket-blindness in the latter relative to the former fairly significant.
It seems reasonable to me to allow this syntax to be nested in a type expression:
// Old syntax
func dropLatterHalf <T> (of array: [T]) -> [T]
// New syntax
func dropLatterHalf (of array: [<T>]) -> [T]
func dropLatterHalf (of array: [T]) -> [<T>]
Properties
Given that <T>
means a type that will be chosen by the caller, how do we interpret this?:
let foo: <T> = 7
This is effectively the same as this:
typealias T = Int
let foo = 7
in the sense that after using <T>
as the type of foo
we can then reference T
for the rest of the scope:
let foo: <T> = 7
let maximumInteger = T.max // This is `Int.max`
(I can't quite put my finger on it at the moment but I have a feeling that there's something about this use-case that could prove extremely useful for writing and especially for maintaining unit tests).
If there is a constraint included in the type declaration then it is enforced at compile time as always:
let a: <T: Numeric> = 1.4 // Ok
let b: <T: Numeric> = "string" // Error
let c: <T: Numeric>
switch something {
case .oneThing: c = 1.2
case .anotherThing: c = 1.9 // Ok - both are `Double`
}
let d: <T: Numeric>
switch something {
case .oneThing: d = 1.5
case .anotherThing: d = Int(7) // Error: mismatched types
}
This would allow computed properties to have generic return types:
var anyKindOfSevenYouWant: <T: ExpressibleByIntegerLiteral> {
.init(integerLiteral: 7)
}
Existentials
This syntax would naturally allow us to unwrap existentials. For example:
let boxedUpValue: some Equatable = ...
let value: <Value> = boxedUpValue
if let dynamicallyCasted = someOtherValue as? Value {
print(value == dynamicallyCasted)
}
I'm thinking where clauses would be allowed on any declaration that contains a type placeholder declaration:
let existential: some Equatable = ...
let value: <T> = existential where T: Equatable
I suppose that in many cases the generic constraint on the type declaration can be implicit:
let existential: some Equatable = ...
let value: <T> = existential // T is known to conform to `Equatable`
Alternative Generic Type Syntax? (Controversial and not to be taken too seriously)
Here's another thought (and this one's a little bit out there) - could using one of these within the type expression of a stored property of a type be interpreted as a new generic parameter of the enclosing type?
struct Queue {
private(set) var elements: [<Element>]
}
would be equal to:
struct Queue <Element> {
private(set) var elements: [Element]
}
it could also be done like this:
struct Queue {
private var _privateDictBecauseWhoKnowsWhy: [Int: <Element>]
var elements: [Element] {
Array(_privateDictBecauseWhoKnowsWhy.values)
}
}
Either way, the Queue
type would be usable as a normal generic type (e.g., Queue<Int>
).
The Result
type for example could then be declared like this:
enum Result {
case success (<Success>)
case failure (<Failure: Error>)
}
I suppose the proper order of generic type parameters for a type could be determined simply by the order in which they appear in the type declaration.
This has the order A
then B
:
struct Foo {
var a: <A>
var b: <B: Collection>
}
let _: Foo<Int, Array<Bool>> // Ok
let _: Foo<Array<Bool>, Int> // Error, the Collection must come second
Extensions On Any
Lastly, perhaps this would also be the right syntax for extending any type (if that's actually a good idea in the first place):
extension <T> {
func mutated (by mutation: (inout Self)->()) -> Self {
var copy = self
mutation(©)
return copy
}
}
One obvious question here is regarding the usage of the letter T
, when we could equivalently have written:
extension <AnythingWeWantToWrite> {
func mutated (by mutation: (inout Self)->()) -> Self {
var copy = self
mutation(©)
return copy
}
}
and achieved the same result. I suppose that the name chosen is nothing more than a typealias for Self
which is declared in the scope of that extension, so the name is chosen by the programmer the same way a more descriptive local typealias is chosen by the programmer, as it won't affect anything but the way his or her own code reads. Writing this paragraph then evoked the idea for me, when we don't feel the need for a new typealias T = Self
could we write it like this?:
extension <_> {
func mutated (by mutation: (inout Self)->()) -> Self {
var copy = self
mutation(©)
return copy
}
}