When generic parameter could not be inferred

Hi,

I'm trying to model the relatively easy idea of returning an instance of a type that conforms to a given protocol.

Here's the thing: Let's say there is a protocol Theme and I model two types of themes:
Theme1: Theme and Theme2: Theme.
Now, I have an instance of instance of type either Theme1 or Theme2 that is a value of current_theme.

let current_theme = Theme1()

I want to build a generic function that can return current_theme variable, something like this:

func currentTheme<T: Theme>() -> T {
    return current_theme
}

this is just a draft because it doesn't seem to work as I expect. Either I have to downcast to T, or T cannot be interred.

For a full example, please check this gist:

the most surprising for me is this form (it actually works):

func currentTheme<T>() -> T {
	return current_theme as! T
}

My question is whether it is possible to build a generic function with constraints (without downcasting) where concrete type can be inferred from the returning value?

2 Likes

Disclaimer: I may have no idea what I'm talking about.

I think the problem isn't the type inference. It's the fact that current_theme can be one of several concrete types at runtime, and the compiler can't tell which one it actually will be at any point in time. Thus, you have to cast to the concrete type at runtime (using as! or as?), as you have done. Note that the compiler is able to determine what T you intend when you call currentTheme(), if there's enough context at the call site.

The problem is that your function is not really generic.

A generic type is actually a declaration of a family of related types: specifically, one type for each possible type satisfying the generic constraints. In your example, this declaration:

func currentTheme<T: Theme>() -> T {
    return current_theme
}

can be thought of as declaring many functions, one for each T that conforms to Theme. The compiler will select the appropriate specific type based on the call site. For example, I (as the caller) am allowed to write this:

let x: Theme1 = currentTheme()

However, the implementation does not match that. That's because for all but one of the infinite possible conforming types this function is invalid and will not compile. If it could compile my runtime example would be gibberish and it shouldn't compile, but the rules of generics allow me to write exactly that code.

What you're trying to write, I think, is a function where the return type will definitely conform to Theme, but you don't want to write down what specific concrete implementation it will be at the function definition site. This is presumably because you have some non-local knowledge about what the concrete type will be, likely in a different file covered by some compile-time guards.

In this case, you should simply declare a typealias and use that as the return type. You can then keep the typealias declared near the underlying variable you're returning.

Another possibility is that you are changing the current theme at runtime. In this case, you need to return the Theme existential from this function.

5 Likes

It may be that I fixated in the problem too much in the evening, and I'm lookingnfor existentials, and now all I want is to understand

I'm not sure if I follow the idea. What is a typealias value here?

In this case, you need to return the Theme existential from this function.

I guess this is something you think about: https://gist.github.com/krzyzanowskim/b26271db5efa63aebb0e68b682445d52

func currenTheme() -> AnyTheme<String> {
  return AnyThemeBox(current_theme1)
}

since Kind is an arbitrary type, this function can't return any possible Theme, especially Theme that has Kind = Int. I kinda failed to make it generic enough, so it could anything that conforms to Theme.

Extending your gist slightly below:

protocol Theme {
	associatedtype Kind
}

struct Theme1: Theme {
	typealias Kind = String
	let kind: Kind
}

let theme1 = Theme1(kind: "abc")

struct Theme2: Theme {
	typealias Kind = Int
	let kind: Kind
}

let theme2 = Theme2(kind: 999) 

#if SOME_DEFINE
typealias TheTheme = Theme1
let myTheme = theme1
#else
typealias TheTheme = Theme2
let myTheme = theme2
#end

func current() -> TheTheme {
	return myTheme
}

Yeah, so you do definitely need to perform type-erasure here. Your type erasure can be a lot more simple though:

protocol Theme {
    associatedtype Kind
    var value: Kind { get }
}

struct ThemeS: Theme {
    typealias Kind = String
    let value: Kind
}

struct ThemeI: Theme {
    typealias Kind = Int
    let value: Kind
}

class AnyTheme<WrappedKind>: Theme {
    typealias Kind = WrappedKind

    let value: Kind

    init<T: Theme>(_ wrapped: T) where T.Kind == WrappedKind {
        self.value = wrapped.value
    }
}

let current_theme1 = ThemeS(value: "Hello")

func currentTheme() -> AnyTheme<String> {
    return AnyTheme(current_theme1)
}

let t = currentTheme()
print(t)

tried that. Notice, that it can't return 'ThemeI' still. AnyTheme could store theme as Any, then it's not very easy to use. What I'd need is id<Theme> from ObjC "basically"

What you need is generalised existentials, really, because you're asking to work with a protocol irregardless of its associated types. That's just not possible in Swift today.

I think the question has to be: what are you actually trying to do? The Theme protocol would be useless, as it has only one property, defined by an associated type that you do not know at compile time. I assume your actual use-case is a bit more complex than this. So if we want to fix it, I think we'll need to go into the specifics.

yes.

I think the question has to be: what are you actually trying to do?

At this point, it's just an academic discussion.

It's not necessary to model Theme like this:

protocol Theme {
	associatedtype PanelTheme
	associatedtype ToolbarTheme
	var comp1: PanelTheme { get }
	var comp2: ToolbarTheme { get }
}

it can be modeled with more protocols:

protocol PanelTheme {}
protocol ToolbarTheme {}

protocol Theme {
	var comp1: PanelTheme { get }
	var comp2: ToolbarTheme { get }
}

thank you for discussion @lukasa

Terms of Service

Privacy Policy

Cookie Policy