What is the purpose of declaring typealias for generic type parameter

Viewed 1498

Consider following code:

public struct Foo<E: Hashable>: Equatable, Collection 
{
    public typealias Element = E
    private var array: [Element]
...
}

This kind of coding practice can be found in many places, including official Apple code-bases like https://github.com/apple/swift-package-manager/blob/master/Sources/Basic/OrderedSet.swift

What is the purpose of declaring typealias Element instead of using Element directly as generic type parameter name like:

public struct Foo<Element: Hashable> 
{
    private var array: [Element]
...
}

Is it just some sort of coding preference, or there are some other reasons, like achieving some functionality that could not be made without such simple typealias declarations?

5 Answers

Declaring a public typealias makes it possible to access the generic type parameter outside of the closed generic type.

For example if you declare a typealias WidgetFoo = Foo<Widget> and keep using WidgetFoo in other places it will be possible to access its T via WidgetFoo.Element(which refers to Widget) whereas you cannot access the generic type parameter E itself. This enables robust and refactoring friendly code - imagine you want to replace Widget with BetterWidget you only have to change one place (the type alias declaration) and no other because WidgetFoo.Element will then refer to BetterWidget.


Example code (provided by @Airspeed Velocity)

struct S<T> { typealias U = T }

typealias TA = S<Int>

let x: TA.T // 'T' is not a member type of 'TA' (aka 'S<Int>')
let y: TA.U

Here is a simple example of why you (sometimes) need to declare type aliases in a type that implements a protocol.

The Collection protocol is "generic" in that it contains "placeholder" types, known as associated types in Swift syntax. Collection includes two associated types which must be resolved in an implementing type: Element and Index.

In this example of a stack, we can start by declaring type aliases that will tell the compiler which concrete types we are going to use to satisfy the associated types from the protocol (itemT needs to derive from Hashable if it is to match the requirement for the Element associated type from the protocol):

class Stack<itemT : Hashable> : Collection
{
  typealias Element = itemT

  typealias Index = Int

  lazy var items: [itemT] = .init()

  func push(item: itemT)
  {
    items.append(item)
  }

  func pop() -> itemT?
  {
    guard let result = items.last,
          let lastIndex = items.lastIndex(of: result) else
    {
      return nil
    }

    items.remove(at: lastIndex)

    return result
  }
}

This now means that the compiler knows the Collection's Element type is going to be whatever we use as the itemT type when we use the stack. The Collection's Index type is going to be an Int, because that is the index type of the array we are using to hold the stack's items

Unfortunately, that is not enough to satisfy the requirements of the Collection protocol because there are a few "abstract" methods that also have to be implemented. So, we add those methods to our stack class. This will then tell the compiler the signatures of the required methods and allows the correct Fixits to be suggested, which, if we apply them, gives us the following code:

class Stack<itemT : Hashable> : Collection
{
  typealias Element = itemT

  typealias Index = Int

  lazy var items: [itemT] = .init()

  func push(item: itemT)
  {
    items.append(item)
  }

  func pop() -> itemT?
  {
    guard let result = items.last,
          let lastIndex = items.lastIndex(of: result) else
    {
      return nil
    }

    items.remove(at: lastIndex)

    return result
  }

  var startIndex: Int
  {
    return items.startIndex
  }

  var endIndex: Int
  {
    return items.endIndex
  }

  func index(after i: Int) -> Int
  {
    return items.index(after: i)
  }

  subscript(position: Int) -> itemT
  {
    return items[position]
  }
}

Finally, the class compiles! As a side effect of the compiler having now suggested correctly typed implementations of its "abstract" methods, it no longer needs us to tell it what the associated types from the protocol need to be linked to in our implementing type. So, we can now remove the type aliases from our code thus:

class Stack<itemT : Hashable> : Collection
{
  lazy var items: [itemT] = .init()

  func push(item: itemT)
  {
    items.append(item)
  }

