[pitch] composition as part of the language


(Jay) #1

Let's take a quick look at how we can achieve very simple compile-time
composition in Swift today.

I want to define a `Doorman` behaviour, and I want to compose it from other
behaviours that are shared by some of my other staff types:

protocol Greeter {
    func greet()
}
protocol Fareweller {
    func farewell()
}
protocol Doorman: Greeter, Fareweller {}

Great - that's my interface defined, now some implementations that I can
compose my staff from:

protocol FriendlyGreeter: Greeter {}
extension FriendlyGreeter {
    func greet() {
        print("Hello and welcome")
    }
}

protocol FriendlyFareweller: Fareweller {}
extension FriendlyFareweller {
    func farewell() {
        print("I bid thee farewell")
    }
}

protocol InsultingGreeter: Greeter {}
extension InsultingGreeter {
    func greet() {
        print("You make me sick")
    }
}

protocol InsultingFareweller: Fareweller {}
extension InsultingFareweller {
    func farewell() {
        print("Get lost")
    }
}

Now we have two kinds of `Greeter` and two kinds of `Fareweller` that can
be used to compose different `Doorman` types (and potentially other staff
types). Here's two examples:

struct FriendlyDoorman: Doorman, FriendlyGreeter, FriendlyFareweller {}
struct TrickingDoorman: Doorman, FriendlyGreeter, InsultingFareweller {}

I can instantiate and make use of these to perform their defined behaviours:

let friendly: Doorman = FriendlyDoorman()
let tricking: Doorman = TrickingDoorman()
friendly.greet() // Hello and welcome
friendly.farewell() // I bid thee farewell
tricking.greet() // Hello and welcome
tricking.farewell() // Get lost

It works! But there are some things that could be nicer:
* I don't really want `***Greeter` or `***Fareweller` to be sub-protocols
at all, these are supposed to be implementations - the only reason they are
protocols is so I can extend them with a default implementation and then
use more than one of them to compose my actual `Doorman` types. This
clutters up the namespace with unnecessary protocols, that have the same
interface as their parent.
* Since the `***Doorman` types need to be instantiable, they are structs. I
couldn't compose a `LobbyMultiTasker` from a `FriendlyDoorman` and a
`GrumpyPorter` at compile-time, the same easy way I composed the
`***Doorman` types. The manual solution would be to add properties for
`doormanDelegate` and `porterDelegate` and assign appropriate instances
(run-time composition), then add the boiler-plate to pass on the
`LobbyMultiTasker` behaviour to these delegates. This is also how the
compiler could implement this feature.
* Actually providing the implementations is optional for the protocols (the
extensions can happily be missing), meaning any error messages will appear
in the types that fail to fully implement the protocols, instead of here in
my `***Greeter` implementation.

So I'd like to discuss the possibility of a new category of types called
`component`, to allow compile-time composition; composition as part of the
language. The `***Greeter` and `***Fareweller` might look like this:

component FriendlyGreeter: Greeter {
    func greet() {
        print("Hello and welcome")
    }
}

component FriendlyFareweller: Fareweller {
    func farewell() {
        print("I bid thee farewell")
    }
}

component InsultingGreeter: Greeter {
    func greet() {
        print("You make me sick")
    }
}

component InsultingFareweller: Fareweller {
    func farewell() {
        print("Get lost")
    }
}

And the `TrickingDoorman` might look like this:

component TrickingDoorman: Doorman⎄(FriendlyGreeter, InsultingFareweller) {
    // optionally override any Doorman-defined functions
}

Here's how I think they would work:

* Components must conform to at least one protocol.
* Components must provide an implementation for all the things from the
protocols - no part of it is 'abstract' - and they can't provide extra
things (although it might be useful to allow some kind of config-values,
but I'd say initially keep it simple).
* Components can be composed of other components and override some/all of
the borrowed behaviour. This should provide well defined
multiple-inheritence semantics (not sure of details), some syntax would be
required to allow the compiler to totally flatten the component, selecting
which "parent" implementations to use if needed to satisfy all the
protocols. Only the functions from the explicit protocols are pulled in,
not everything implemented in the "parent" components.
* Components can be instantiated and have value-semantics, like structs
(they are basically structs with extra features/checks). They can therefore
be used at compile-time but also at run-time for example `delegate =
MyComponent()` - so composed behaviour can be either static or dynamic,
also existing protocol-based `delegate` variables can have a component
instance assigned.
* Classes, structs, and enums can NOT override functions implemented in a
component, when they compose themselves from those components. To do this,
create a sub-component and override the method, then compose your type from
that instead.

Other benefits:
* I think this would encourage more single-responsibility by default.
Because when designing a protocol, people tend to forget composition.
Enforcing must-implement-everything would remind/encourage API designers to
split protocols up, especially if they want to provide a default
implementation for some but not all of the functions.
* Tidier code, with much clearer intention (component vs extension in
particular).
* Easy re-use without having pass-through boilerplate code.
* Brings well-defined-ness to default implementations, which can sometimes
be unclear whether it's an actual default implementation or an empty
placeholder

I haven't thought of everything here, obviously - and I'm tired, so please
poke holes and supply constructive corrections :slight_smile:

Any suggestions for the "composed-of" syntax for when a type wants to
utilise a component would be welcome. I used `Doorman⎄(FriendlyGreeter,
InsultingFareweller)` in the example above, where ⎄ is the composition
symbol that I just discovered, but this is just intended to be a
placeholder.


#2

This sounds similar to automatic protocol forward, have you looked into
prior discussions on that topic here?

Nevin

···

On Thu, Jun 22, 2017 at 10:56 PM, Jay Abbott via swift-evolution < swift-evolution@swift.org> wrote:

Let's take a quick look at how we can achieve very simple compile-time
composition in Swift today.

I want to define a `Doorman` behaviour, and I want to compose it from
other behaviours that are shared by some of my other staff types:

protocol Greeter {
    func greet()
}
protocol Fareweller {
    func farewell()
}
protocol Doorman: Greeter, Fareweller {}

Great - that's my interface defined, now some implementations that I can
compose my staff from:

protocol FriendlyGreeter: Greeter {}
extension FriendlyGreeter {
    func greet() {
        print("Hello and welcome")
    }
}

protocol FriendlyFareweller: Fareweller {}
extension FriendlyFareweller {
    func farewell() {
        print("I bid thee farewell")
    }
}

protocol InsultingGreeter: Greeter {}
extension InsultingGreeter {
    func greet() {
        print("You make me sick")
    }
}

protocol InsultingFareweller: Fareweller {}
extension InsultingFareweller {
    func farewell() {
        print("Get lost")
    }
}

Now we have two kinds of `Greeter` and two kinds of `Fareweller` that can
be used to compose different `Doorman` types (and potentially other staff
types). Here's two examples:

struct FriendlyDoorman: Doorman, FriendlyGreeter, FriendlyFareweller {}
struct TrickingDoorman: Doorman, FriendlyGreeter, InsultingFareweller {}

I can instantiate and make use of these to perform their defined
behaviours:

let friendly: Doorman = FriendlyDoorman()
let tricking: Doorman = TrickingDoorman()
friendly.greet() // Hello and welcome
friendly.farewell() // I bid thee farewell
tricking.greet() // Hello and welcome
tricking.farewell() // Get lost

It works! But there are some things that could be nicer:
* I don't really want `***Greeter` or `***Fareweller` to be sub-protocols
at all, these are supposed to be implementations - the only reason they are
protocols is so I can extend them with a default implementation and then
use more than one of them to compose my actual `Doorman` types. This
clutters up the namespace with unnecessary protocols, that have the same
interface as their parent.
* Since the `***Doorman` types need to be instantiable, they are structs.
I couldn't compose a `LobbyMultiTasker` from a `FriendlyDoorman` and a
`GrumpyPorter` at compile-time, the same easy way I composed the
`***Doorman` types. The manual solution would be to add properties for
`doormanDelegate` and `porterDelegate` and assign appropriate instances
(run-time composition), then add the boiler-plate to pass on the
`LobbyMultiTasker` behaviour to these delegates. This is also how the
compiler could implement this feature.
* Actually providing the implementations is optional for the protocols
(the extensions can happily be missing), meaning any error messages will
appear in the types that fail to fully implement the protocols, instead of
here in my `***Greeter` implementation.

So I'd like to discuss the possibility of a new category of types called
`component`, to allow compile-time composition; composition as part of the
language. The `***Greeter` and `***Fareweller` might look like this:

component FriendlyGreeter: Greeter {
    func greet() {
        print("Hello and welcome")
    }
}

component FriendlyFareweller: Fareweller {
    func farewell() {
        print("I bid thee farewell")
    }
}

component InsultingGreeter: Greeter {
    func greet() {
        print("You make me sick")
    }
}

component InsultingFareweller: Fareweller {
    func farewell() {
        print("Get lost")
    }
}

And the `TrickingDoorman` might look like this:

component TrickingDoorman: Doorman⎄(FriendlyGreeter, InsultingFareweller) {
    // optionally override any Doorman-defined functions
}

Here's how I think they would work:

* Components must conform to at least one protocol.
* Components must provide an implementation for all the things from the
protocols - no part of it is 'abstract' - and they can't provide extra
things (although it might be useful to allow some kind of config-values,
but I'd say initially keep it simple).
* Components can be composed of other components and override some/all of
the borrowed behaviour. This should provide well defined
multiple-inheritence semantics (not sure of details), some syntax would be
required to allow the compiler to totally flatten the component, selecting
which "parent" implementations to use if needed to satisfy all the
protocols. Only the functions from the explicit protocols are pulled in,
not everything implemented in the "parent" components.
* Components can be instantiated and have value-semantics, like structs
(they are basically structs with extra features/checks). They can therefore
be used at compile-time but also at run-time for example `delegate =
MyComponent()` - so composed behaviour can be either static or dynamic,
also existing protocol-based `delegate` variables can have a component
instance assigned.
* Classes, structs, and enums can NOT override functions implemented in a
component, when they compose themselves from those components. To do this,
create a sub-component and override the method, then compose your type from
that instead.

Other benefits:
* I think this would encourage more single-responsibility by default.
Because when designing a protocol, people tend to forget composition.
Enforcing must-implement-everything would remind/encourage API designers to
split protocols up, especially if they want to provide a default
implementation for some but not all of the functions.
* Tidier code, with much clearer intention (component vs extension in
particular).
* Easy re-use without having pass-through boilerplate code.
* Brings well-defined-ness to default implementations, which can sometimes
be unclear whether it's an actual default implementation or an empty
placeholder

I haven't thought of everything here, obviously - and I'm tired, so
please poke holes and supply constructive corrections :slight_smile:

Any suggestions for the "composed-of" syntax for when a type wants to
utilise a component would be welcome. I used `Doorman⎄(FriendlyGreeter,
InsultingFareweller)` in the example above, where ⎄ is the composition
symbol that I just discovered, but this is just intended to be a
placeholder.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jay) #3

Today I had a read through the various protocol forwarding threads in
December 2015, including Matthew Johnson's proposal, and some more recent
ones in which it was discussed.

I think the ideas are very similar in many ways - it's to improve the
experience of using composition and make it easier. The protocol forwarding
seems to be mostly about reducing boilerplate code, but I'd really like to
see composition become part of the language itself. One of the fantastic
thing about the evolution of programming languages is how programmers
discover many good and bad patterns, then we make new language features to
incorporate the good patterns and mitigate against the bad ones. Data and
pointer types, classes/objects, inheritance, abstract interfaces, to name
but a few are all derived from good disciplined practice, codified such
that they become easier, or required practices.

The protocol forwarding proposal is great, but it seems like the good
pattern/practice it is trying to aid (composition) would still be a
programmer discipline that sits atop that particular language feature. Does
anyone else think encouraging composition directly as a language feature
would be a good thing? A new platform-level on top of which we can develop
new good and bad patterns, to continue the evolution? It's apparent from
other active threads on this list that composition over inheritance is
still not widely followed. I still make this same mistake. I remember going
from C++ to Java (this was probably around 17 years ago now) I often wanted
multiple inheritance, but the language wouldn't let me and I soon realised
the error of my ways and the power of interfaces. This is the great power
of a well designed language feature - it forces programmers to improve
their thinking while making it easier to write better code at the same
time. These days we can easily compose interfaces (in Swift for example we
can adopt multiple protocols, or use protocol composition) but we still
can't easily compose implementations. We tend to use delegates for
composition, and auto-forwarding seems to be a good approach for making
this pattern easier, but composing a type's implementation from other
existing component implementations is subtly different to delegation.

So instead of wanting to inherit from a struct, or forward a protocol to
avoid boilerplate, I want composition to be a natural and easy way to think
of a particular type, and I want the language and compiler to nudge my
thinking towards this way when I start to do something stupid. Is anyone
with me on this or am I just out there talking crazy?

In the specifics of this case, there seem to be some issues with `Self`
parameters and return types in the protocol forwarding proposal. Also, the
idea of a lot of synthesized boilerplate to forward calls seems a bit
messy. Perhaps it would be good if composition were able to pull in
implementations like templates. So `Self` types become the type of the
composed type, and the compiler decides when it needs to generate a
type-specific version or when to use a generic version of an
implementation. And the intention would be very clear because a component
of a composition would be explicitly named something obvious like
`component`.

It'd be great to hear Matthew Johnson's thoughts on this, and anyone else
who has been involved with the forwarding discussions.

···

On Fri, 23 Jun 2017 at 14:29 Nevin Brackett-Rozinsky < nevin.brackettrozinsky@gmail.com> wrote:

This sounds similar to automatic protocol forward, have you looked into
prior discussions on that topic here?

Nevin

On Thu, Jun 22, 2017 at 10:56 PM, Jay Abbott via swift-evolution < > swift-evolution@swift.org> wrote:

Let's take a quick look at how we can achieve very simple compile-time
composition in Swift today.

I want to define a `Doorman` behaviour, and I want to compose it from
other behaviours that are shared by some of my other staff types:

protocol Greeter {
    func greet()
}
protocol Fareweller {
    func farewell()
}
protocol Doorman: Greeter, Fareweller {}

Great - that's my interface defined, now some implementations that I can
compose my staff from:

protocol FriendlyGreeter: Greeter {}
extension FriendlyGreeter {
    func greet() {
        print("Hello and welcome")
    }
}

protocol FriendlyFareweller: Fareweller {}
extension FriendlyFareweller {
    func farewell() {
        print("I bid thee farewell")
    }
}

protocol InsultingGreeter: Greeter {}
extension InsultingGreeter {
    func greet() {
        print("You make me sick")
    }
}

protocol InsultingFareweller: Fareweller {}
extension InsultingFareweller {
    func farewell() {
        print("Get lost")
    }
}

Now we have two kinds of `Greeter` and two kinds of `Fareweller` that can
be used to compose different `Doorman` types (and potentially other staff
types). Here's two examples:

struct FriendlyDoorman: Doorman, FriendlyGreeter, FriendlyFareweller {}
struct TrickingDoorman: Doorman, FriendlyGreeter, InsultingFareweller {}

I can instantiate and make use of these to perform their defined
behaviours:

let friendly: Doorman = FriendlyDoorman()
let tricking: Doorman = TrickingDoorman()
friendly.greet() // Hello and welcome
friendly.farewell() // I bid thee farewell
tricking.greet() // Hello and welcome
tricking.farewell() // Get lost

It works! But there are some things that could be nicer:
* I don't really want `***Greeter` or `***Fareweller` to be sub-protocols
at all, these are supposed to be implementations - the only reason they are
protocols is so I can extend them with a default implementation and then
use more than one of them to compose my actual `Doorman` types. This
clutters up the namespace with unnecessary protocols, that have the same
interface as their parent.
* Since the `***Doorman` types need to be instantiable, they are structs.
I couldn't compose a `LobbyMultiTasker` from a `FriendlyDoorman` and a
`GrumpyPorter` at compile-time, the same easy way I composed the
`***Doorman` types. The manual solution would be to add properties for
`doormanDelegate` and `porterDelegate` and assign appropriate instances
(run-time composition), then add the boiler-plate to pass on the
`LobbyMultiTasker` behaviour to these delegates. This is also how the
compiler could implement this feature.
* Actually providing the implementations is optional for the protocols
(the extensions can happily be missing), meaning any error messages will
appear in the types that fail to fully implement the protocols, instead of
here in my `***Greeter` implementation.

So I'd like to discuss the possibility of a new category of types called
`component`, to allow compile-time composition; composition as part of the
language. The `***Greeter` and `***Fareweller` might look like this:

component FriendlyGreeter: Greeter {
    func greet() {
        print("Hello and welcome")
    }
}

component FriendlyFareweller: Fareweller {
    func farewell() {
        print("I bid thee farewell")
    }
}

component InsultingGreeter: Greeter {
    func greet() {
        print("You make me sick")
    }
}

component InsultingFareweller: Fareweller {
    func farewell() {
        print("Get lost")
    }
}

And the `TrickingDoorman` might look like this:

component TrickingDoorman: Doorman⎄(FriendlyGreeter, InsultingFareweller)
{
    // optionally override any Doorman-defined functions
}

Here's how I think they would work:

* Components must conform to at least one protocol.
* Components must provide an implementation for all the things from the
protocols - no part of it is 'abstract' - and they can't provide extra
things (although it might be useful to allow some kind of config-values,
but I'd say initially keep it simple).
* Components can be composed of other components and override some/all of
the borrowed behaviour. This should provide well defined
multiple-inheritence semantics (not sure of details), some syntax would be
required to allow the compiler to totally flatten the component, selecting
which "parent" implementations to use if needed to satisfy all the
protocols. Only the functions from the explicit protocols are pulled in,
not everything implemented in the "parent" components.
* Components can be instantiated and have value-semantics, like structs
(they are basically structs with extra features/checks). They can therefore
be used at compile-time but also at run-time for example `delegate =
MyComponent()` - so composed behaviour can be either static or dynamic,
also existing protocol-based `delegate` variables can have a component
instance assigned.
* Classes, structs, and enums can NOT override functions implemented in a
component, when they compose themselves from those components. To do this,
create a sub-component and override the method, then compose your type from
that instead.

Other benefits:
* I think this would encourage more single-responsibility by default.
Because when designing a protocol, people tend to forget composition.
Enforcing must-implement-everything would remind/encourage API designers to
split protocols up, especially if they want to provide a default
implementation for some but not all of the functions.
* Tidier code, with much clearer intention (component vs extension in
particular).
* Easy re-use without having pass-through boilerplate code.
* Brings well-defined-ness to default implementations, which can
sometimes be unclear whether it's an actual default implementation or an
empty placeholder

I haven't thought of everything here, obviously - and I'm tired, so
please poke holes and supply constructive corrections :slight_smile:

Any suggestions for the "composed-of" syntax for when a type wants to
utilise a component would be welcome. I used `Doorman⎄(FriendlyGreeter,
InsultingFareweller)` in the example above, where ⎄ is the composition
symbol that I just discovered, but this is just intended to be a
placeholder.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution