[Suggestion] Case-based dispatch for enum methods


(Taras Zakharko) #1

Recently, I have been working on implementing some non-trivial data structures in Swift (its about storing polygons in a space-partitioning tree). I am using enums to represent different types of parent nodes and leafs and I had some ideas to make them more fit for this kind of work. I expect that I will write multiple enum-related emails, each one concentrating on one particular feature. A usual disclaimer: these are random, quite rough ideas that might or not make sense, but I’d like to get some feedback from the community.

Case-based dispatch for enum methods

Often, behaviour of enum method depends on the enum value. For instance, imagine a tree structure with an insert(value:) method: the way how the inserting is handled depends on the type of the node. Usually, you’d implement it as a switch operation:

func insert(value:T) {
  switch self {
    case let Leaf1(a, b, c): …
    case let Leaf2(a, b): …
    case let Parent1(x, y): …
  }
}

If the insert operation is non-trivial, this becomes quite convoluted. You can of course define private helper methods or functions that perform the specific functionality, but I don’t find it to be a very satisfying solution: there is still the switch boilerplate + you need to be careful to call the correct helper, so there are some safety issues.

Now, suppose there was a way to add a method implementation that is case-specific:

enum MyTree {
  case Leaf1(Float, Float) {
    mutating func insert(value: T) {
       let (a, b) = self.Leaf1 // or something like that
       // handle insert for the case that node is of type Parent1

       ...
     }
  }

case Parent1(Int, Float) {
     mutating func insert(value: T) {
       let (x, y) = self.Parent1 // or something like that
       // handle insert for the case that node is of type Parent1
       ...
     }
  }

default {
   mutating func insert(value: T) {
      // handle insert for all other cases
       ...
     }
}
}

etc. The case method specification needs to be exhaustive and adhere to the same signature. It is a compile-time error to specify a method or property only in some cases but not in the other ones (that is why we have the default implementation). Outer scope definitions apply to all cases and cannot be overridden by a case-specific implementation.

