Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make return type of function generic in Swift

Intro

In my app, I have one superclass called "ElementData" and several child classes that inherit from it.

Each child class has its own validateModel() method which returns a different type, depending on the class - always in an Array.

In other words: the method returns different Types in every subclass.

Example

Class A: func validateModel() -> [String]

Class B: func validateModel() -> [Int]

Class C: func validateModel() -> [MyCustomEnum]

As you can see, only the return values differ from each other.

EDIT: Examples of validateModel() method:

Class A:

func validateModel() -> [DefaultElementFields]{ // DefaultElementFields is an enum with the different view types of my collection view

        var checkResult: [DefaultElementFields] = []

        if name == "" {
            checkResult.append(.Name)
        }

        if Int(rewardedPoints) == nil {
            checkResult.append(.Points)
        }

        if description == "" {
            checkResult.append(.Description)
        }

        if selectedImage == nil {
            checkResult.append(.Image)
        }

        return checkResult
    }

Class B:

func validateModel() -> [Int] { // returns the index of the text field which is wrong
        var checkResult: [Int] = []

        let filledValues = codes.filter {
            $0 != ""
        }

        if filledValues.count == 0 { // if no values have been entered, all fields should be marked red.
            checkResult.append(-1)
            return checkResult
        }


        for (i, code) in codes.enumerated() {
            if code != "" && (code.count < 3 || code.count > 10 || code.rangeOfCharacter(from: NSCharacterSet.alphanumerics.inverted) != nil){ // code must have more than 3 and less than 11 characters. No symbols are allowed.
                checkResult.append(i)
            }
        }



        return checkResult
    }

EDIT: What the classes are for:

The classes basically store data a user entered into collection view cells, e.g. text, numbers or dates. Each CollectionViewCellType has its own class. Due to reuse behavior of the collection view, it is necessary to store the entered values in a model.

The model also takes care of the validation and returns - depending on the cell - an array of values that tells the cell which fields should get a red border (be marked as invalid).

This can sometimes be an Enum, an Int or a String.

What I would like to achieve

As you might imagine, it is quite annoying to have almost the same validationMethods in each child-class as it is necessary to downcast every time I want to use the method on one of the classes.

Therefore, I would like to keep the return type open i.e. don't specify a specific one in the parent class as the subclass should be able to return any type. Then, I would move the validateModel() method into the parent class and override the method in its subclasses.

