Access control for enum case initializers

(Matthew Johnson) #1

Enum cases serve as both factory functions and patterns for use in pattern matching. All cases in an enum are currently visible at the same level as the enum itself for both of these uses.

I have run into a number of times where an enum is the right tool for the job but I did not wish to expose and initializer for some (or all) cases outside a library. There is no good way out of this design problem. If an enum is not used, users of the library lose the ability to pattern match and destructure the value.

Following the parameterized access control syntax we use for properties, I think we should allow the visibility of the case initializer portion of an enum case to be modified.

public enum Foo {
    // this case may only be initialized by `Foo` itself within the declaring file
    private(init) case bar

    // this case may only be initialized within the declaring file
    fileprivate(init) case baz

    // this case may only be initialized within the declaring module
    internal(init) case fizz

    // this case may be initialized anywhere
    // by default cases initializers still have the same visibility as the type itself
    case buzz
}

Note: this pitch does not introduce the ability to hide a case for the purpose of pattern matching. Unparameterized access control on a case would be rejected by the compiler. That may be a viable future direction but I believe it is a much larger change to the language and is not necessary to solve the library design issues caused by the inability to restrict enum value initialization.

14 Likes
(Jordan Rose) #2

Infrequently requested—the Radar for it, rdar://problem/22891351, has no dups, but that could be us Apple folks not consolidating properly—but a reasonable feature in my mind. I like your spelling for it, too.

1 Like
(Ankit Aggarwal) #3

+1 I've always wanted this for SwiftPM's PackageDescription library!

(Alejandro Alonso) #4

I'd be curious to hear what you think when it comes to something like CaseIterable. I imagine this only uses public cases, but what if I really want to iterate over all of my cases inside the declaring enum?

(Matthew Johnson) #5

I think when this is used CaseIterable synthesis would have to be disabled. The semantics break if you don’t include all cases. Manual conformance would still be possible if desired but would be inadvisable given the semantic conflict.

2 Likes
(TJ Usiyan) #6

I never filed a bug for this because I assumed it just wouldn't ever be allowed. I wasn't sure that the asymmetry of being able to switch over something that you couldn't construct would be accepted.

I have wanted the ability to do this since day 1.

one use case is projecting enums for pattern matching from a class (that should be a class because caching or some other reason)

2 Likes
(David Hart) #7

That would definitely be useful!

1 Like
(Adrian Zubarev) #8

As there are bugs open which suggest that we also should allow static members to kind of fake enum cases, would it rather make sense to call it private(call) instead of using init which would be only limitted to enum cases? I know that I probably suggest a bigger feature here, but I think it might be worth pointing out that access_level(call) can limit the call of a type member to its type while the actual member itself is exposed due to other reasons.

enum AnimationDuration {
  case never
  private(call) case value(Double)

  static var oneSecond: AnimationDuration {
    return .value(1)
  }

  private(call) static var indefinitely: AnimationDuration {
    return .value(.nan)
  }
}
(Adrian Zubarev) #9

Can you explain why there is a conflict? I think if you would implement the protocol in the type declaring body even your private(init) would not prevent you from generating the case for the collection. That said, I only see CaseIterable limited in the way where an enum conforms to the protocol. (Kind of similar to the synthetization rules for Hashable/Equatable.) So if you can move the conformance close to the enum, you should be fine.

#10

Would you be able to obtain the private(init) case in this way? If not, which part would be forbidden? (It errors out with "Enum case 'privateCase' is not a member of type 'PatternHijacker'", but I don't think it should. I'll make a bug report later)

enum MyCoolEnum {
    private(init) case privateCase
    case publicCase
}
class PatternHijacker {
    var pattern: MyCoolEnum?
    init() { }
}

func ~=(pattern: MyCoolEnum, value: PatternHijacker) -> Bool {
    value.pattern = pattern
    return true
}

var hijacker = PatternHijacker()
switch hijacker {
case MyCoolEnum.privateCase:
    break
default:
    break
}

let valueThatIShouldntBeAbleToObtain = hijacker.pattern!
1 Like
(Adrian Zubarev) #11

With the current access control rules this will error out and you will need to change it to fileprivate(init). Feel free to correct me if I'm wrong.

#12

