Access control for enum case initializers

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

That is hard to wrap the head around, can you define a pattern? Does not a pattern in a usual case require an equality test? (I know there are other patterns, but in the above example I could imagine there is an internal ~= that falls back to ==.)

Since you limited case a with private(init), the static case initializer would not leak out to other files, which in my brain will prevent you from writing case .a: in the switch statement in another file.

This was a perfect example why I think pattern matching will error out in your example as well.

If that's not true, then I guess I don't understand pattern matching after 5 years working in Swift.

2 Likes

Not in general, no. The way enums are matched is different from the way matches using ~=works.

No. The entire point of the parameterized access control is to not prevent this. That would make values of the enum less ergonomic to work with for users of a library. But just because a library wants to expose pattern matches on its values does not necessarily mean it wants those values to be initializable by users of the library. Imagine if a public struct could only have public initializers. That would be nuts. But that's exactly the situation we have right now for enums.

If you're talking about an example that uses ~= to match a value of a different type then yes, that will produce a compiler error under my pitch. That is because the case initializer is required to construct a value. The case pattern is not being used at all in this example. It just looks like it is because of how ~= patterns and case initializers work.

:exploding_head:

Okay then I need to apologize for derailing this thread so much with false information. Sorry @cukr if I confused you and other readers. I guess I need to do more homework and dig up more in depth documentation about pattern matching in Swift.

I at first would think that as well, but it's easy to find a counter-example:

enum Foo {
  case foo(Int)
}
func checkFoo(_ foo: Foo) {
  switch foo {
  case .foo: print("foo")
  }
}

Is the case initialized here to do the pattern match? :slight_smile:

1 Like
Terms of Service

Privacy Policy

Cookie Policy