[Pre-proposal] Use of angle bracket type generics with protocols


(Haravikk) #1

One of the big things that bugs me about working with protocols and generics is that they have a fundamentally different style to working with generics on structs and classes. While has some minor benefits on differentiating them, I think that overall it results in inconsistency that makes them harder to work with.

I’d like to propose two fairly minor changes that allow protocols to be defined using angle brackets for generics to make things much easier.

First is allowing a protocol to be defined using angle brackets for its generic type(s) like so:

  protocol FooType<Element, Index:ForwardIndexType> {
    func getElement(atIndex:Index) -> Element?
  }

In this example the protocol Foo has implicit type aliases for Element and Index, which can be fulfilled like so:

  struct Foo<Element> : FooType<Element, Int> {
    func getElement(atIndex:Index) -> Element? { … }
  }

The other important case is defining type constraints with protocol generics, currently we have to do stuff like this:

  func append<S:SequenceType where S.Generator.Element == Element>(contentsOf theSequence:S) { … }

However, while definition with a where clause is powerful, in the majority of cases it’s much more complex than it needs to be. So I’d like to propose that we can have the following:

  protocol SequenceType<Element> { … }
  func append(contentsOf theSequence:SequenceType<Element>) { … }

Behind the scenes these may be unwrapped into type aliases and where clauses like we have now, but for the developers this is much, much simpler to work with, especially when most generic constraints are pretty simple.

I’m not proposing the removal of where clauses as they can be really useful in more unusual cases, so will still have utility there. I’m not so sure about explicit type alias declaration like we have now though; using the angle brackets to declare these seems just as capable to me unless there are some cases I haven’t considered.

This seems like something that may have been discussed in the past, but I’m not that great on specific terminology for features (I’m not as well versed as others are on the correct names for language features) so I haven’t found anything, but fully expect that I may have missed it somehow, in which case a link would be appreciated.

But if no proposal currently exists then I’d love to know, as I could try to create a more formal proposal for this if necessary, as I know that I for one would benefit greatly from simplified protocol generics, as they’re currently one of my least favourite features to use syntactically, but one I need to use a lot.


(Ross O'Brien) #2

I've been using generics a fair amount recently. The Swift 3 notes declare
an intention for 'complete generics', but there isn't much information on
what this consists of.

I think there are three key elements to the terminology here. In a
declaration of a generic type, you have associated types, generic
parameters, and generic constraints. I imagine the Swift for a Set type
looks like this:

struct Set<Element where Element : Hashable> : CollectionType

{

typealias Index = SetIndex<Element>

}

Element is a generic parameter. Element : Hashable is a generic constraint.
Index is an associated type and it's required to conform to CollectionType.
The distinction is important; we could create a non-generic type IntSet
like this:

struct IntSet : CollectionType

{

typealias Index = SetIndex<Int>

}

I think it should be possible to declare a property which conforms to
CollectionType, constrained such that its Index is a SetIndex<Int> type (as
opposed to, say, SetIndex<String>). Or, I think it should be possible to
declare a property which conforms to GeneratorType, constrained that its
Element conforms to IntegerArithmeticType, so I can generate random numbers
and add them together and not yet decide whether they're Ints or Doubles or
whether the generation was by dice or playing cards.

I think I should be able to declare a Graph generic data structure with
values for its nodes and edges - e.g. a Travelling Salesman Problem graph
of Graph<City, Route> - and have a matching generic GraphTraverser type
which takes a graph and a starting node, and traverses the graph reporting
to a matching GraphTraversalDelegate type when it traverses an edge,
reaches a node, or needs a decision making on which of the available edges
it should traverse next. For this, the GraphTraverser should have a
matching 'generic parameter signature' to the Graph, but the
GraphTraversalDelegate will be context specific and should dictate
type-safe decisions based on City nodes and Route edges.

At the moment, this traversal pattern isn't possible because protocols
can't have generic parameters. I've seen programmers develop 'thunk' types
to work around this, but this should be neater.

So, I think it's important that we keep generic parameters and associated
types distinct - but, protocols should be able to handle generics, perhaps
with some implicit typealiasing.

The OP's examples; the developer would write this:

protocol FooType<Element, Index where Index : ForwardIndexType> {

func getElement(atIndex:Index) -> Element?

}

struct Foo<Element> : FooType<Element, Int> {

func getElement(atIndex:Index) -> Element? { ... }

}

But what would result would be this:

protocol FooType<Element, Index where Index : ForwardIndexType> {

typealias Element // implicitly generated associatedtypes and typealiases

typealias Index // implicit

func getElement(atIndex:Index) -> Element?

}

struct Foo<Element> : FooType {

typealias Element = Element // implicitly generated associatedtypes and
typealiases

typealias Index = Int // implicit

func getElement(atIndex:Index) -> Element? { return nil }

}

I'm not sure what I'd prefer for the syntax of a type which e.g. conformed
to FooType and whose Index was an Int. One of these two?
1) generic parameters matched by position in sequence