  func pop() -> itemT?
  {
    guard let result = items.last,
          let lastIndex = items.lastIndex(of: result) else
    {
      return nil
    }

    items.remove(at: lastIndex)

    return result
  }

  var startIndex: Int
  {
    return items.startIndex
  }

  var endIndex: Int
  {
    return items.endIndex
  }

  func index(after i: Int) -> Int
  {
    return items.index(after: i)
  }

  subscript(position: Int) -> itemT
  {
    return items[position]
  }
}

Whether or not we need to declare type aliases for associated types often depends on the complexity of the associated types in a given protocol.

typealias and associatedtype mean that the values of type parameters can be hidden and become an implementation detail rather than the public interface of a type. This gets rid of an awful lot of noise when defining extensions, properties, methods, referencing superclasses/protocols etc.

Russ Bishop explains this really well with excellent examples here.

We seem to be talking at cross purposes here.

You cite this code:

public struct Foo<E: Hashable>: Equatable, Collection 
{
  public typealias Element = E

  private var array: [Element]

  …
}

… where the generic struct Foo<E : Hashable> has a placeholder E (which is required to implement Hashable, in order that any type that is used for E will be compatible with the Element parameter declared in the Collection protocol); and the struct Foo<E> also is required to implement the Collection (generic) protocol.

The typealias Element = E line is there to bind the E parameter of the generic struct to the Element associatedtype from the (generic) Collection protocol; nothing more nothing less. This is the reason I gave you my example of using type aliases to help the compiler to give you the missing "abstract" methods that are needed to fully implement the Collection protocol.

On the other hand, your code:

public struct Foo<Element: Hashable> 
{
  private var array: [Element]
  …
}

… has nothing to do with implementing a protocol; it is simply a generic struct with a placeholder parameter called Element, which is required to implement Hashable.

This time, the struct simply contains an array of whatever Element is used as when you use Foo<Element>. There is no need for a typealias because there is nothing to bind the Element placeholder to; it serves the same purpose as E in the first example, but without the bit where the struct implements Collection.

I must admit to being puzzled about what @Airspeed Velocity's code:

struct S<T> { typealias U = T }

typealias TA = S<Int>

let x: TA.T // 'T' is not a member type of 'TA' (aka 'S<Int>')
let y: TA.U

… is intended to achieve. Why would you want a typealias U for the type T that is already known and available? In many, many years of programming generics, it something I've not yet come across, unless it's another way of writing something else I might have used in C# generics.

Let me replace Swift's own Collection protocol with one just for you Dalija:

protocol DalijaCollection
{
  associatedtype ItemType : Hashable // placeholder - does the same as Element from Collection

  func append(item: ItemType)

  func remove(item: ItemType)

  var count: Int { get }

  subscript(index: Int) -> ItemType { get }
}


class Stack<itemT : Hashable> : DalijaCollection
{
  typealias ItemType = itemT // not needed once all the members of DalijaConnection have been implemented

  lazy var items: [itemT] = .init()

  func push(item: itemT)
  {
    items.append(item)
  }

  func pop() -> itemT?
  {
    guard let result = items.last,
          let lastIndex = items.lastIndex(of: result) else
    {
      return nil
    }

    items.remove(at: lastIndex)

    return result
  }

  func append(item: itemT)
  {
    items.append(item)
  }

  func remove(item: itemT)
  {
    guard let index = items.index(of: item) else
    {
      fatalError("item not found")
    }

    items.remove(at: index)
  }

  var count: Int
  {
    return items.count
  }

  subscript(index: Int) -> itemT
  {
    return items[index]
  }
}

With the DalijaCollection version, you can do this:

  while stack.count > 0
  {
    print(stack.pop())
  }

  // or

  for i in 0..<stack.count
  {
    print(stack[i])
  }

… Swift's Collection protocol also allows you to use the for…in construct like this:

{
  for item in stack
  {
    print(item)
  }
}

… as well as a whole load more.

Related