Concrete Type Conformance to 'Hashable' Protocol

generics
protocols
hashable

(Maurice) #1

Hi all,

I am trying to define a Word protocol which is then adopted by each type of word (adjective, adverb, noun, pronoun and verb). My goal is to have a dictionary support subscripting so that it accepts any type that conforms to Word. Initially Word was a struct and had an enum which stored the word type. However when adding new functionality that is specific to each word type, I found myself storing all properties in this struct (since structs cannot be subclassed per se).

Using classes, this would be easy to achieve by having one base class and subclassing for each type of word, which can then provide specific implementation. However I wanted to use protocols and structs for this problem.

protocol Word: Hashable {

    var word: String { get }

    // Additional properties of a word that apply to all types

}

struct Noun: Word {

    let word: String

    let gender: String // masculine, feminine, neuter

}

struct Verb: Word {

    let word: String

    let tense: String // present, future, past

}

// Structs for Adjective, Adverb and Pronoun omitted for the sake of brevity

let walk = Verb(word: "walk")

let andar = Verb(word: "andar")

var englishToSpanishDictionary: [Word: Word] = [walk: andar]

The above code results in the following error:

Using 'Word' as a concrete type conforming to protocol 'Hashable' is not supported

Would anyone be able to provide some insight as to how best to structure this sort of approach?

Note: even though I am declaring two verbs above, the dictionary of Word should also work with all other word types.


(Suyash Srijan) #2

You need a type eraser as protocols don't conform to themselves (exceptions apply). You can use AnyHashable or your own.

import Foundation

protocol Word: Hashable {
	var word: String { get }
}

struct Noun: Word {
	let word: String
	let gender: String
}

struct Verb: Word {
	let word: String
	let tense: String
}

let walk = Verb(word: "walk", tense: "")
let andar = Verb(word: "andar", tense: "")
var englishToSpanishDictionary: [AnyHashable: AnyHashable] = [walk: andar]

let word = englishToSpanishDictionary[walk]
print((word?.base as? Verb)?.word) // andar

(Davide De Franceschi) #3

The issue is with Hashable: it has some requirements such that it can be used only in homogeneous contexts. In other words, once Word conforms to Hashable, you cannot have a group of "words" in general, but only of the same type of word (so only all Noun or only all Verb).

One workaround is to type-erase and with a few coding hoops getting to create an AnyWord. Another is to wait for existential to be implemented :grimacing:


(Maurice) #4

Thank you all very much for your suggestions. I will consider using a type eraser.


(Benjamin Mayo) #5

What @DeFrenZ and @suyashsrijan said is completely right if you want to do a protocol-based solution. However, I'm not sure that a protocol is really the best approach.

If you have a small closed set of types (noun, verb, adjective, adverb, pronoun) then modelling as an enumeration is the natural way to go.

See this example structure:

struct WordEntry {
   let word: String 
   // additional properties of a word that apply to all types

   let kind: Kind

   enum Kind {
       case verb(tense: Tense)
       case noun(gender: Gender)

      // etcetera
   }

   enum Gender {
       case masculine
       case feminine
       case neuter
   }

   enum Tense {
       case present
       case future
       case past
   }
}

I changed the tense and gender to an enumeration, but they could be raw strings if you preferred. This type structure has a 'top level' where you can put properties that all types share, and then a deeper level represented by an enum that you can switch over to get kind-specific data out of.

One thing that might make you hesitate is that the initializer for a WordEntry is going to be unwieldy. You can make custom inits that simplify instantiation. For instance, you can write a custom init or static factory methods as convenient shortcuts, to remove boilerplate:

extension WordEntry {
    static func verb(_ word: String, tense: Tense) -> WordEntry { /* ... */ }
    static func noun(_ word: String, gender: Gender) -> WordEntry { /* ... */ }
}

let walk = WordEntry.verb("walk", tense: .present)
let phone = WordEntry.noun("phone", gender: .neuter)

You have to write these manually which is annoying, but you only have to do it once per kind. You could also add computed properties like var gender: Gender? for additional convenience when using the WordEntry types in your code.