Access control for enum case initializers

Can you explain what you mean by 'two attributes' here?


Also I see your examples equivalent to:

enum MyCoolEnum {
    private(init) case privateCase
    case publicCase
}

let foo = MyCoolEnum.publicCase
if foo == .privateCase { // this will raise an error
} else if foo == .publicCase {}

internal(init) case x (or internal(call) )and the future direction internal case x
You haven't shown me any example where these two will differ

Well this was never on the table. It's not fair calling this 'dumb' as the problem already exists today with properties that can be either just internal or internal(set).

I can show you an example where the behavior of private and private(set) is different

struct Foo {
    private var a = 123
    private(set) var b = 123
}
var foo = Foo()
print(foo.a) // errors out
print(foo.b) // doesn't error out

Can you show me an example where internal(init) case x and the future direction internal case x behave differently?

Sorry, I automatically read whatever(set) as private(set)

I think that adding internal and internal(set) as standalone features would be dumb. But internal and public internal(set) make sense.

1 Like

Sure.

// Module X
public enum MyCoolEnum {
  case a
  internal(init) case b
  internal case c

  public static var indirectB: MyCoolEnum {
    return .b
  }

  public static var indirectC: MyCoolEnum {
    return .c
  }
}  

// Module Y
import X
// ASTPrinter will give you this.
// public MyCoolEnum {
//   case a
//   internal(init) case b
//
//   public static var indirectB: MyCoolEnum { get }
//   public static var indirectC: MyCoolEnum { get }
// }

// `case c` is not exposed at all, while `case b` is but is restricted
// so that the user of module X cannot initialize it directly.
1 Like

Thank you :) I interpreted the pitch as less restrictive.

1 Like

I wouldn't have been in favour for this before, but after SE-0192 I am. Some prerequisites I feel should be there:

  • frozen enums cannot specify access control for cases different than the enum itself
  • when switching from a context where all cases are accessible then switch behaves as for normal enums requiring covering all cases
  • when switching from a context where not all cases are accessible then switch behaves as with non-frozen enums and @unknown default is needed

If these are satisfied I'd be happy with the feature :slight_smile:

2 Likes

Wouldn't that mean that if you had previously a non-frozen enum with private cases (on apple platforms) that would become frozen you would need to expose all the private cases? Since the frozen attribute is not yet allowed for library developers to be used I'm not sure what to think about this situation.

@jrose do you have a clearer vision of this?

Is that not already the case? If they are frozen then you don't need @unknown default:, which means you have all the cases available to switch on

It's a semantic conflict. private(init) prevents code outside the declaring file from creating an instance. But that is defeated if the compiler makes a value of that case available via allCases. If the compiler does synthesize CaseIterable for an enum with a case initializer less visible than the type at minimum there should be a diagnostic about this. private(init) would be pretty pointless. It would even be possible to write a static computed property that looks just like the case initializer in an extension.

I don't fully understand what you are asking here, but it would absolutely be possible for values of the case to be vended by code that can see the case initializer to code that cannot see the case initializer. The pattern is still visible for matching, etc. All this does is hide the case initialize so a value of the case cannot be created by code that cannot see the case initializer.

I'm not sure I follow this. From my point of view it's exactly what private(init) would cause.

enum E {
  private(init) case a
  
  static var indirectA: E {
    return .a
  }
}

This is valid so why can't you still have CaseIterable directly sitting near E?

Pattern matching also includes if case and guard case.

1 Like

Yes, the case is visible for the purpose of pattern matching. That is why I am pitching the parameterized access control instead of applying access control to the case across the board. Access control for the purpose of pattern matching is much different in scope and semantics.

This would indeed raise an error. In order to compare equality you need to be able to construct an instance using the case initializer, which is not visible to the code in the example.

I hope I have answered that question in this post. Code that cannot see the case initializer is able to pattern match but cannot use the case initializer to make a value. The future direction would disallow both pattern matching and value initialization. This means that exhaustiveness analysis would need to consider access control. That is a much larger change than what I am pitching.

As noted above, this pitch does not change the behavior of switch or pattern matching at all. That is why the access modifier must be parameterized (i.e. private(init) instead of just private). The future direction of a non-parameterized access modifier would require code that can't see the pattern for every case to use @unknown default when switching.

3 Likes

CaseIterable would give users a way to get a guaranteed value of all cases. Consider this:

enum E: CaseIterable {
  private(init) case a
  case b
}

// some other file:
extension E {
   static var a: E {
       for value in allCases {
           switch value {
           case .a: return value
           default: continue
           }
       }
       fatalError()
   }
}

By providing a semantically valid conformance to CaseIterable you have allowed the case initializer to be written manually in an extension anywhere that can see the enum. private(init) is not getting you anything. I think the compiler should at least warn about this and possibly even refuse to synthesize CaseIterable. I don't have a strong opinion about which one.

When you say private(init) will hide the initializer, what exactly do you mean? Do you want to hide the static-like member of the enum type for that specific enum? If so, this would of course mean that the semantics of access_level(init) are slightly different than access_level(call) would have, as the latter would not hide the (static) type member but prevent calls from certain access levels, which is fairly similar but not entirely the same thing.

Wait, why is your example valid code? How can you pattern match against a if you cannot construct a new value as it's already restricted by private(init). case .a: return value should error out.

1 Like

Of course it should not be an error to write that code yourself. But I’m not sure the compiler should write it for you.

Yes, it would hide the static property or factory method that the language provides for enum cases. I don’t understand the exact semantics of your call idea but it does sound different from what I am talking about here.

Pattern matching an enum does not construct a value, it matches the pattern of a case.

1 Like