Basically, the compiler would synthesise an outer-scope method that does a switch operator to dispatch to the particular implementation. This is thus mostly syntactic sugar which also promotes safety (as it becomes impossible to call the wrong implementation). These would make the case-specific methods fully compatible with protocols etc. (e.g. a protocol Insertable { mutating func insert(value:) }

Looking forward to your thoughts on this!

Best,

Taras


(Taras Zakharko) #2

Is the lack of comments due to general disinterest in such a thing or did my mail go amiss somehow? :wink:

Best,

Taras

···

On 31 Mar 2016, at 14:39, Taras Zakharko <taras.zakharko@uzh.ch> wrote:

Recently, I have been working on implementing some non-trivial data structures in Swift (its about storing polygons in a space-partitioning tree). I am using enums to represent different types of parent nodes and leafs and I had some ideas to make them more fit for this kind of work. I expect that I will write multiple enum-related emails, each one concentrating on one particular feature. A usual disclaimer: these are random, quite rough ideas that might or not make sense, but I’d like to get some feedback from the community.

Case-based dispatch for enum methods

Often, behaviour of enum method depends on the enum value. For instance, imagine a tree structure with an insert(value:) method: the way how the inserting is handled depends on the type of the node. Usually, you’d implement it as a switch operation:

func insert(value:T) {
  switch self {
    case let Leaf1(a, b, c): …
    case let Leaf2(a, b): …
    case let Parent1(x, y): …
  }
}

If the insert operation is non-trivial, this becomes quite convoluted. You can of course define private helper methods or functions that perform the specific functionality, but I don’t find it to be a very satisfying solution: there is still the switch boilerplate + you need to be careful to call the correct helper, so there are some safety issues.

Now, suppose there was a way to add a method implementation that is case-specific:

enum MyTree {
  case Leaf1(Float, Float) {
    mutating func insert(value: T) {
       let (a, b) = self.Leaf1 // or something like that
       // handle insert for the case that node is of type Parent1

       ...
     }
  }

case Parent1(Int, Float) {
     mutating func insert(value: T) {
       let (x, y) = self.Parent1 // or something like that
       // handle insert for the case that node is of type Parent1
       ...
     }
  }

default {
   mutating func insert(value: T) {
      // handle insert for all other cases
       ...
     }
}
}

etc. The case method specification needs to be exhaustive and adhere to the same signature. It is a compile-time error to specify a method or property only in some cases but not in the other ones (that is why we have the default implementation). Outer scope definitions apply to all cases and cannot be overridden by a case-specific implementation.

Basically, the compiler would synthesise an outer-scope method that does a switch operator to dispatch to the particular implementation. This is thus mostly syntactic sugar which also promotes safety (as it becomes impossible to call the wrong implementation). These would make the case-specific methods fully compatible with protocols etc. (e.g. a protocol Insertable { mutating func insert(value:) }

Looking forward to your thoughts on this!

Best,

Taras


(Brent Royal-Gordon) #3

Is the lack of comments due to general disinterest in such a thing or did my mail go amiss somehow? :wink:

I suspect it has something to do with the very similar thread I started about a week before you. :^) <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160321/013153.html> Your use of `default` is new and very interesting, though.

I've actually been thinking about this a little more, and it occurred to me that we might be able to borrow Joe Groff's accessor idea from the property behaviors proposal. As you may recall, accessors are a way to specify a method (or, perhaps, a computed property) that can be implemented by the property to customize the behavior; accessors are implemented in a curly-bracket-enclosed block attached to the property. In property behaviors, they look something like this:

  var behavior getObserver<Value>: Value {
    accessor func willGet() {}
    accessor func didGet() {}
    
    var value: Value
    
    get {
      willGet()
      let v = value
      didGet()
      return v
    }
    set {
      value = newValue
    }
    ...
  }
  
  @getObserver var x: Int {
    willGet { print("Getting x") }
  }

Using the same mechanism in enums might remove the redundant boilerplate caused by redeclaring the same methods in every case, and also reduce the appearance that cases are types. Your example might look like:

  enum MyTree {
    mutating accessor func insert(value: T)
    
    case Leaf1(Float, Float) {
      insert {
        …
      }
    }
    case Parent1(Int, Float) {
      insert {
        …
      }
    }
    default {
      insert {
        …
      }
    }
  }

Or, to use my example:

  enum Suit: Int {
    accessor var isRed: Bool { get }
    accessor var description: String { get }
    
    case hearts {
      description { return ":heart:️" }
      isRed { return true }
    }
    case spades {
      description { return ":spades:️" }
    }
    case diamonds {
      description { return ":diamonds:️" }
      isRed { return true }
    }
    case clubs {
      description { return ":clubs:️" }
    }
    default {
      isRed { return false }
    }
  }

···

--
Brent Royal-Gordon
Architechies


(TJ Usiyan) #4

I missed it the first time around.

I like this idea but worry that it will invite some difficult to follow
implementations.

I think that there would need to exist some restrictions on how methods
could be broken up. One such restriction might be that these
implementations could not be broken up between extensions for a given
method. I don't know if the added complexity of reading this pays off
though. you get something like this if you call a method in each case of
the enum.

Would keeping track of exhaustiveness be more of a technical issue with
this feature? On the user side, this feature would be a pain without
improving the 'switch must be exhaustive' message to include the cases that
the compiler believes are missing.

TJ

···

On Tue, Apr 5, 2016 at 1:57 AM, Taras Zakharko via swift-evolution < swift-evolution@swift.org> wrote:

Is the lack of comments due to general disinterest in such a thing or did
my mail go amiss somehow? :wink:

Best,

Taras

On 31 Mar 2016, at 14:39, Taras Zakharko <taras.zakharko@uzh.ch> wrote:

Recently, I have been working on implementing some non-trivial data
structures in Swift (its about storing polygons in a space-partitioning
tree). I am using enums to represent different types of parent nodes and
leafs and I had some ideas to make them more fit for this kind of work. I
expect that I will write multiple enum-related emails, each one
concentrating on one particular feature. A usual disclaimer: these are
random, quite rough ideas that might or not make sense, but I’d like to get
some feedback from the community.

*Case-based dispatch for enum methods*

Often, behaviour of enum method depends on the enum value. For instance,
imagine a tree structure with an insert(value:) method: the way how the
inserting is handled depends on the type of the node. Usually, you’d
implement it as a switch operation:

func insert(value:T) {
  switch self {
    case let Leaf1(a, b, c): …
    case let Leaf2(a, b): …
    case let Parent1(x, y): …
  }
}

If the insert operation is non-trivial, this becomes quite convoluted. You
can of course define private helper methods or functions that perform the
specific functionality, but I don’t find it to be a very satisfying
solution: there is still the switch boilerplate + you need to be careful to
call the correct helper, so there are some safety issues.

Now, suppose there was a way to add a method implementation that is
case-specific:

enum MyTree {
  case Leaf1(Float, Float) {
    mutating func insert(value: T) {
       let (a, b) = self.Leaf1 // or something like that
       // handle insert for the case that node is of type Parent1

       ...
     }
  }

case Parent1(Int, Float) {
     mutating func insert(value: T) {
       let (x, y) = self.Parent1 // or something like that
       // handle insert for the case that node is of type Parent1
       ...
     }
  }

default {
   mutating func insert(value: T) {
      // handle insert for all other cases
       ...
     }
}
}

etc. The case method specification needs to be exhaustive and adhere to
the same signature. It is a compile-time error to specify a method or
property only in some cases but not in the other ones (that is why we have
the default implementation). Outer scope definitions apply to all cases and
cannot be overridden by a case-specific implementation.

Basically, the compiler would synthesise an outer-scope method that does a
switch operator to dispatch to the particular implementation. This is thus
mostly syntactic sugar which also promotes safety (as it becomes impossible
to call the wrong implementation). These would make the case-specific
methods fully compatible with protocols etc. (e.g. a protocol Insertable {
mutating func insert(value:) }

Looking forward to your thoughts on this!

Best,

Taras

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Taras Zakharko) #5

I missed it the first time around.

I like this idea but worry that it will invite some difficult to follow implementations.

True. I think, the question is how flexible one wants enum to be. Right now, Swift enums are nice for representing compact state, but they are quite awkward when trying to do more (e.g. complex recursive data structures with polymorphic behavior). This might be by design, or this could be simply because enums are still incomplete. Extending enum functionality would effectively make then another alternative to class hierarchies/protocols, only that in the enum case the polymorphic structure is constrained initially. I think it might make sense for enum to be used to implement highly specialised polymorphic data structures, but its more a question of language design. It would be fine in my book if one said ‘no, we really don’t want enums to be that powerful use protocols/structs or classes for complex cases like that’.

This is something where I’d like to have more input from the core team, how do they envision the enum future?

I think that there would need to exist some restrictions on how methods could be broken up. One such restriction might be that these implementations could not be broken up between extensions for a given method. I don't know if the added complexity of reading this pays off though. you get something like this if you call a method in each case of the enum.

A restriction like this would make perfect sense. Tracking of exhaustiveness could (should) be done at a declaration scope level, i.e. declarations must be exhaustive after every enum definition/extension.

Would keeping track of exhaustiveness be more of a technical issue with this feature? On the user side, this feature would be a pain without improving the 'switch must be exhaustive' message to include the cases that the compiler believes are missing.

I imagine a compiler error along the lines of ‘missing implementation for func() for case A, B, C'

···

On 05 Apr 2016, at 08:19, T.J. Usiyan <griotspeak@gmail.com> wrote:

TJ

On Tue, Apr 5, 2016 at 1:57 AM, Taras Zakharko via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Is the lack of comments due to general disinterest in such a thing or did my mail go amiss somehow? :wink:

Best,

Taras

On 31 Mar 2016, at 14:39, Taras Zakharko <taras.zakharko@uzh.ch <mailto:taras.zakharko@uzh.ch>> wrote:

Recently, I have been working on implementing some non-trivial data structures in Swift (its about storing polygons in a space-partitioning tree). I am using enums to represent different types of parent nodes and leafs and I had some ideas to make them more fit for this kind of work. I expect that I will write multiple enum-related emails, each one concentrating on one particular feature. A usual disclaimer: these are random, quite rough ideas that might or not make sense, but I’d like to get some feedback from the community.

Case-based dispatch for enum methods

Often, behaviour of enum method depends on the enum value. For instance, imagine a tree structure with an insert(value:) method: the way how the inserting is handled depends on the type of the node. Usually, you’d implement it as a switch operation:

func insert(value:T) {
  switch self {
    case let Leaf1(a, b, c): …
    case let Leaf2(a, b): …
    case let Parent1(x, y): …
  }
}

If the insert operation is non-trivial, this becomes quite convoluted. You can of course define private helper methods or functions that perform the specific functionality, but I don’t find it to be a very satisfying solution: there is still the switch boilerplate + you need to be careful to call the correct helper, so there are some safety issues.

Now, suppose there was a way to add a method implementation that is case-specific:

enum MyTree {
  case Leaf1(Float, Float) {
    mutating func insert(value: T) {
       let (a, b) = self.Leaf1 // or something like that
       // handle insert for the case that node is of type Parent1

       ...
     }
  }

case Parent1(Int, Float) {
     mutating func insert(value: T) {
       let (x, y) = self.Parent1 // or something like that
       // handle insert for the case that node is of type Parent1
       ...
     }
  }

default {
   mutating func insert(value: T) {
      // handle insert for all other cases
       ...
     }
}
}

etc. The case method specification needs to be exhaustive and adhere to the same signature. It is a compile-time error to specify a method or property only in some cases but not in the other ones (that is why we have the default implementation). Outer scope definitions apply to all cases and cannot be overridden by a case-specific implementation.

Basically, the compiler would synthesise an outer-scope method that does a switch operator to dispatch to the particular implementation. This is thus mostly syntactic sugar which also promotes safety (as it becomes impossible to call the wrong implementation). These would make the case-specific methods fully compatible with protocols etc. (e.g. a protocol Insertable { mutating func insert(value:) }

Looking forward to your thoughts on this!

Best,

Taras

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution