ctholho
ctholho

Reputation: 1051

How to pass a class as a function argument?

I want to pass classes that conform to a protocol as function parameters. Concretely: I have a breadth/depth first search algorithm that builds on (respectively) a Queue or Stack data structure. I would love to have a single function that accepts either my Queue or Stack class as an argument. Potentially wrapped in an enum that hides the Queue and Stack objects for a more friendly breadth first or depth first.

I tried to use if or switch variants with simple Strings as either "depth" or "breadth" but variables declared in a closure are invisible to the outer scope and then I would need to repeat almost the whole function body for both cases.

Here is a minimal, reproducible example. The actual example is more complicated but I get the same error and believe it has the same underlying problem.

protocol Container {
    associatedtype T
    var container: [T] { get }
    func push(_ thing: T)
}

public class fakeQueue<T>: Container {
    internal var container: [T] = [T]()
    public func push(_ thing: T) { container.append(thing) }
}

public class fakeStack<T>: Container {
    internal var container: [T] = [T]()
    public func push(_ thing: T) { container.insert(thing, at: 0) }
}

func addValue<Algorithm: Container>(someValue: String, algorithm: Algorithm) {
    // next line raises: Cannot specialize a non-generic definition
    let importantVariable = algorithm<String>()
    importantVariable.push("Something important")
}

// The call raises: Argument type 'fakeQueue.Type' does not conform to expected type 'Container'
addValue(someValue: "_", algorithm: fakeQueue) // or fakeStack

I understand that I can't use algorithm<String>(). In this version it raises the Error: Cannot specialize a non-generic definition. But when I use fakeStack<String>() or fakeQueue<String>() and simply avoid specifying an algorithm: fakeQueue it works as expected.

I simply want to avoid having to make two functions.

Upvotes: 0

Views: 134

Answers (1)

matt
matt

Reputation: 535119

Some initial cautions

Passing types in Swift is nearly always a Bad Smell. Swift does not treat metatypes like types.

Moreover, your addValue can never be written as specified. You cannot pass a Container into it or out of it, because Container is a generic protocol, which cannot be used as a type (e.g. when specifying a function parameter or function return type).

You can make generic classes that conform to a generic protocol, thus guaranteeing that you can push to an instance of any such class. But you cannot further unify them under some single head, because they are both generics and might be resolved differently.

Having said all that, we can probably make a pretty good approach to your idea, as I shall now demonstrate.

Revising your protocol and classes

Thinking about the general sort of thing you're trying to do, I suspect you're after an architecture somewhat like this:

protocol Pushable : class {
    associatedtype T
    init(_ t:T)
    var contents : [T] {get set}
    func push(_ t:T)
}

final class Stack<TT> : Pushable {
    init(_ t:TT) { self.contents = [t]}
    var contents = [TT]()
    func push(_ t:TT) {
        self.contents.append(t)
    }
}

final class Queue<TT> : Pushable {
    init(_ t:TT) { self.contents = [t]}
    var contents = [TT]()
    func push(_ t:TT) {
        self.contents.insert(t, at:0)
    }
}

I've called your Container by the name Pushable just because the ability to call push is all we have in common right now. You'll notice that I've added an init to the Pushable protocol; that's so that we have a way of resolving a Pushable generic. Whatever value we initialize a Pushable with, its type becomes its generic parameterized type; at the moment, that instance goes into the contents and further instances can be pushed, though I'll show later how to change that.

So now we can say stuff like this:

let stack = Stack("howdy")
stack.push("farewell")
let queue = Queue(1)
queue.push(2)

The joy of where clauses

Okay, now let's return to your desire to push an arbitrary value to an arbitrary Pushable. The way to express that is to use Pushable, not as a passed type or a return type, but as a constraint on a generic. That is something we are allowed to use a generic protocol for:

func push<TTT,P>(_ what:TTT, to pushable: P) 
    where P:Pushable, P.T == TTT  {
        pushable.push(what)
}

A factory method and passing a metatype

But you'll no doubt notice that I still haven't supplied a function with the ability to make a Queue-or-Stack. To do that, we would indeed need to pass a metatype. Aha, but I've given Pushable an init requirement! So now we can do it:

func createStackOrQueue<TTT,P>(_ what:TTT, type pushableType: P.Type) -> P
    where P:Pushable, P.T == TTT {
       return P.init(what)
}

let stack = createStackOrQueue("howdy", type:Stack.self)

That isn't identically what you were trying to do, but perhaps it's close enough to get you going.

Factory method, passing only metatypes

If you really insist on passing metatypes around, let's change init so that it too takes a metatype:

protocol Pushable : class {
    associatedtype T
    init(_ t:T.Type)
    var contents : [T] {get set}
    func push(_ t:T)
}

final class Stack<TT> : Pushable {
    init(_ t:TT.Type) { self.contents = [TT]()}
    var contents = [TT]()
    func push(_ t:TT) {
        self.contents.append(t)
    }
}

final class Queue<TT> : Pushable {
    init(_ t:TT.Type) { self.contents = [TT]()}
    var contents = [TT]()
    func push(_ t:TT) {
        self.contents.insert(t, at:0)
    }
}

Now we can write a generic factory function very close to what you were originally after, where both the Pushable (Stack or Queue) and the content type are expressed as metatypes:

func createPushable<TTT,P>(_ whatElementType:TTT.Type, type pushableType: P.Type) -> P 
    where P:Pushable, P.T == TTT {
        return P.init(TTT.self)
}

I can't say I approve of that sort of thing, but at least you can see how it's done.

Something very close to your original idea

And now I think we can make something very close to your original conception, where we say whether we want a stack or a queue along with something to push onto it! Ready?

func createPushable<TTT,P>(type pushableType: P.Type, andPush element:TTT) -> P
    where P:Pushable, P.T == TTT {
        let result = P.init(type(of:element).self)
        result.push(element)
        return result
}

And here's how to call it:

let stack = createPushable(type:Stack.self, andPush:"howdy")

Upvotes: 3

Related Questions