Guess the Protocol Behavior

In Today's post, well, this evenings' really, it's time to play "guess what happens".

Let's start with the given code:

public protocol Animal {
    func speak()
}

extension Animal {
    public func speak() {
        print("rawr?")
    }
}

Simple enough, all animals say "rawr?", by default.

Let's add a bit more to the puzzle:

public struct Dog : Animal {
    public func speak() {
        print("ruff ruff!")
    }

    public init() {} // yes, we must define this because of "good reasons"
}

This is all pretty straight forward, nothing really interesting here. So let's create a little function to get the animals talking.

public func talk(animals: [Animal]) {
    animals.forEach { $0.speak() }
}

This is where it is going to start to get interesting:

public struct Sheep : Animal {
    public init() {}
}

And then:

let animals : [Animal] = [Dog(), Sheep()]
talk(animals)

The output is:

ruff ruff!
rawr?

But what if this is added:

extension Sheep {
    func speak() {
        print("bah!")
    }
}

What if I told you the output what still:

ruff ruff!
rawr?

Can you tell me why that is?

If I did this:

Sheep().speak()

The output would be correctly this:

bah!

The issue here is where the talk() function is defined. If talk() is defined within the same module as the extension for Sheep, then the output is the following:

ruff ruff!
bah!

However, if the talk() function is defined outside of that module, well then the output is:

ruff ruff!
rawr?

This behavior is unsettling to me. For one, it makes some sense that you cannot change the functionality of another module with extensions to types belonging to that module. On the other hand, if I provide an extension for Sheep in my module, I'll be able to use the new functionality just fine there, but anytime the type gets used in another module, the functionality will fall-back to the original behavior.

This just sounds like a scary source of bugs waiting to happen. I think the solution might be to simply dissallow extensions to protocols that are not defined within the same module. I rather lose out on potential functionality to maintain certain guarantees in my program.

Thoughts?

Update August 20th, 2015 @ 7:30am HST

The above explanation of talk() is a bit incorrect; here's the version I meant to copy in:

public func talkOf(animals: [Sheep]) {
    animals.forEach { $0.speak() }
}

The issue with the original talk() function is that the extension will never be used as the type is defined within another module if the base protocol of Animal is used.

Here are my three talk()-like functions I used:

public func talk(animals: [Animal]) {
    print("in-module: talk")
    animals.forEach { $0.speak() }
}

public func talkOf<T : Animal>(animals: [T]) {
    print("in-module: talkOf")
    animals.forEach { $0.speak() }
}

public func sheepTalk(animals: [Sheep]) {
    print("in-module: sheepTalk")
    animals.forEach { $0.speak() }
}


public func talk(animals: [Animal]) {
    print("out-of-module: talk")
    animals.forEach { $0.speak() }
}

public func talkOf<T : Animal>(animals: [T]) {
    print("out-of-module: talkOf")
    animals.forEach { $0.speak() }
}

public func sheepTalk(animals: [Sheep]) {
    print("out-of-module: sheepTalk")
    animals.forEach { $0.speak() }
}

The in-module versions are "my code"; it's the code where the Sheep extension is defined (the extension being public or internal had no effect). The out-of-module code is where the Animal protocol and Sheep type are defined. The thing to note that is that even with-in my own module, where I've defined the extension for Sheep, if I use the base type Animal, I'll not see my extension's behavior:

print("\nin-module: Sheep()")
let s = Sheep()
s.speak()

print("\nin-module: Animal - Sheep()")
let a: Animal = Sheep()
a.speak()

The output is:

in-module: Sheep()
bah!

in-module: Animal - Sheep()
rawr?

In anycase, a simple error or warning when defining extensions on types defined in a different module would alleviate this problem.

Project File: ProtocolDispatch.zip

Guess the Protocol Behavior