pointfreeco / swift-case-paths

🧰 Case paths extends the key path hierarchy to enum cases.
https://www.pointfree.co/collections/enums-and-structs/case-paths
MIT License
921 stars 108 forks source link

Add Extractable and Embeddable protocols to provide a KeyPath-like interface #110

Closed Ryu0118 closed 1 year ago

Ryu0118 commented 1 year ago

Hi, I added Extractable and Embeddable protocols, which provide an interface similar to KeyPath.

enum MyEnum: Embeddable {
    case foo(String)
}

var myEnum =  MyEnum.foo("Hello, World!!")
print(myEnum[casePath: /MyEnum.foo]!) // Hello, World!!

myEnum[casePath: /MyEnum.foo] = "Hello!!"
print(myEnum[casePath: /MyEnum.foo]!) // Hello!!
stephencelis commented 1 year ago

Hey @Ryu0118! Thanks for taking the time to PR. This is actually a feature that folks request and try to implement regularly, most recently here: https://github.com/pointfreeco/swift-case-paths/pull/62

The main issue is that Swift doesn't seem to make it possible to implement a property with an optional getter and a non-optional setter, and if we use optionality everywhere, we lose some precision and introduce some nonsensical states:

result[/Result.success] = nil  // What does this mean?

@iampatbrown got pretty close to getting things working the way we would want, but it still had a few issues, unfortunately.

iampatbrown commented 1 year ago

@Ryu0118 The most recent experiment can be found here https://github.com/pointfreeco/swift-case-paths/compare/main...iampatbrown:swift-case-paths:case-accessible

It hasn't been thoroughly tested and it's not something I use, it's just an attempt to work around the limitations @stephencelis mentioned.

The following scenarios seemed to be working:

struct Baz: Equatable { var array: [Int] = [1, 2, 3], string: String = "Blob" }
indirect enum Foo: Equatable, CaseAccessible {
  case foo(Foo)
  case bar(Int)
  case baz(Baz)
}

var foo = Foo.bar(42)

foo[/Foo.foo] = .baz(Baz()) // foo == .foo(.baz(Baz())
foo[/Foo.baz]?.string = "Blobby" // no-op
foo[/Foo.foo .. /Foo.bar] = 42 // foo == .foo(.bar(42))
foo[/Foo.baz] = Baz() // foo == .baz(Baz())
foo[/Foo.foo]?[/Foo.baz] = Baz() // no-op
foo[/Foo.foo .. /Foo.baz] = Baz() // foo == .foo(.baz(Baz())
foo[/Foo.foo]?[/Foo.baz]?.array = [3, 2, 1] // foo == .foo(.baz(Baz(array: [3, 2, 1]))
foo[/Foo.foo] // .baz(Baz(array: [3, 2, 1]))
foo[/Foo.foo]?[/Foo.baz] // Baz(array: [3, 2, 1])
foo[/Foo.foo]?[/Foo.baz]?.array // [3, 2, 1]
foo[/Foo.bar] // nil
foo[/Foo.baz] // nil

foo[/Foo.foo] = nil // 🛑 forbidden by the compiler
foo[/Foo.foo]?[/Foo.baz] = nil // 🛑 forbidden by the compiler
foo[/Foo.foo .. /Foo.baz] = nil // 🛑 forbidden by the compiler
foo[/Foo.foo] = .none // 🛑 forbidden by the compiler
foo[/Foo.foo] = nil as Foo? // 🛑 forbidden by the compiler

foo[/Foo.foo] = nil as _OptionallyChained<Foo>? // 🙃 no-op
foo[/Foo.bar] = nil as _OptionallyChained<Int>? // 🙃 no-op
Ryu0118 commented 1 year ago

By doing this I was able to solve the Optional problem, but could not solve the other problems :'(

public protocol Embeddable: Extractable {}

public extension Extractable {
  subscript<Value>(casePath: CasePath<Self, Value>) -> Value? {
    casePath.extract(from: self)
  }
}

public extension Embeddable {
  @_disfavoredOverload
  subscript<Value>(casePath: CasePath<Self, Value>) -> Value {
    @available(*, unavailable, message: "only available as optional getter")
    get { fatalError() }
    set { self = casePath.embed(newValue) }
  }

  subscript<Value: _OptionalProtocol>(casePath: CasePath<Self, Value>) -> Value {
    get { casePath.extract(from: self)! }
    set { self = casePath.embed(newValue) }
  }
}
enum Foo: Embeddable {
  case c1(Int?)
  case c2(Int)
}
var e1 = Foo.c1(1)
var e2 = Foo.c2(2)
e1[/Foo.c1] = nil // e1 == .c1(nil)
e1[/Foo.c1] = 3 // e1 == .c1(3)
e2[/Foo.c2] = nil // forbidden by the compiler
e2[/Foo.c2] = 3 // e2 == .c2(3)
stephencelis commented 1 year ago

@Ryu0118 Thanks again for opening this and continuing the conversation! We plan on supporting functionality like this soon, but in a very different capacity (and are actively working on the case-key-paths branch, if you are curious). As such, I'm going to close this for now, but thanks again for your original work on it!