I thought of a solution with generics (if it's possible).

What I tried

Here is my generic approach for the whole thing:

class ElementData {

    func validateModel<T>() -> [T] {
        return [1] as! [T] // just a test return
    }

}

And the call of the method:

dataObject.validateModel() // dataObject inherits from ElementData -> has access to validateModel()

Unfortunately, it does not work and I get the following error:

"Generic parameter 'T' could not be inferred"

Summary:

  • I have one superclass "ElementData" and several subclasses (classes that inherit)
  • Each subclass has a method validateModel() in which the model is validated
  • Only the return types of the validateModel() methods inside the subclasses differ - so I would like to put the method inside the parent class (ElementData) and just override it on the subclasses

Is that possible, if so, how?

Any help would be appreciated.

like image 948
linus_hologram Avatar asked Oct 16 '25 19:10

linus_hologram


2 Answers

This isn't possible.

What generics are for

Suppose you have this function:

func identity(_ value: Any) -> Any {
    return value
}

It doesn't actually work:

let i = 5
assert(identity(i) == i) // ❌ binary operator '==' cannot be applied to operands of type 'Any' and 'Int'

Any causes type information to be lost. Even though we see that the type of the parameter and the return value will always be the same, we haven't expressed that to the type system. This is a perfect use-case for a generic type parameter. It allows us to express the relationships between the types of the parameter and the return value.

func identity<T>(_ value: T) -> T {
    return value
}

let i = 5
assert(identity(i) == i) // ✅

What generics are not for

Looking back at your problem, you see that there are no type relationships here to be expressed.

  • ClassA.validateModel() always returns [String]
  • ClassB.validateModel() always returns [Int]
  • ClassC.validateModel() always returns [MyCustomEnum]

That's not generic.

How would it even work?

Suppose you had an object of type ElementData. That object could be an instance of ElementData, or of ClassA, or ClassB, or ClassC. Given that all four of these types are possible, and assuming there exists some concoction to do what you want, how would this code work?

let elementData = someElementData()
let validatedModel = elementData.validateModel() // 🤔 What type is `someValue` supposed to be?

Since we (nor the compiler) knows what concrete type the value of elementData will be (we only know that it's an ElementData, or one of its subclasses), how is the compiler supposed to determine the type of validatedModel ?

Furthermore, your code would be breaking the Liskov Substitution Principle. ClassA needs to support being substituted where ElementData is expected. One of the things that ElementData.validateModel()can do is return a Something. Thus, ClassA.validateModel() needs to either return a Something, or a subclass (strangely, it seems that only inheritence relationships work, not protocol subtype relationships. For example, returning Int where Any is expected doesn't work). Since ClassA.validateModel() returns Array<String>, and Array isn't a class (thus, can't have a superclass), there's no possible type Something that could be used to make the code not violate LSP and compile.

Here's an illustration of LSP, and how Covariance works in return types of overridden methods, and not in parameter types of overridden methods.

// https://www.mikeash.com/pyblog/friday-qa-2015-11-20-covariance-and-contravariance.html

class Animal {}
class Cat: Animal {}
    
class Person {
    func purchaseAnimal() -> Animal {
        return Animal()
    }
}

class CrazyCatLady: Person {
    // Totally legal. `Person` has to be able to return an `Animal`.
    // A `Cat` is an animal, so returning a `Cat` where an `Animal` is required is totally valid
    override func purchaseAnimal() -> Cat {
        return Cat()
    }

//  This method definition wouldn't be legal, because it violates the Liskov Substitution Principle (LSP).
//  A `CrazyCatLady` needs to be able to stand in anywhere a `Person` can be used. One of the things a
//  `Person` can do is to `pet(animal: Animal)`. But a `CrazyCatLady` can't, because she can only pet cats.
//
//  If this were allowed to compile, this could would be undefined behaviour:
//
//      let person: Person = getAPerson()
//      let animal: Animal = getAnAnimal()
//      person.pet(animal)
//
//  override func pet(animal: Cat) { // ❌ method does not override any method from its superclass
//      
//  }
}

One approach to a solution

First of all, we need to establish what's in common between these return types. If we could do that, then the compiler has an answer to the question of "what type should someModel be?" above.

There are two tools available:

  1. Class inheritance (subclasses are subtypes of their superclasses)
  2. Protocol conformance (protocol conforming types are subtypes of the protocols they conform to)

Both have advantages/disadvantages. Protocols force you in the painful road of dispair that is associated-type, whereas Classes are less flexible (since they can't be subclassed by enums or structs). In this case, the answer lies with what you want this code to do. Fundamentally, you're trying to hook this data up to a table cell. So make a protocol for that:

protocol CellViewDataSource {
    func populate(cellView: UICellView) {
        // adjust the cell as necessary.
    }
} 

Now, update your methods to return this type:

class ElementData {
    func validateModel() -> CellViewDataSource {
        fatalError()
    }
}

class ClassA {
    func validateModel() -> CellViewDataSource {
        fatalError()
    }
}

To implement these methods, you'll have to extend Array to conform to CellViewDataSource. However, that's a pretty terrible idea. I suggest instead that you create a new type (probably a struct) that stores the data you need.

struct ModelA {
    let name: String
    let points: Int
    let description: String
    let image: UIImage
}

extension ModelA: CellViewDataSource {
    func populate(cellView: UICellView) {
        // Populate the cell view with my `name`, `points`, `description` and `image`.
    }
}

class ElementData {
    func validateModel() -> CellViewDataSource {
        fatalError("Abstract method.")
    }
}

class ClassA {
    func validateModel() -> CellViewDataSource {
        return ModelA(
            name: "Bob Smith",
            points: 123,
            description: "A dummy model.",
            image: someImage()
        )
    }
}
like image 132
Alexander Avatar answered Oct 19 '25 11:10

Alexander


A possible solution is a protocol with associated types. You have to specify the return type as typealias in each subclass.

protocol Validatable {
    associatedtype ReturnType
    func validateModel() -> [ReturnType]
}

class ElementData {}

class SubClassA : ElementData, Validatable {
    typealias ReturnType = Int

    func validateModel() -> [Int] { return [12] }

}

class SubClassB : ElementData, Validatable {
    typealias ReturnType = String

    func validateModel() -> [String] { return ["Foo"] }
}

Now the compiler knows the different return type of all subclasses

enter image description here

like image 23
vadian Avatar answered Oct 19 '25 12:10

vadian



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!