Improving the UI of generics

(Jon Hull) #21

Would it work to just add the name after another space when needed or would that conflict with some other syntax?

func foo (_ c: some Collection C)  
8 Likes
(Davide De Franceschi) #22

Thank you so much for the great write up. I think I grokked the concept decently with the opaque return types thread, but this still helped :slight_smile:
I like a lot about this:

  • the fact that before generics was the "low level" way of doing things, more syntax but more power, to approach with caution. While existential were the "nice" way since the syntax is so straightforward
  • Given that as highlighted both are ways of defining type constraints, it's better to have them more similar looking so that an user can actually think of which they want to use instead of going for the most convenient (apart from current PAT issues)
  • the fact that constraints can be defined on the types themselves
  • extension any Hashable: Hashable is quite interesting as it actually defines what's happening

There are some things I'm not too sold on though:

  • Using type(of:) suggests the dynamic type of something, while in this case we're talking about static type. I actually use it quite a bit in inits and never understand the rules around when it can be used as a statically evaluated utility and when as a runtime one
  • Especially type(of: return) looks like an hack :cry:

Also just a few questions:

  • func foo <T> (_ t: T) translates to func foo(_ t: some Any) or func foo(_ t: some)?
  • Would this allow extension any Any?
  • Should then current mixins like extension Collection { ... } be extension some Collection { ... } instead?
  • Can properties be some without making the type itself generic? Sounds like it would be some kind of implicit genericness...
1 Like
(Tellow Krinkle) #23

Just because it's possible doesn't mean it's fast. I would expect that a lot of IEnumerators get compiled to dynamic dispatches by the AOT compiler.

Also, could you clarify on the easy and intuitive method that you could use to transform a Sequence if only it had an existential iterator? Here's my best attempt (using an Int-only sequence since Swift currently lacks the ability to make existentials from associated type protocols):

protocol IntIter {
	mutating func next() -> Int?
}

protocol IntSequence {
	func makeIterator() -> IntIter
}

struct AddedIntSequence<A: IntSequence, B: IntSequence>: IntSequence {
	var a: A
	var b: B
	struct Iter: IntIter {
		var a: IntIter
		var b: IntIter
		mutating func next() -> Int? {
			return a.next().flatMap { anext in b.next().map { bnext in anext + bnext } }
		}
	}
	func makeIterator() -> IntIter {
		return Iter(a: a.makeIterator(), b: b.makeIterator())
	}
}

and here's the same thing with the current Sequence

struct AddedSequence<A: Sequence, B: Sequence>: Sequence where A.Element == Int, B.Element == Int {
	typealias Element = Int
	var a: A
	var b: B
	struct Iterator: IteratorProtocol {
		var a: A.Iterator
		var b: B.Iterator
		mutating func next() -> Int? {
			return a.next().flatMap { anext in b.next().map { bnext in anext + bnext } }
		}
	}
	func makeIterator() -> Iterator {
		return Iterator(a: a.makeIterator(), b: b.makeIterator())
	}
}

literally the only difference is whether the Iterator's a and b are of type Iterator or A.Iterator and B.Iterator

From what I can tell, the only way to easily transform a Sequence, existential iterator or not, is to use methods where someone else wrote the iterator wrappers:

zip(a, b).lazy.map { $0 + $1 }

Note: I do realize C# has generators / a yield keyword, which can be used to easily transform IEnumerables, but that has nothing to do with whether the iterators are existential. If Swift ever gets a yield keyword that can make iterators, then you would be able to do this as well.

(Adam Kemp) #24

In my experience modern CPU architectures make the overhead of most dynamic dispatches negligent in all but the most tight loops. That's why Objective-C, where every single method call is a dynamic dispatch, isn't nearly as slow as you might think intuitively. If I had to choose between avoiding dynamic dispatch and ease of use I would choose ease of use every time.

As you mentioned, yield return is definitely another aspect of this, but that's just a specific example of a pattern of building a transform out of lower-level pieces. We already have functions like map and filter, so why can't a write something like this?

func eventValues<T:BinaryInteger>(collection: Sequence<T>) -> Sequence<T>
{
    return collection.filter { $0 %2 == 0 }
}

That's simpler than the example given by Joe above. This function is generic in terms of the element type but requires a Sequence specifically. Do we really need this function to be compiled specifically for each type of Sequence to avoid a dynamic dispatch or do we just want to write expressive and understandable code?

3 Likes
(David Smith) #25

I get what you're saying, but having just put a lot of effort into removing objc message sends in some code one by one, I'm unconvinced for things like loop iteration. That said, C# (non-AOT anyway) should be able do the usual dynamically inlined fast path + on-stack-replacement trick to avoid it. In theory Swift can do a similar trick (speculative devirtualization), but it gets expensive real fast if there's too many possible types.

