Polymorphic methods in enums

Obviously:

case _ where case (.foo,.bar) = (x,y)

:smile:

2 Likes

For me it is not so much about the typing, it is about the clarity. The switch statement is incredibly procedural; test if it is this, if so do that. Whereas the function definitions are declarative. Also the function definitions group everything together nicely associated with each case.

People seem to like clarity in Swift, for example I often see:

struct SomeStructure { ... }
extension SomeStructure: SomeProtocol {
    func protocolMethod1() { ... }
    func protocolMethod1() { ... }
}

This is clearly longer than just defining the protocol methods directly in the struct. However people like the grouping that the extension brings. Similarly grouping the methods with the case adds nice clarity.

2 Likes

You would have to have labelled associated values.

enum Foo { 
    case bar(_ value: Int) { 
        func someFunction() -> Int { return value } 
    } 
}
1 Like

I would appreciate something like this. For this exact reason, I currently format my enum functions like this:

func readTo() -> NetworkReadTo { switch self {
  case .readLength:        return .length(2)
  case .readString(let l): return .length(l)
  case .disconnect:        return .finished
}}
3 Likes

This is exactly how I do it, too.

I think this is a very reasonable sugar to let us drop the enclosing switch self { ... } within the body of the functions defined within an enum.

In Kotlin you can do a short syntax switch (when in Kotlin speak) without the extra brackets that is very similar to what people are proposing in this discussion for Swift:

enum class TrafficLightSwiftLike {
    green, yellow, red;
    val canAdvance: Boolean
        get() = when (this) {
            green -> true
            yellow -> true
            red -> false
        }
}

However, overwhelmingly people do:

enum class TrafficLightKotlinLike {
    green {
        override val canAdvance: Boolean = true
    },
    yellow {
        override val canAdvance: Boolean = true
    },
    red {
        override val canAdvance: Boolean = false
    };
    abstract val canAdvance: Boolean
}

So I think that given the choice people will choose the 2nd form in Swift like they do in Kotlin. Which is the form I prefer; because it is declarative and groups the methods with the case.

Number two there seems to be far less readable than the first the more overrides you add, as it forces you to parse each case and find the override you're interested in before being able to see the value that's returned. Plus, for more cases, it prevents collapsing cases that return the same value into the same line. So again, this just seems like a different kind of verbosity with no real advantage over the Swift status quo.

5 Likes

One is the inverse of the other; you can either see what each method does for all types or you can see what each type does for all methods. For people use to OOP the later is more natural, but for people use to FP the former. It is definitely true that in Kotlin the OOP style is much more prevalent, I don't even recall seeing the FP approach, but that may be because there are many more OOP programmers than there are FP programmers. Anyway it would be nice if you could do both.

That mostly sounds like a good way to have a confusing duplication of syntax, since they both do the same thing, and the new version doesn't offer any new capabilities. Generally, it doesn't seem like a good idea to add new syntax that duplicates existing function if it isn't strictly superior (or sugary) to the existing solution.

3 Likes

Although the original idea that uses extensions is too much of a deviation for Swift in my opinion, I like how this thread's discussion is going more into how we could allow conditional statements act as expressions to avoid the additional nesting this currently requires.

I think this reads pretty nicely, even more so for functions:

enum class TrafficLightSwiftLike {
    green, yellow, red;

    fun canAdvance(): Boolean = when (this) {
        green -> true
        else -> false
    }
}

There are two independently useful features that make this happen:

  1. when is an expression
  2. Single-expression functions

If Swift had equivalents to those (and hell, let's make that "case" prefix optional while we're at it, what has that ever done for us :crazy_face:) it could look like this:

enum TrafficLight {
    case green, yellow, red

    func canAdvance() -> Bool = switch self {
        .green: true
        default: false
    }
}

I'd say that looks pretty good, with the added benefit of both of the features being nice to have on their own.

FWIW, the case prefix lets the compiler know when the previous case-block is finished, since Swift (mostly) avoids treating newlines or indentation as affecting program structure.* It's possible to resolve this ambiguity by scanning ahead for a colon, but that requires arbitrary lookahead (which affects the live-editing experience), and makes it harder to recover the user's intent and produce a good diagnostic when code is incomplete or malformed.

* Technically, newlines do commonly affect program structure, but no differently from any other whitespace. There are a few exceptions, of course, with #if being the most obvious one.

6 Likes

Ah, I guess that's why both Kotlin and Rust require a block if you want multiple statements in a case. I'd prefer that over needing to write "case" for each case, but that might be too big of a change by now :confused:

+1

From my experience, we deal with enum methods as a short-hand map to concrete types in a given context.
Probably, it makes sense to separate self-swiftching methods on a syntax level.

Example:

enum IntEnum { // Why not?    
    case one, two, three, many(Int)

    map intify => Int {
        case .one:       1
        case .two:       2
        case .many(val): val
        default:         0
    }   
}

It's certainly grammatically tractable to just allow case and default at the top level of a function with the semantics that there's implicitly a switch over self. (We'd forbid it in functions that aren't instance methods, and arguably the conservative position would to be to only allow it on enums.) It's not as elegant as the ML/Haskell feature because switching over any other arguments would still require an explicit switch, but practically speaking it would clean up methods on enums a lot.

But there's a reasonable argument against it as a novel special case rule that just removes some very obvious boilerplate.

3 Likes

Which is why if we wanted this kind of feature, I would rather it be a generalized thing like Haskell/Elixir/Erlang that could be used on any function/method, rather than a special case for enums. From my experience, I'm rarely writing enum methods that span many lines per case that would really warrant this sugar. Whereas a generic when on a function/method declaration to switch on arguments could be a pretty useful feature.

1 Like

Composing a single function out of a bunch of apparently-independent declarations with when clauses on them is just never going to work as well in Swift as it does in those languages. Swift is just too opinionated about adding other information to function declarations besides the parameter list — types, modifiers, attributes, etc. — plus of course Swift allows overloading, which complicates the problem quite a bit. The right approach has to be aimed at making it easier to define a function whose body is a switch.

4 Likes

I agree with this. If we're going to add sugar it should be general even if it is a smaller win for enum methods. I have had struct methods taking an enum parameter where the body is a single switch where every case returns and would have enjoyed using this sugar in that context.

1 Like

I agree with this also and would love to see this happen!

1 Like

Yeah, I've never found a syntax for this that is significantly better than a switch if you want any flexibility here (e.g. choosing whether to include arguments in the switch instead of just self). You'll also probably quickly find that, as code evolves, you want to add some preamble or postscript code around the switch and it would be preferable if this didn't require major restructuring. So, as you say, it would probably end up being best to limit it to an implicit switch over self (or self plus all arguments, which might be more useful?), which would be appreciated, especially in enum heavy code like state machines, but is a special case with fairly minor benefits.