let foo : FooType<_, Int> = Foo<String>

2) associated type matched to type.

let foo : FooType<Index:Int> = Foo<String>

Here's a further code example - the graph traverser from before might look
like this:

struct GraphTraverser<NodeType, EdgeType>

{

let graph : Graph<NodeType, EdgeType>

var currentNode : NodeType

weak var delegate : GraphTraversalDelegate<NodeType, EdgeType>?

func move()

{

let options = graph.edgesFromNode(currentNode)

if let decision = delegate?.graphTraverser(self,
chooseEdgeFromOptions:options)

{

currentNode = decision.destination

delegate?.graphTraverser(self, didTraverseEdge:decision)

delegate?.graphTraverser(self, didReachNode:currentNode)

}

}

}

protocol GraphTraversalDelegate<NodeType, EdgeType>

{

// implicit typealiasing

func graphTraverser(graphTraverser:GraphTraverser<NodeType, EdgeType>,
didTraverseEdge edge:EdgeType)

func graphTraverser(graphTraverser:GraphTraverser<NodeType, EdgeType>,
didReachNode node:NodeType)

func graphTraverser(graphTraverser:GraphTraverser<NodeType, EdgeType>,
chooseEdgeFromOptions options:[EdgeType]) -> EdgeType?

}

struct TravellingSalesman : GraphTraversalDelegate<City, Route>

{

// implicit typealiasing; all NodeTypes are City, all EdgeType are Route

var citiesVisited : Set<City>

var distanceTravelled : Int

mutating func graphTraverser(graphTraverser:GraphTraverser<City, Route>,
didTraverseEdge edge:Route)

{

distanceTravelled += edge.distance

}

mutating func graphTraverser(graphTraverser:GraphTraverser<City, Route>,
didReachNode node:City)

{

citiesVisited.insert(node)

graphTraverser.move()

}

func graphTraverser(graphTraverser:GraphTraverser<City, Route>,
chooseEdgeFromOptions options:[Route]) -> Route?

{

return Random(options.filter{ route in
!citiesVisited.contains(route.destination) })

}

}

Comments?

···

On Fri, Feb 26, 2016 at 11:22 AM, Haravikk via swift-evolution < swift-evolution@swift.org> wrote:

One of the big things that bugs me about working with protocols and
generics is that they have a fundamentally different style to working with
generics on structs and classes. While has some minor benefits on
differentiating them, I think that overall it results in inconsistency that
makes them harder to work with.

I’d like to propose two fairly minor changes that allow protocols to be
defined using angle brackets for generics to make things much easier.

First is allowing a protocol to be defined using angle brackets for its
generic type(s) like so:

protocol FooType<Element, Index:ForwardIndexType> {
func getElement(atIndex:Index) -> Element?
}

In this example the protocol Foo has implicit type aliases for Element and
Index, which can be fulfilled like so:

struct Foo<Element> : FooType<Element, Int> {
func getElement(atIndex:Index) -> Element? { … }
}

The other important case is defining type constraints with protocol
generics, currently we have to do stuff like this:

func append<S:SequenceType where S.Generator.Element ==
>(contentsOf theSequence:S) { … }

However, while definition with a where clause is powerful, in the majority
of cases it’s much more complex than it needs to be. So I’d like to propose
that we can have the following:

protocol SequenceType<Element> { … }
func append(contentsOf theSequence:SequenceType<Element>) { … }

Behind the scenes these may be unwrapped into type aliases and where
clauses like we have now, but for the developers this is much, much simpler
to work with, especially when most generic constraints are pretty simple.

I’m not proposing the removal of where clauses as they can be really
useful in more unusual cases, so will still have utility there. I’m not so
sure about explicit type alias declaration like we have now though; using
the angle brackets to declare these seems just as capable to me unless
there are some cases I haven’t considered.

This seems like something that may have been discussed in the past, but
I’m not that great on specific terminology for features (I’m not as well
versed as others are on the correct names for language features) so I
haven’t found anything, but fully expect that I may have missed it somehow,
in which case a link would be appreciated.

But if no proposal currently exists then I’d love to know, as I could try
to create a more formal proposal for this if necessary, as I know that I
for one would benefit greatly from simplified protocol generics, as they’re
currently one of my least favourite features to use syntactically, but one
I need to use a lot.

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