6 Likes
(Goffredo Marocchi) #26

Is the performance win big enough to offset the lack of flexibility having dynamic dispatch by default for everything like Objective-C did and the simplified mental model of it?

If you had a way to force the compiler (it strongly hint) to statically dispatch a call would that satisfy the hot loops case while not impacting the flexibility of the language?

3 Likes
(David Smith) #27

Expanding @_specialize() (which does sorta that) to handle more situations seems like an interesting direction for things like this :slight_smile:

I can't comment on how likely that is or what pitfalls it might have though, I just work on libraries.

(Goffredo Marocchi) #28

I will give you a whoooohoooo :smiley: anyways ;)!

(David Smith) #29

One further thought here: the way ObjC handles this is by relying on bulk accessors wherever possible. So -objectAtIndex: loops are indeed very slow, but nothing (…almost) actually does that, they use for(in) which goes 16 at a time, or they use -getObjects:range: and then loop over it in C, or…

(Mox) #30

@Joe_Groff, Excellently written addendum to the Generics manifesto. I like both the clarity it has to describe the concepts as well as the direction it’s setting.

In particular, I see very strong case for the pair of ”some” and ”any” keywords, and feel it’s worth breaking the source compatibility by switching to use ”any” with existentials. This can have a big impact in making generics in Swift easier to get right.

As for shortcut syntaxes, I prefer clarity more (incl. naming types), and would try to use typealiases or other similar ways to reduce verbosity on the type declarations themselves. But YMMV.

(Tellow Krinkle) #31

This is the point of the some Sequence notation mentioned in the original post, to allow an easy way to notate it without sacrificing speed, that way you can have both your nice notation and speed if needed.

1 Like
(Asa Z) #32

Perhaps a more natural syntax would be

func concatenate<T>(a: some Collection where .Element == T, b: some Collection where .Element == T) 
  -> (some Collection where .Element == T) where T:SomeConstraintOnTheFunction 

Or not using where and using something like with: a: some Collection with Element == T

1 Like
#33

It is a wonderful document, with many of the previous discussions being neatly organized.

The ORT discussion was very complicated, but I think the extension of type system by concept of reverse generics and concise notation with the some keyword are very beautiful.

If the some keyword is implemented, users will gradually notice this ease of use and spread. And I think that the speed of learning that Swift beginners use Generics will accelerate. Reducing the indirection of expressivity by one is quite powerful.

In fact, I was looking at these arguments, and I thought that Existentials must be attached keyword any, and I was wondering if I would like to post that pitch.

