0

I'm trying to create a system where implementations of a protocol (or abstract class) fulfill the minimum requirements of that protocol, but still has access to the unique functionality that come with the provided solution. Let me illustrate with an example:

interface Food
interface Animal {
    val favoriteFood: Food
}

class Banana: Food {
    fun peel() {
        print("Banana peeled")
    }
}

class Monkey: Animal {
    override val favoriteFood: Food = Banana()

    init {
        favoriteFood.peel() // Doesn't work as type is Food, not Banana
    }
}

Now to crack this I've been looking into generics and reified functions, but I haven't been able to come up with something that works in an expressive way.

I should note that my implementations would have to fulfill more than one requirement. I could make that work by using generics on the protocol.

interface Animal<T: Food, B: Behaviour>

This would work, but with multiple requirements it quickly looks ridiculous and I feel I'm missing something. Generics on properties don't work, unless I'm missing something in the next example.:

// Protocol
val <T> favoriteFood: <T: Food>
// Implementation (ideally, but wouldn't work like that I guess)
override val favoriteFood: Banana = Banana()

Then my reified approach doesn't look that pretty either:

open class Animal {
    // This should ideally be private to prevent incorrect use
    var favoriteFood: Food? = null

    fun registerFavoriteFood(food: Food) {
        favoriteFood = food
    }

    // This probably doesn't even have to be inline+reified
    inline fun <reified  T> getFavoriteFood() : T {
        // Of course we need to validate if favoriteFood matches T 
        return favoriteFood as T
    } 
}

class Monkey: Animal() {
    init {
        registerFavoriteFood(Banana())
        getFavoriteFood<Banana>().peel()
    }
}

At this point I'm not fully sure whether I don't see how to do it, or whether it's not possible. It feels like it's possible though and I wonder if any of you can point me in the right direction on how to solve it.

Or maybe it doesn't even make sense what I'm trying to do, but even that I'd like to hear, as it does make sense for the setup I envision.

Thanks in advance.

Edit after first response: I intend to access favoriteFood beyond the construction phase as well. Turned the mutable favoriteFood into a val as that better fits the goal.

Justin Hammenga
  • 1,091
  • 1
  • 8
  • 14

1 Answers1

1

As long as you're only trying to access the favoriteFood at construction time, this isn't too bad:

class Monkey: Animal {
    override var favoriteFood: Food? // no initializer here

    init {
        val food = Banana()
        favoriteFood = food
        food.peel()
    }
}

If you're not restricting yourself to init sections and other construction-time operations, then what you're trying to do is fundamentally broken.

val animal: Animal = Monkey()
animal.favoriteFood = Orange() // favoriteFood is a var, so it's publicly modifiable
animal.something() // can't assume favoriteFood is a Banana!

You could make favoriteFood into a val, which would help. But you can't make favoriteFood mutable from outside the type in Food and try to restrict the type of favoriteFood in subtypes.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • Thanks for a quick answer. In my case I'd access it post-construction as well. Good point on the immutable state, as that's what I intend. I'll add that as Edit Comments to the original post. The goal is to implement the minimum protocol requirements by supplying a more complex variant in the subtype, but still identifiable as the complex variant by it's protocol accessor. I keep looking at Swift's associatedtype on this topic as I feel that hints to what I seek: https://www.hackingwithswift.com/example-code/language/what-is-a-protocol-associated-type – Justin Hammenga Jan 23 '19 at 22:56
  • 1
    In that case, it sounds like you _do_ want the generic type parameter, in which case it should look like `class Animal { val favoriteFood: F }`. No casting or the like is necessary, but you _will_ have to accept the extra type parameters. – Louis Wasserman Jan 23 '19 at 23:55