Instance-level polymorphism

I've seen some similar ideas discussed on the forums (notably Polymorphic methods in enums) which are focused around the idea of improving the experience of attaching case-specific behavior to enums without having a totally fragmented implementation. I encountered this issue yet again yesterday and wanted to solicit feedback on some thoughts I had about potential solutions.

Motivation

It is a common pattern to allow a type to take on one of a limited set of values defined ahead of time by the type implementer. In the simplest case, an enum is well suited to model this behavior. To borrow the example from the linked thread:

enum TrafficLight {
    case red
    case yellow
    case green
}

Since enums allow the specification of methods and computed variables, we can easily add simple behavior to this enum that depends on the current case:

enum TrafficLight {
    case red, yellow, green

    var canAdvance: Bool {
        switch self {
        case .green: 
            return true
        default:
            return false
        }
    }
}

However, this approach doesn't scale super well. Adding a flashingRed or flashingYellow state would require us to add additional case statements to every method and computed variable, and if any of them have default statements then the compiler will not catch us if we forget to edit one of the methods. This sort of construct also suffers from readability—there's no way for me as a client of the TrafficLight type to see all of the behavior associated with a specific case (perhaps we added color, luminosity, displayDuration computed variables as well...). Instead, I have to visit each member and work out where exactly the case in question falls in the switch statement.

Existing Solutions

Private init structs

One solution that exists today is to define TrafficLight as a struct with a private initializer:

struct TrafficLight {
    let canAdvance: Bool
    let color: UIColor
    let luminosity: Double

    private init(canAdvance: Bool, color: UIColor, luminosity: Double) { ... }

    static let red = TrafficLight(canAdvance: false, color: .red, luminosity: 0.9)
    static let yellow = TrafficLight(canAdvance: false, color: .yellow, luminosity: 10)
    static let green = TrafficLight(canAdvance: true, color: .green, luminosity: 0.75)
}

With a suitably verbose implementation, we can even create methods on TrafficLight that can reference "self":

struct TrafficLight {
    ...
    private let _allowsVehicleThrough: (TrafficLight, Vehicle) -> Bool = { _self, vehicle in 
        return _self.canAdvance
    }

    func allowsVehicleThrough(_ vehicle: Vehicle) -> Bool {
        return self._allowsVehicleThrough(self, vehicle)
    }

    static let red = TrafficLight(
        canAdvance: false,
        color: .red,
        luminosity: 0.9,
        _allowsVehicleThrough: { _, vehicle in
            return vehicle.isEmergencyVehicle
        }
    )
    ...
}

A struct can also leverage stored properties, so that each traffic light could have an associated intersectionId (and static let red = ... would become static func red(withIntersectionId: Int) -> TrafficLight), which on an enum would necessitate adding an associated value to each individual case.

This solution, however, results in a large amount of boilerplate in order to support the same functionality as the enum based approach, and requires difficult-to-name private members that make the code difficult to understand, so the readability benefits of specifying instance behavior inline are questionable.

Protocols

When modeling more complex behavior like this, it might make sense to model a TrafficLight as a protocol, which guarantees accessors for canAdvance, color, etc. Then, implementations would be declared as struct RedLight: TrafficLight, and appropriate implementations can be provided.

This solution falls short for a few different reasons. It provides no way for the author of TrafficLight to enforce the fact that TrafficLight should only be conformed to by a specific set of types (see Constrain protocol usage outside the module). Furthermore, it introduces a conflation of instances and types. Under protocol TrafficLight, var x = RedLight() and var y = RedLight() aren't meaningfully different but could exhibit different behavior, counter to the intention of the author of TrafficLight. Some of these problems could be mitigated by marking the requirements of TrafficLight as static, and then accepting arguments of type TrafficLight.Type everywhere, but this introduces further issues that arise when dealing with metatypes (notably, lack of protocol conformance).

Proposed Solutions

Lower friction for the "switch on self" pattern

One solution discussed in the thread linked at the beginning of this post is to introduce syntax which would act as an implicit switch over some portion of a method on an enum (either self or the arguments). Under this proposal, we might write:

var canAdvance: Bool {
case .green:
    return true
default:
    return false
}

This reduces some boilerplate, but ultimately does not solve many of the issues with the switch-based solution that exists today. The implementation for a specific case is still split apart the entire type in ways that may be non-trivial to verify, and adding a new case requires touching every existing method.

Explicit instance-level polymorphism

For structs