The reason I was wavered was that there was an ambiguous memory like I saw a reference to the notation Any<Base, Proto1> when there was a discussion of subclass existential (SE-0156, https://github.com/apple/swift-evolution/blob/master/proposals/0156-subclass-existentials.md) at the time, and I don't know the details so I thought that it might be a story reached conclusion.

So, I was happy to mention any mandatory idea in this thread.

However, when it comes to that, it is the any notation for subclass existence.
There are likely to be several possibilities:

let p1: any UIViewController & UITableViewDelegate & UITableViewDataSource
let p2: UIViewController & any UITableViewDelegate & any UITableViewDataSource
let p3: UIViewController & any(UITableViewDelegate & UITableViewDataSource)

Considering transformation from a simple UIViewController type variable, p2 or p3 seems to be good, but p2 looks like an intersection type between Existentials. This construction is not wrong either, but strictly it will be one existential that satisfies two protocols. p3 looks good in that respect, but it is wierd to write in parentheses. This part will need to be discussed in the future.

The point that almost all intermediate level players in Swift squeeze in is that “protocol has two functions of existential and protocol”. I think this is the biggest cause of confusion that two completely different concept appear in the same way in appearance. So I think we should change the look. This is achieved by any requirement.

Another difficulty is the existential self-conformance problem. Understanding this is one of the keys to becoming a Swift expert. In this regard, I thought that the existential-only extensions listed are very nice. The current Swift has become more difficult due to the special support of the Error type, but this is not special if it is defined as follows.

extension any Error : Error {}

In addition, user-defined protocols can also be made self-conformance by themselves. And if self-conformance can not be achieved due to the presence of static members, etc., the code becomes a compilation error, and the user can learn the theory from the error.

Equatable is a very good example in this respect, and in the process of giving an implementation of ==, we can touch the nature of the difficulties of the type system of self-conformance. It is necessary to compare different types dynamically because Equatable is essentially the same type comparison, but existential is super type, and it is necessary to consider various implicit conversions in Swift runtime.

I think that the ease of use of Swift will evolve at a stretch if we go in the direction that this summary shows, including the open notation of existential. I am very looking forward to it.

1 Like
(Adrian Zubarev) #34

I think sou have it wrong there. p1 is the correct one as the composed type as a whole is an existential here. Therefore p1 is similar to any (Class & Protocol & Protocol) where you just omit ().

#35

If q2 is ok, q3 also looks correct expression.
But this doesn't have independent specific meaning so its just wrong form of q1.

let q1: UIViewController
let q2: any (UIViewController & UITableViewDelegate) // OK
let q3: any (UIViewController) // ??

I'm wondering if this point is confusing.

(Adrian Zubarev) #36

If you recall P & Q is a newer syntax for the old protocol<P, Q> construct for protocol composition. And since 'protocol as a type' is the 'existential type' it becomes clear what an existential is in your example. q1 is not an existential, while q2 is. q3 would be ill-formed to my knowledge.

2 Likes
(Yuta Koshizawa) #37

I really love the direction. some and any seems perfect keywords. I think explicit existential types are worth breaking source compatibility. It also helps to notify people who are familiar with languages like Java that protocols in Swift are different from interfaces in those languages when they learn Swift.

Supporting "reverse generics" first seems better for me because

  1. Swift has already supported generics. I think having generics and "reverse generics" in a transition period makes sense better than having only generics and opaque result types.
  2. I think the some syntax can be understood easily when it is considered as sugar of generics and "reverse generics". Threads about SE-0244 shows a lot of people got confused to understand opaque result types directly without the concept of "reverse generics".
  3. "reverse generics" is more expressive. For example, a function below can't be implemented with the some syntax in my understanding. Tackling "reverse generics" first can prevent lack of consideration about internal implementation.
func makeAnimals() -> <A: Animal> (A, A) {
    return (Cat(), Cat())
}
7 Likes
(Garth Snyder) #38

It's been said multiple times already, but this is a fine piece of expository technical writing. It's my highest-value time spent reading so far of 2019. I'm reminded of Anand Shimpi's 2009 article about SSD architectures -- in some sense it was just another tech media article, but it was so beautifully written and honed in so clearly on a major and widespread knowledge gap that it became a bit of a viral sensation.

There's so much to consider here that it seems myopic to respond to any one thing. But I won't let that stop me, since part of it bears on my personal bête noir of recent Swift coding: the lack of static members for protocol existential types. As near as I can tell, that's only been seriously addressed here once before, and that discussion veered off track pretty quickly.

This isn't a feature that many people are clamoring for, but when you need it, it's hugely valuable. I could eliminate quite a few pages of code from one project if that feature were available.

I was thinking about writing a pitch for it, but from the above it seems clear that this interacts with several other potential changes that might be coming in the near-ish future. Is there any value in pursuing this right now, or is it better left for later?

One thing I will say is that the suggested notation:

extension any Hashable: Hashable {
  static func ==(a: any Hashable, b: any Hashable) -> Bool {
    return AnyHashable(a) == AnyHashable(b)
  }
  ....
}

reads as bass-ackwards to me. Wouldn't you think that if you extended any Protocol, you'd be adding members to any (i.e., all) implementations of that protocol?

That is, it seems like according to commonsense rules of English, the spelling extension any Protocol should mean what is currently meant by extension Protocol, and that extension Protocol should mean what is indicated above by extension any Protocol.

2 Likes
(Mox) #39

To me, extension any Fooable {} reads as extending any existential of type Fooable. So exactly as it should. This gives much better sense that there are many different existentials, not just a single one as there is with some.

In particular for self conformance it’s great:
extension any Fooable: Fooable {} extends any existential of type Fooable to conform to Fooable protocol.

1 Like
(Adam Kemp) #40

The proposed notation may let you do the same thing, but it's not as easy to use or understand, which is I think the goal of the proposal. There were a few ways that the proposal suggested some could be used for. The simplest, without the angle brackets, works when you don't want to place any constraints on the types. Or if you want to have some constraints or relate two types there was this proposed notation:

func concatenate<T>(a: some Collection<.Element == T>, b: some Collection<.Element == T>)
  -> some Collection<.Element == T>

Is that really better than this, which could mean the same thing?

func concatenate<T>(a: Collection<T>, b: Collection<T>)
  -> some Collection<T>

some may be a useful syntax for when you don't need to constrain the types and if you don't like angle brackets, but it's also a new concept that I don't find particularly intuitive. It seems like something an expert on type theory would understand, but for the rest of us it's something we have to really think about. I'm just wondering if it should even be necessary.

2 Likes