Which line will error out? What would be the error?

If the line is case MyCoolEnum.privateCase: then the sentence " Note: this pitch does not introduce the ability to hide a case for the purpose of pattern matching." is kinda misleading, because it hides it for non-compiler-generated pattern match operators

(Adrian Zubarev) #13

That is exactly the line that will error out, because you still need to initialize the enum case to be able to pattern match it. The compiler will likely say something like:

Call to `MyCoolEnum.privateCase` is inaccessible due to `private(init)` protection level

The sentence says, in other words, this pitch does not introduce private cases - or cases that can be hidden from greater access levels. It is however true that some can misinterpret what the sentence really means.

#14

I am confused. Would this be possible?

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

let foo = MyCoolEnum.publicCase
switch foo {
case .privateCase:
    break
case .publicCase:
    break
}

How about this?

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

let foo = MyCoolEnum.publicCase
switch foo {
case MyCoolEnum.privateCase:
    break
case MyCoolEnum.publicCase:
    break
}
(Adrian Zubarev) #15

I'd say none of these are possible until you use fileprivate(init) or internal(init). Here an example with a alternate access_level(call) option.

// file A
struct Test {
  private func foo() {}
  internal private(call) func bar() {
    foo() // fine
  }

  internal fileprivate(call) func baz() {
    bar() // fine
  }
}

// still file A
let test = Test()

// `foo` is not visible by autocompletion at all
// error: 'foo' is inaccessible due to 'private' protection level
test.foo() 

// `bar` is visible by autocompletion
// error: 'bar' is inaccessible due to 'private(call)' protection level
test.bar()

// `baz` is visible by autocompletion
test.baz() // okay as we're in the same file

// file B
test.foo() // same error as above
test.bar() // same error as above

// error: 'baz' is inaccessible due to 'fileprivate(call)' protection level
test.baz() 

This kind of the same for pitched access_level(init). I would prefer access_level(call) as it is would not be exclusive to enum cases, which would be really odd.

#16

What does pattern matching means in swift if not switch?

(Adrian Zubarev) #17

A simple example if the future direction mentioned in the pitch was accepted and implemented.

enum E {
  case a
  case b
  private case c
}

let e = E.c
swtich e {
case .a:
  break
case .b:
  break
case .c: // would error out as `.c` is not visible
  ...
default:
  /* we can end up here because `e` can be the hidden `.c` case */
}

The nearest feature to private(init) is private(set). Pattern matching is not magic. Most of the time we compare for equality of two values there the second value is initialized by the switch statement when comparing each new case. The only case I'm not quite sure about is when you destructure the case and extract the payload from it. (Please correct me if I'm wrong.)

#18

According to you it would also error out if .c is private(init). I don't see the difference.

(Adrian Zubarev) #19

In your original example you had a ~= overload which explicitly requires you to provide an instance of MyCoolEnum that will be stored inside pattern argument. For that to work you still have to initialize that case. In your example you do it using a switch statement, which won't be possible for a case marked as private(init) from a switch that lives outside the any MyCoolEnum (extension) body in the current file. Mark it as fileprivate(init) and it will work.

// Module A
public enum MyEnum {
  case a
  internal(init) case b

  // in the future
  internal case c
}

// in module A
_ = MyEnum.b // okay
_ = MyEnum.c // okay

// in module B
import A
// `MyEnum.b` is exposed and visible to the user but he cannot initialize it
_ = MyEnum.b // error due to `internal(init)` protection level

// `MyEnum.c` is not visible to the module user
_ = MyEnum.c // error `MyEnum` has no member `c`

To be clear, I'm trying to make you understand how I see the current situation. If I'm wrong about anything I said so far, I apologize in advance, so please anyone feel free to correct me.

#20

I'll ask again. Would this be possible? Notice how there is no ~= overload here

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

let foo = MyCoolEnum.publicCase
switch foo {
case .privateCase:
    break
case .publicCase:
    break
}

How about this?

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

let foo = MyCoolEnum.publicCase
switch foo {
case MyCoolEnum.privateCase:
    break
case MyCoolEnum.publicCase:
    break
}

If your answer is still "none of these are possible", then I think that having two attributes that behave exactly the same, just with different messages is dumb.

1 Like