Search code examples
swiftstructprotocolspublicinternals

Difficulty Using Internal or Private Types in Swift Protocol Conformance


I'm encountering issues while trying to use internal or private types in Swift protocol conformance. Here's a simplified version of my code:


internal protocol RATIONAL {
    associatedtype rational
    
    static func makeFrac(_ x: Int, _ y: Int) throws -> rational
    static func add(_ r1: rational, _ r2: rational) -> rational
    static func toString(_ r: rational) -> String
}


public struct Rational1: RATIONAL {
    
    // error: Type alias cannot be declared public because its underlying type uses an internal type
    public typealias rational = Rational
    
    internal enum Rational {
        case Whole(Int)
        case Frac(Int, Int)
    }
    
    // error: Method cannot be declared public because its result uses an internal type
    public static func makeFrac(_ x: Int, _ y: Int) throws -> Rational {
        <#code#>
    }
    
    // error: ...
    public static func add(_ r1: Rational, _ r2: Rational) -> Rational {
        <#code#>
    }
    
    // error: ...
    public static func toString(_ r: Rational) -> String {
        <#code#>
    }

    fileprivate func foo(_ x: Int) {
        <#code#>
    }
    
    private func boo(_ x: Int) -> Int {
        <#code#>
    }
    
    
}

I want the Rational enum to be kept only within the module or even to be private, but the rest of the methods that return or use a Rational to be public. If I change the enum declaration to public, everything works fine. How can I achieve this while keeping the enum internal or private?

At this point protocol could be whatever, if that is needed for this code to work. Thanks

BTW, new to this, so any advice is welcome.


Solution

  • You cannot hide the types you use to conform to a protocol. This is mostly due to a limitation in some types that will possibly be lifted in the future. There is no way in Swift today to express "the opaque type that me.f() returns is the same opaque type that me.g() accepts." If that existed, you could do this entirely with opaque types, but you doesn't and you can't.

    The most important document for this discussion is Improving the UI of Generics. If you're interested in these topics, you should start there to get your footing in what is "because it's not implemented" vs "because it's impossible." You will also want to read the urtext on the topic, the Generics Manifesto.

    There is second limitation in Swift that blocks your specific attempt to implement this, and that is that individual cases of an enum type cannot be more restrictive than the entire type. Again, this could be possible, but it is not. So you cannot expose the return type as an enum without exposing its internal workings.

    This is one of many cases where Swift favors structs over enums, even though they should be duals. With structs, this is straightforward, if requiring one layer of indirection.

    I will use the following protocol which is a bit more Swift-like in its naming (though this is a deeply un-Swifty approach, as I'll discuss later).

    I've made this protocol public because if it's internal the rest of the question is irrelevant. There is absolutely no problem with a type conforming in any way it likes to a protocol an outside caller cannot see.

    public protocol RationalAlgebra {
        associatedtype RationalValue
    
        static func makeFrac(_ x: Int, _ y: Int) throws -> RationalValue
        static func add(_ r1: RationalValue, _ r2: RationalValue) -> RationalValue
        static func toString(_ r: RationalValue) -> String
    }
    

    With this, you can create your own "opaque type" using a struct that exposes nothing:

    public struct Rational: RationalAlgebra {
    
        // Rational.RationalValue is public, but exposes no internal details.
        // It exists only as "the RationalAlgebra type that Rational returns."
        public struct RationalValue {
            private enum Value {
                case Whole(Int)
                case Frac(Int, Int)
            }
            private let value: Value
        }
    
        private let value: RationalValue
    
        public static func makeFrac(_ x: Int, _ y: Int) throws -> RationalValue { fatalError() }
    
        public static func add(_ r1: RationalValue, _ r2: RationalValue) -> RationalValue { fatalError() }
    
        public static func toString(_ r: RationalValue) -> String { fatalError() }
    
        fileprivate func foo(_ x: Int) { fatalError() }
    
        private func boo(_ x: Int) -> Int { fatalError() }
    }
    

    All of this is deeply "un-Swifty" for many reasons. One is that the protocol is doing no work. Protocols are not meaningless "abstraction layers" like the pimpl pattern in C++ or the Impl pattern in Java. Protocols exist to allow algorithms to be written over heterogeneous types. If your protocol and your conforming type have exactly the same shape, then you're probably just playing with types rather than using them as a real tool. (Fun, yes, but not the point.)

    This is also extremely un-Swifty in that it a type that relies entirely on static methods. See BinaryInteger as a starting point for how you should think about "Rational."

    But even so, your underlying goal of hiding the details (if not the name) of a returned type is completely possible. It is just not easily accomplished with enums because Swift likes structs better.