Swift 5.6: Member cannot be used on value of protocol type; use a generic constraint instead

I’m implementing a changelog of sorts for when my app syncs with iCloud. Types that conform to Recordable can be “recorded” in this changelog and “replayed” later on other devices. Each record is stored in the changelog as a JSON-encoded Data object.

In order to avoid switching on the record type at each use all over the place, I wanted to implement a convenience method that other parts can call to have stuff done on a record – without having to duplicate the switching (which is likely to be expanded with more types) and decoding.

I don’t know how much sense this makes; I’m probably missing central concepts in all this. It's illustrated in the much simplified Playground below.

My question are:

  1. What do the errors mean?
  2. Why can I use r.name, but not r.id?
  3. What’s needed to make this work (what am I doing wrong)?

Same errors occur if I make a computed Recordable value on Change that does the decoding.

Thanks :pray:

import Foundation

protocol Recordable: Codable & Identifiable where ID == Int64 {
	var name: String { get set }
}

struct Game: Recordable {
	var id: Int64
	var name: String
	var players: Array<Player>
}

struct Player: Recordable {
	var id: Int64
	var name: String
}

struct Change {
	
	let recordKind: String // The type name basically
	let recordData: Data // The record as a data object
	let operation: String // Insert, Update, or Delete
	
	init<R: Recordable>(record: R, recordKind: String, operation: String) throws {
		self.recordKind = recordKind
		self.recordData = try JSONEncoder().encode(record)
		self.operation = operation
	}
	
	/// Applies the closure on the `Recordable` record, encoded in our `recordData`.
	func apply(_ applying: (Recordable) -> Void) throws {
		
		let decoder = JSONDecoder()
		
		switch self.recordKind {
			case "Game":
				let game = try decoder.decode(Game.self, from: self.recordData)
				applying(game)
			case "Player":
				let player = try decoder.decode(Player.self, from: self.recordData)
				applying(player)
			case let kind:
				fatalError("Unknown Record Kind: \(kind)")
		}
	}
}

let player = Player(id: 1, name: "The player")
let game = Game(id: 2, name: "The game", players: [player])

// Record, print changes...

let changes = [
	try! Change(record: player, recordKind: "Player", operation: "Insert"),
	try! Change(record: game, recordKind: "Game", operation: "Insert"),
	try! Change(record: player, recordKind: "Player", operation: "Delete"),
]

for change in changes {
	try change.apply { (r: Recordable) in
		print("This prints fine: \(r.name), \(r)")
		let id = r.id // Error: Property 'id' requires that 'Recordable' be a class type
		print(r.id) // Error: Member 'id' cannot be used on value of protocol type 'Recordable'; use a generic constraint instead
	}
}
$ swiftc --version  
swift-driver version: 1.44.2 Apple Swift version 5.6 (swiftlang-5.6.0.320.8 clang-1316.0.18.8)
1 Like

Try this change:

    protocol Recordable: Codable {
        var id: Int64 { get }
        var name: String { get set }
    }
1 Like

This error is because the code is using a protocol requirement that uses Self or associated types on an SE-0309 existential type. In this case, the requirement id has type ID which is an associated type of Identifiable - the limitation exists even when the associated type is bound to a concrete type via same-type requirement.

This is supported by the SE-0309 proposal, but isn't implemented in Swift 5.6. I've disabled SE-0309 in Swift 5.6 for this reason. The suggestion in the error message is the only way to access that requirement for now - using a type parameter conforming to the protocol instead of the protocol/existential type. @tera 's suggested change to not refine Identifiable will also work, because that removes associated type requirements from the Recordable protocol altogether.

4 Likes

Thanks for your helpful answers! I have been struggling with understanding “protocol requirement that uses Self or associated types” from the beginning.

I come from OOP, and am not a compiler engineer (let alone any engineer, actually), and often want to be able to say: “here is some Recordable.” Regardless of which specific type it may be, with or without protocol constraints, its interface is known at compile time. Like having a base class and treating its children as such. Logically, at least to me, the compiler should know that each Recordable has an ID of type Int64, in this case, also from my original implementation.

For now I have removed the Codable where ID… from the protocol definition (and thereby the associated type requirement) and added Codable conformance on each Recordable-conforming type.

1 Like

You should watch WWDC 2018 Swift Generics (Expanded) for en explanation of how Swift does OOP with "parametric polymorphism" aka generic. This is different from the traditional class inheritance kind of oop.

You should also watch the linked to video Embrace Swift type inference because with generic, types can become very complex, impossible to figure out, but the compiler can figure out for you with type inference.

2 Likes