We could also add an explicit syntax for supporting instance-level polymorphism, which allows a type to specify inline that instances may (or must) define implementations for methods as they see fit. This would essentially act as sugar for the struct based approach mentioned above, with a straw man syntax as something like:

struct TrafficLight {
    var canAdvance: Bool
    @instanceFunc func allowsVehicleThrough(_ vehicle: Vehicle) -> Bool {
        // default implementation
    }

    private init(canAdvance: Bool, func allowsVehicleThrough: @instanceFunc (Vehicle) -> Bool) {
        self.canAdvance = canAdvance
        self.allowsVehicleThrough = allowsVehicleThrough
    }

    static let red = TrafficLight(
        canAdvance: false,
        allowsVehicleThrough: { vehicle in 
            // 'self' is available here since TrafficLight accepts an 
            // @instanceFunc for this parameter, refers to this specific
            // instance of TrafficLight.
            return self.canAdvance || vehicle.isEmergencyVehicle
        }
    )
}

For enums

Also proposed in the thread linked at the top is some sort of syntax for separating out method implementations by case. This usually looked something like:

enum TrafficLight {
    case red, yellow

    // default implementation
    var canAdvance: Bool {
        return false
    }

    case green

    var canAdvance: Bool where self == .green {
        return true
    }
}

or

enum TrafficLight {
    var canAdvance: Bool // implementation provided by cases

    case red {
        var canAdvance: Bool { return false }
    }

    case yellow {
        var canAdvance: Bool { return false }
    }

    case green {
        var canAdvance: Bool { return true }
    }
}

This solution, like the init-based one, successfully separates implementation by instance, and in the first case it reuses existing where syntax in a way that is relatively apparent what it means.

"Closed" protocols

Raised in the link in the Protocols section above, we could allow users to specify a closed protocol:

// module Traffic
closed protocol TrafficLight { ... }

// module MyApp
struct PurpleLight: TrafficLight { // error: 'PurpleLight' cannot conform to 'TrafficLight' because 'TrafficLight' is closed.
    ...
}

This would allow the compiler to enforce the restriction of "only these specific types may conform to this protocol." This would not enable some features that would be possible with instance-level poly morphism (e.g. it may not be possible to have TrafficLight: Hashable, depending on use-case).

Other alternatives...?

The existing and proposed solutions are just what came to mind over the course of the couple days. If there are better existing tools to model this type of behavior, or if anyone else has thought of ways that Swift could provide better support for this pattern, I'd love to hear!

Suggested by @Paul_Cantrell, adapting Java syntax:

Something like Kotlin's sealed class, via @GetSwifty:

2 Likes

Java enums have both this (transposed to Swift syntax):

struct TrafficLight {
    case red(canAdvance: false)
    case yellow(canAdvance: false)
    case green(canAdvance: true)

    let canAdvance: Bool
}

…and this:

They’re nice features, and I miss them in Swift.

1 Like

That's cool, I like the idea of adding cases to structs. It makes the intention clearer that the struct is meant to take on one of a limited set of values, rather than searching and searching for an init and coming up empty handed.

1 Like

Making this easier would definitely be great!

This reminds me a lot of one way kotlin's sealed class can be used.

sealed class StopLight(val canAdvance: Boolean) {
    class Red: StopLight(canAdvance: false)
    class Yellow: StopLight(canAdvance: false)
    class Green: StopLight(canAdvance: true)
}
val myLight = StopLight.Red
val canAdvance = myLight.canAdvance

I think my preferred implementation would be a combination of your proposed enum implementations.

enum TrafficLight {
    // Default implementation
    var canAdvance: Bool { false }

    case red
    case yellow
    case green {
        // Overriding the implementation
        var canAdvance: Bool { true }
    }
}

I'm not sure how enums are implemented, but I'm guessing this should be possible just with code generation. e.g. it could translate the above into the original enum

enum TrafficLight {
    case red, yellow, green

    var canAdvance: Bool {
        switch self {
        case .green: 
            return true
        default:
            return false
        }
    }
}
2 Likes

I have long wanted closed protocols with support for exhaustive switch. They open up design space in ways other purely sugar features don't.

I also think a general version of implicit switch sugar (not just for enums and not just over self) would be an interesting feature. But if I had to choose only one it would be closed protocols.

8 Likes

What do you imagine we would switch over, and what would constitute exhaustiveness? Would it be:

let light = RedLight()

switch light {
case is RedLight: 
    print("red") 
case is GreenLight:
    print("green")
case is YellowLight:
    print("yellow")
}

or

let light = RedLight.self

