[swift-evolution] [Pitch] [Phase 2] New `permuting` keyword for protocols

Xiaodi Wu xiaodi.wu at gmail.com
Tue Dec 27 11:49:10 CST 2016


TBH, I think you're trying to solve this problem in a very complicated way.
What you're describing screams out for its own value subtype with let
variables and an initializer that will provide defaults. I'm skeptical such
a complicated design as you propose is necessary to achieve what you want.

On Tue, Dec 27, 2016 at 05:25 Adrian Zubarev via swift-evolution <
swift-evolution at swift.org> wrote:

> Okay now I see your point there. :) Thank you Xiaodi and Tony.
> ------------------------------
>
> It’s an interesting approach you have there, but I see another problem
> with Self and - ProtocolName. Self does not refer to the current type
> returned from the protocol member. SE–0068 might help there, but as soon
> we’re working with non-final classes it will be problematic again.
>
> That means something like this will be possible constraint.x(1).y(1).x(2),
> which by solving the main problem of this topic we’d like to avoid.
>
> We’d need a way to subtract a protocol from the returned type + the
> ability of keeping T only members.
>
> As for T : P1 & P2 & P3, T - P1 should return T + P2 & P3, because it’s
> what the user would logically assume there. The next chain needs to
> remember - P1 on that path, so the result for the followed reduction of -
> P3 would be equivalent to T - P1 & P3 = T + P2.
>
> As you already mentioned, one would assume that we might be able to cast
> back to T from T - P1 & P3. I think this leads us to the right direction
> where we should realize that we should escape from T in general. That
> means that the return type should somehow create a new subtraction type,
> which can be reduced further by member chaining or escaped similar to what
> I wrote in the original post.
>
> We’d need a new type or keyword that refers to the current (reduced) type.
> Let’s call it Current instead of Self. Furthermore we’d need a concrete
> result type, to be able to pass the result value around. Let’s call the
> latter type Subtraction<T>.
>
> protocol WidthConstrainable {
>   func width(_ v: CGFloat) -> Subtraction<Current – WidthConstrainable>
> }
> protocol HeightConstrainable {
>   func height(_ v: CGFloat) -> Subtraction<Current – HeightConstrainable>
> }
> protocol XConstrainable {
>   func x(_ v: CGFloat) -> Subtraction<Current – XConstrainable>
> }
> protocol YConstrainable {
>   func y(_ v: CGFloat) -> Subtraction<Current – YConstrainable>
> }
> struct Constraint: WidthConstrainable, HeightConstrainable, XConstrainable, YConstrainable {
>   ...
> }
>
> Subtraction<T> could be a similar type, like we’re proposing to change
> the metatypes from T.Type/Protocol to Type<T> and AnyType<T>. That means
> that it would know all the members of Current without the subtrahend.
> Each further member chain would create ether a nested Subtraction<Subtraction<T
> - P1> - P2> or a flattened type like Subtraction<T - P1 & P2>.
>
> let constraint = Constraint()
> let a: Subtraction<Constraint - XConstrainable> = constraint.x(1)
> let b: Subtraction<Subtraction<Constraint - XConstrainable> - YConstrainable> = a.y(2)
> let c: Subtraction<Subtraction<Subtraction<Constraint - XConstrainable> - YConstrainable> - WidthConstrainable> = b.width(100)
>
> _ = constraint.x(1).y(2).width(100)
>
> That way we could even remove the new - operator and use generics
> parameter list instead. Maybe instead of Subtraction we could call the
> new type Difference<T>
>
> type Difference<Minuend, Subtrahend> : Minuend - Subtrahend { … } // should know everything `Minuend` has, but exclude everything from `Subtrahend`
>
> struct Constraint: WidthConstrainable, HeightConstrainable, XConstrainable, YConstrainable {
>    func width(_ v: CGFloat) -> Difference<Current, WidthConstrainable> {
>
>       var copy = self
>       copy.width = v
>       return Difference(copy)
>    }
>
>    func height(_ v: CGFloat) -> Difference<Current,  HeightConstrainable> { … }
>    func x(_ v: CGFloat) -> Difference<Current, XConstrainable> { … }
>    func y(_ v: CGFloat) -> Difference<Current, YConstrainable> { … }
> }
>
> What do you guys think about this approach? :)
>
>
>
> --
> Adrian Zubarev
> Sent with Airmail
>
> Am 26. Dezember 2016 um 17:17:29, Tony Allevato (allevato at google.com)
> schrieb:
>
> Xiaodi's point is really important—being able to express the notions
> simultaneously that "T has method a()" and "T does not have method a()"
> would break the type system.
>
> Instead of focusing on the proposed syntax, let's consider the problem
> you're trying to solve. It sounds like what you're asking for could be
> expressed more cleanly with a richer protocol algebra that supported
> subtraction. It wouldn't be quite as automatic as what you propose, but it
> would feel like a more natural extension of the type system if you could do
> something like below, and would avoid combinatorial explosion of protocol
> types (you go from O(n!) to O(n) things you actually have to define
> concretely):
>
> ```
> protocol WidthConstrainable {
>   func width(_ v: CGFloat) -> Self – WidthConstrainable
> }
> protocol HeightConstrainable {
>   func height(_ v: CGFloat) -> Self – HeightConstrainable
> }
> protocol XConstrainable {
>   func x(_ v: CGFloat) -> Self – XConstrainable
> }
> protocol YConstrainable {
>   func y(_ v: CGFloat) -> Self – YConstrainable
> }
> struct Constraint: WidthConstrainable, HeightConstrainable,
> XConstrainable, YConstrainable {
>   ...
> }
> ```
>
> If a type X is just a union or protocols (for example, X:
> WidthConstrainable & HeightConstrainable & XConstrainable &
> YConstrainable), the subtraction (X – something) is easy to define. It's
> either valid if the subtrahend is present in the set, or it's invalid (and
> detectable at compile time) if it's not.
>
> But there are still some rough edges: what does it mean when a concrete
> type is involved? Let's say you have T: P1 & P2 & P3, and you write (T –
> P1). That could give you a type that contains all the members of T except
> those in P1, which would be the members in P2, P3, and any that are defined
> directly on T that do not come from protocol conformances.
>
> But what is the relationship between types T and (T – P1)? (T – P1) being
> a supertype of T seems fairly straightforward—any instance of T can be
> expressed as (T – P1). But if I have an instance of type (T – P1), should I
> be able to cast that back to T? On the one hand, why not? I can obviously
> only get (T – P1) by starting with T at some point, so any instance of (T –
> P1) must *also* be an instance of T. So that implies that T is a supertype
> of (T – P1). In other words, they're supertypes of each other, without
> being the same type? That would be a first in Swift's type system, I
> believe. And if we allow the cast previously mentioned, that effectively
> circumvents the goal you're trying to achieve. (We could argue that you'd
> have to use a force-cast (as!) in this case.)
>
> This could be worked around by forbidding subtraction from concrete types
> and reducing T to the union of its protocols before performing the
> subtraction. In that case, (T – P1) would equal P2 & P3. But that
> relationship is still a little wonky: in that case, (T – P1) would also not
> contain any members that are only defined on T, even though the expression
> (T – P1) implies that they should. You would have to make that reduction
> explicit somehow in order for that to not surprise users (require writing
> something like `#protocols(of: T) – P1`?), and it leaves a certain subset
> of possible type expressions (anything that wants members defined on T
> without members of a protocol) unexpressible.
>
> I actually glossed over this earlier by writing "..." in the struct body.
> If I defined `width(_:)` there, what would my return type be? We currently
> forbid `Self` in that context. Would `Self – WidthConstrainable` be
> allowed? Would I have to use the new protocol-reduction operator above?
> More details that would have to be worked out.
>
> Protocol inheritance would pose similar questions. If you have this:
>
> ```
> protocol P1 {}
> protocol P2: P1 {}
> ```
>
> What is the subtype/supertype relationship between P2 and (P2 – P1)? It's
> the same situation we had with a concrete type. Maybe you just can't
> subtract a super-protocol without also subtracting its lowest sub-protocol
> from the type?
>
> My PL type theory knowledge isn't the deepest by any means, but if
> something like this was workable, I think it would be more feasible and
> more expressive than the member permutation approach. And that being said,
> this is probably a fairly narrow use case that wouldn't warrant the
> complexity it would bring to the type system to make it work.
>
>
> On Mon, Dec 26, 2016 at 7:03 AM Xiaodi Wu via swift-evolution <
> swift-evolution at swift.org> wrote:
>
> Should the following compile?
>
> let bar = foo.a()
> func f(_ g: T) {
> _ = g.a()
> }
> f(bar)
>
> If so, your proposal cannot guarantee each method is called only once. If
> not, how can bar be of type T?
>
> On Mon, Dec 26, 2016 at 06:30 Adrian Zubarev <
> adrian.zubarev at devandartist.com> wrote:
>
> I think I revise what I said about value semantics in my last post.
>
> let chain: T = foo.a()
>
> let new = chain
> new. // should not see `a` here
>
> It’s more something like a local scoped chain. I’m not sure how to call it
> correctly here. I’m not a native English speaker. =)
>
>
>
> --
> Adrian Zubarev
> Sent with Airmail
>
> Am 26. Dezember 2016 um 12:11:23, Adrian Zubarev (
> adrian.zubarev at devandartist.com) schrieb:
>
> By ‘calling once’ I meant, calling once at a single permutation chain. If
> the chain is escaped or followed by a non-permuting member that returns the
> same protocol, you’d have the ability to use all members at the starting
> point of the new chain.
>
> permuting protocol T {
>     func a()
>     func b()
>     func c()
>     func d()
> }
>
> var foo: T = …
>
> func boo(_ val: T) -> U {
>     // Here val escapes the chain and creates a new one
>     // That means that you can create a local permutation chain here again
>
>     val.a() // we can use `a` here
>     return …
> }
>
> boo(foo.a()) // a is immediately invoked here
>
> I imagine this keyword to follow value semantics, so that any possible
> mutation is handled locally with a nice extra ability of permutation member
> chaining.
>
> Did I understood your point correctly here?
> ------------------------------
>
> Sure the idea needs to be more fleshed out, but I’m curious if that’s
> something that we might see in Swift one day. :)
>
>
> --
> Adrian Zubarev
> Sent with Airmail
>
> Am 26. Dezember 2016 um 11:50:50, Xiaodi Wu (xiaodi.wu at gmail.com) schrieb:
>
> Given `foo: T` and methods a(), b(), c(), d(), each of which can only be
> called once, how can the return value of these methods be represented in
> the type system?
>
> That is, if `foo.a()` can be passed as an argument to an arbitrary
> function of type `(T) -> U`, either the function cannot immediately invoke
> a(), in which case foo is not of type T, or it can immediately invoke a(),
> in which case your keyword does not work.
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20161227/3e1d44f7/attachment.html>


More information about the swift-evolution mailing list