Can a generic class have a non-generic nested type?

Is there a way to do something like the following where Bar is namespaced to Foo, but not generic
(Bar doesn't use G in any way)

class Foo<G> where G:CustomStringConvertible {
    struct Bar {
        var something:Bool
    }
}

var test = Foo.Bar(something:true)

this errors with 'Generic parameter 'G' could not be inferred', but I want to say 'ignore G - you don't need it!'

I can pull Bar out of Foo and make it independent - but the namespacing is useful and meaningful in the case where I'm wanting to use it.

3 Likes

Define Bar elsewhere and use a typealias:

struct Bar_ {
  var something:Bool
};
class Foo<G> where G:CustomStringConvertible { 
  typealias Bar = Bar_
}
var test = Foo.Bar(something: true)

Edit: also note that you shouldn't use CustomStringConvertible as a constraint, all values can be printed using string interpolation.

1 Like

that doesn't work unfortunately; I still get 'Generic parameter 'G' could not be inferred'

re the CustomStringConvertible constraint; I'm not actually using that - it is just the first thing that sprang to mind for a minimal example.

Oh.
I tested it, using an old version of the repl, which must differ sufficiently from the main compiler.
perhaps this will work?

struct Bar_ {}
class Foo<G> {}
// arbitrary constant type
extension Foo where G == Bool { 
  typealias Bar = Bar_
}

I wonder if it would be possible for the compiler to recognize that the nested type doesn't depend on the generic parameter, and therefore treat the nested type as non-generic. That would also mean that you'd have to be permitted to say Foo.Bar without being required to specify G.

2 Likes

struct Bar_ {}
class Foo {}
// arbitrary constant type
extension Foo where G == Bool {
typealias Bar = Bar_
}

No joy with that either I'm afraid. (I'm using Xcode 12.5.1)
Same error (unable to infer G)

It seems that there just isn't such a thing as Foo. only Foo<specify the generic>

Any other likely workarounds?

I'm not thinking of any, sorry. :slightly_frowning_face:

thanks for trying

This would require seeing all method bodies on the class, which might not even be possible if there are extensions that add new methods from outside the module, or in other source files. It's not worth the complexity. If you don't want a nested type to capture generic arguments, it should not be a nested type.

9 Likes

Throw Any in front.

It is a terrible solution, but not as terrible as the language not having the feature. :stuck_out_tongue_closed_eyes:

My prediction is that this will get solved along with nested protocols and their inverse.

public enum AnyOptional {
  public class UnwrapError: Error {
    public init() { }
  }
}
public enum AnyCaseIterable<Case> {
  public enum AllCasesError: Error {
    /// No `AllCases.Index` corresponds to this case.
    case noIndex(Case)
  }
}

@anon9791410 can you elaborate? I'm afraid I'm not following...

If you're not going to make use of the generic placeholder, you can use a dummy conformer.

class Foo<G: CustomStringConvertible> {
  struct Bar where G == Never {
    var something: Bool
  }
}

extension Never: CustomStringConvertible {
  public var description: String { fatalError() }
}

The other thing I was saying was to make a new type with the word Any prepended. It's not nested in Foo.

enum AnyFoo {
  struct Bar {
    var something: Bool
  }
}
2 Likes

i too really want to do this oftentimes, as well as a related use case: nesting a protocol inside another type.

it is not that i care about it being a nested type, it is because of the - at first glance, unrelated - problem where we cannot define more than one top-level type per module, if the module has the same name as the type.

for example, if i have a module named Cromulence, and a type

struct Cromulence<T>

i cannot add a second, non-generic type alongside it, such as:

struct CromulenceOptions

there are really only three possible workarounds for this problem, none of which are satisfactory to me.

  1. we can nest the options type in Cromulence<T> anyway, but we give up on the ability to reuse options between Cromulence<T>.Options and Cromulence<U>.Options.

  2. we can rename the module itself to something like CromulenceModule. but this implies that adding a second top-level type to a module requires a module rename, which can be quite disruptive given how often this situation arises. and if CromulenceOptions later migrates out of CromulenceModule for an unrelated reason, we must once again rename CromulenceModule back to Cromulence for elasticity’s sake.

  3. we can place CromulenceOptions in a separate module that Cromulence depends on and re-exports. but this usually results in a lot of unwanted @inlinable/@frozen proliferation, and forces many APIs to become public that i would rather not become public.

option #1 is obviously an inferior design, and i have gradually come to recognize option #2 as an enormous waste of time and source of unjustified API churn. so i have more or less settled on #3 as my preferred workaround.

but in my opinion the real solution to this problem is not to “fix” option #1 by allowing non-generic nested types, the real solution would simply be to allow eponymous modules to contain more than one top-level type.

2 Likes

I can't explain why, but @anon9791410's example works :slight_smile:

class Foo <G> {
  struct Bar where G == Never {
    var something: Bool
  }
}
@main
struct T1 {
    static func main () {
        let b = Foo.Bar (something: false)
        print (b)
    }
}
Bar(something: false)

1 Like

We need an @ignoreOuterGenerics.

Constraining a nested type to one particular outer type effectively makes the type non-generic. However, the outer type is still generic—you can just use it implicitly because the compiler knows there's only one option for the nested type. You can use any type for the generic removal.

enum Food<🥞> { }
typealias đź«… = ([Void???], Set<Never>, String?)
extension Food where 🥞 == 🫅 {
  struct Bart { }
}
Food.Bart() is Food<đź«…>.Bart // true
4 Likes

i am definitely going to start doing this more. one issue i’ve observed is documentation tooling will vacuum up the sameType constraint and parrot it everywhere on all of Bart’s APIs. but this is a surmountable problem that should be addressed with better tooling heuristics.

thanks Jessy for the new technique!

1 Like

This is pretty cool, I wish it worked for extensions of ObjC classes. I even tried applying the @nonobjc attribute, but no luck so far.

Ever wanted to have a static var in a generic type? With the above Jessy's trick it's now possible:

struct Foo<T> {
    // static var foo = 0 // 🛑 Static stored properties not supported in generic types
    
    struct Bar where T == Never {
        static var staticVar: Int = 0
    }
    static var staticVar: Int {
        get { Foo<Never>.Bar.staticVar }
        set { Foo<Never>.Bar.staticVar = newValue }
    }
}

Foo<Int>.staticVar = 42
print(Foo<Float>.staticVar) // 42
4 Likes

That's possible without a nested type.

struct Foo<T> {
  static var staticVar: Int {
    get { Foo<_>.staticVar }
    set { Foo<_>.staticVar = newValue }
  }
}

extension Foo where T == Never {
  static var staticVar = 0
}

Foo<String>.staticVar = 42
Foo<Bool>.staticVar // 42
Foo.staticVar // 42
4 Likes