switch light {
case RedLight.self:
    print("red")
case GreenLight.self:
    print("green")
case YellowLight.self:
    print("yellow")
}

?

I don't know that I'm totally sold on closed protocols as a replacement for polymorphism on enum-like objects since it still seems to introduce confusion between type and instance identity.

I think it would work exactly the same as it does today:

let light = RedLight()
switch light {
case let red as RedLight: 
    // do something knowing you have a red light
case let green as GreenLight: 
    // do something knowing you have a green light
case let yellow as YellowLight: 
    // do something knowing you have a yellow light
}

or:

let light = RedLight.self
switch light {
case is RedLight: 
    print("red") 
case is GreenLight:
    print("green")
case is YellowLight:
    print("yellow")
}

The difference from today is that currently a default clause is required.

To be honest, I'm not that interested in polymorphism-like syntax for enums. If we're going to have polymorphism over a fixed set of cases I'd like to open up the design space with closed protocols. This makes more sense to me anyway as polymorphism basically implies multiple types. With enum-based polymorphism you end up with something along the lines of inaccessible "case types" where the enum type itself is an existential anyway. If we're going to go there we should make it explicit and open up the design space.

As I also said, I think the appropriate sugar to make enum methods more convenient to write is sugar for a general and implicit top-level switch.

4 Likes

This. We already have a powerful construct for defining an Enumerated Type, it's called enum :wink:. It seems unnecessary to try and shoehorn enumeration-style-functionality into other Types when we can leverage the already existing work done for enum. In one way enums already have a form of polymorphism with associated values.

Regardless, the design pattern of having computed properties that switch over enum cases is something I do regularly and I've seen all over the place. Some syntactic sugar to make that pattern easier to write can only be a good thing.

1 Like

You get real, tangible benefit from this, though. Since the enum type is only a pseudo-existential, the limitations of Self/associatedtype constraints don’t apply, and you can freely conform to protocols so that you can have [EnumType: Int], for example. Some of this would be solved by to-do items from the Generics Manifesto, but I think there would still be benefits to having an existential-like object which is a concrete type as far as the type system is concerned.

FWIW, I still like the idea of closed protocols in general, but I think there’s room in the design space for closed protocols and polymorphic “enums,” whatever form they take.

1 Like

I was about to post about multiple dispatch, and your idea above sounds like the next level: predicate dispatch.

1 Like

Oh, neat! That indeed sounds like the correct name for some of the ideas proposed here :slight_smile:

(Also, most of the solutions in this post are lifted from the linked threads, the only part I hadn’t seen before making this post was the @instanceFunc version for instance polymorphism on structs).

Every single one of my enum methods and computed properties starts with switch self, so I would like an implicit switch feature of some kind. It's pretty hard to beat switch self, though, so it would have to be very lightweight to be valuable, as in your example. I don't personally care about the other aspects, like regrouping the computed property values under the individual cases. These alternatives don't seem as general or useful as an implicit switch to me, and seem to require too much fiddly new syntax and exceptional behaviour to be worthwhile.

3 Likes

If tuples could conform to RawRepresentable, it might be nice to do some like this:

typealias TrafficLightType = (canAdvance: Bool, color: UIColor)
enum TrafficLight : TrafficLightType {
   case red = (true, .red)
   case yellow = (false, .yellow)
   case green = (true, .green)

   var canAdvance: Bool { rawValue.canAdvance }
}

It's similar to the java syntax but might fit in better with the existing compiler?

Allowing arbitrary loads as raw values for an enum is an interesting approach to this issue. I don't love that it requires you to split the full implementation of the type into two separate types for the valid cases and the actual payload, or that you would have to define boilerplate properties/methods for every member of the payload to avoid using rawValue everywhere, but I agree this fits into the existing compiler machinery most closely right now in terms of not really requiring any new syntax. It could apply to nominal types as well, so that this could be done as something like:

enum TrafficLight: TrafficLight.Payload {
    struct Payload {
        let canAdvance: Bool
        let color: UIColor

        fileprivate init(_ canAdvance: Bool, _ color: UIColor) { ... }
    }

    case red = .init(false, .red)
    case yellow = .init(false, .yellow)
    case green = .init(true, .green)

    var canAdvance: Bool { rawValue.canAdvance }
}

At that point, though, I'd probably just want to see case decls in struct definitions directly to cut out all the boilerplate.

As far as closed protocols go, one thing I would love to see from them is something akin to allCases—something like ClosedProtocol.allTypes: [ClosedProtocol.Type] that would publicly export all the accessible types which conform to ClosedProtocol.