Repository: pointfreeco/swift-case-paths
Pull request #44 (2021-07-21 22:03:01)

Clean up helpers, deprecate old extraction code, speed up `Void` extraction
This PR contains a few things in preparation of a 0.6.0, to address the optional promotion issues discovered in #40 (fixed in #41) and #42 (fixed in #43).

First, I removed the new `CasePath.optionalCase` helper because in general we want to remove surface area outside case path literal syntax. Additionally, the following existing functions have been deprecated:

- `CasePath<Root, Value>.case(embed:)`
- `CasePath<Root, Void>.case(root:)
- `extract(_:)`
- `extract(case:from:)`

Instead, users should always prefer case path literal syntax (`/Root.caseName`) or invoke the case path's extract method (`casePath.extract(from:)`).

While reorganizing, I also swapped the void-extracting case path initializers for ones that check the enum tag.

stephencelis: /cc @mayoff in case you have any thoughts.

Patch:
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swift-case-paths-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swift-case-paths-Package.xcscheme
index f79f239..21b38cc 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/swift-case-paths-Package.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/swift-case-paths-Package.xcscheme
@@ -54,7 +54,8 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      shouldUseLaunchSchemeArgsEnv = "YES">
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
       <Testables>
          <TestableReference
             skipped = "NO">
diff --git a/Sources/CasePaths/EnumReflection.swift b/Sources/CasePaths/EnumReflection.swift
index cc56771..2260202 100644
--- a/Sources/CasePaths/EnumReflection.swift
+++ b/Sources/CasePaths/EnumReflection.swift
@@ -5,20 +5,13 @@ extension CasePath {
   ///   otherwise undefined.
   /// - Parameter embed: An enum case initializer.
   /// - Returns: A case path that extracts associated values from enum cases.
+  @available(*, deprecated, message: "Use case path literal syntax (e.g., '/Root.caseName')")
   public static func `case`(_ embed: @escaping (Value) -> Root) -> CasePath {
     self.init(
       embed: embed,
       extract: CasePaths.extract(embed)
     )
   }
-
-  public static func optionalCase<Wrapped>(_ embed: @escaping (Value) -> Wrapped?) -> CasePath
-  where Root == Wrapped? {
-    self.init(
-      embed: embed,
-      extract: CasePaths.extract(embed)
-    )
-  }
 }
 
 extension CasePath where Value == Void {
@@ -29,11 +22,11 @@ extension CasePath where Value == Void {
   ///   values. Its behavior is otherwise undefined.
   /// - Parameter value: An enum case with no associated values.
   /// - Returns: A case path that extracts `()` if the case matches, otherwise `nil`.
+  @available(*, deprecated, message: "Use case path literal syntax (e.g., '/Root.caseName')")
   public static func `case`(_ value: Root) -> CasePath {
-    let label = "\(value)"
-    return CasePath(
+    CasePath(
       embed: { value },
-      extract: { "\($0)" == label ? () : nil }
+      extract: extractVoidHelp(value)
     )
   }
 }
@@ -54,10 +47,28 @@ extension CasePath where Value == Void {
 ///   - root: A root enum value.
 /// - Returns: Values iff they can be extracted from the given enum case initializer and root enum,
 ///   otherwise `nil`.
+@available(*, deprecated, message: "Use case path literal syntax (e.g., '/Root.caseName'), or '(/Root.caseName).extract(from:)'")
 public func extract<Root, Value>(case embed: @escaping (Value) -> Root, from root: Root) -> Value? {
   CasePaths.extract(embed)(root)
 }
 
+/// Attempts to extract values associated with a given enum case initializer from a given root enum.
+///
+/// ```swift
+/// extract(case: Result<Int, Error>.success, from: .success(42))
+/// // 42
+/// extract(case: Result<Int, Error>.success, from: .failure(MyError())
+/// // nil
+/// ```
+///
+/// - Note: This function is only intended to be used with enum case initializers. Its behavior is
+///   otherwise undefined.
+/// - Parameters:
+///   - embed: An enum case initializer.
+///   - root: A root enum value.
+/// - Returns: Values iff they can be extracted from the given enum case initializer and root enum,
+///   otherwise `nil`.
+@available(*, deprecated, message: "Use case path literal syntax (e.g., '/Root.caseName'), or '(/Root.caseName).extract(from:)'")
 public func extract<Root, Value>(case embed: @escaping (Value) -> Root?, from root: Root?) -> Value?
 {
   CasePaths.extract(embed)(root)
@@ -79,7 +90,35 @@ public func extract<Root, Value>(case embed: @escaping (Value) -> Root?, from ro
 ///   otherwise undefined.
 /// - Parameter embed: An enum case initializer.
 /// - Returns: A function that can attempt to extract associated values from an enum.
+@available(*, deprecated, message: "Use case path literal syntax (e.g., '/Root.caseName')")
 public func extract<Root, Value>(_ embed: @escaping (Value) -> Root) -> (Root) -> Value? {
+  extractHelp(embed)
+}
+
+/// Returns a function that can attempt to extract associated values from the given enum case
+/// initializer.
+///
+/// Use this function to create new transform functions to pass to higher-order methods like
+/// `compactMap`:
+///
+/// ```swift
+/// [Result<Int, Error>.success(42), .failure(MyError()]
+///   .compactMap(extract(Result.success))
+/// // [42]
+/// ```
+///
+/// - Note: This function is only intended to be used with enum case initializers. Its behavior is
+///   otherwise undefined.
+/// - Parameter embed: An enum case initializer.
+/// - Returns: A function that can attempt to extract associated values from an enum.
+@available(*, deprecated, message: "Use case path literal syntax (e.g., '/Root.caseName')")
+public func extract<Root, Value>(_ embed: @escaping (Value) -> Root?) -> (Root?) -> Value? {
+  optionalPromotedExtractHelp(embed)
+}
+
+// MARK: - Extraction helpers
+
+func extractHelp<Root, Value>(_ embed: @escaping (Value) -> Root) -> (Root) -> Value? {
   guard
     let metadata = EnumMetadata(Root.self),
     metadata.typeDescriptor.fieldDescriptor != nil
@@ -88,22 +127,10 @@ public func extract<Root, Value>(_ embed: @escaping (Value) -> Root) -> (Root) -
     return { _ in nil }
   }
 
-  // https://github.com/pointfreeco/swift-case-paths/issues/40
-  if let wrappedType = metadata.wrappedTypeIfOptional(),
-    wrappedType != Value.self,
-    EnumMetadata(wrappedType) != nil
-  {
-    assertionFailure(
-      "embed parameter must not be promoted to return Optional (https://github.com/pointfreeco/swift-case-paths/issues/40)"
-    )
-    return { _ in nil }
-  }
-
   var cachedTag: UInt32?
   var cachedStrategy: Strategy<Root, Value>?
 
   return { root in
-    let metadata = EnumMetadata(assumingEnum: Root.self)
     let rootTag = metadata.tag(of: root)
 
     if let cachedTag = cachedTag, let cachedStrategy = cachedStrategy {
@@ -127,13 +154,13 @@ public func extract<Root, Value>(_ embed: @escaping (Value) -> Root) -> (Root) -
   }
 }
 
-public func extract<Root, Value>(_ embed: @escaping (Value) -> Root?) -> (Root?) -> Value? {
-  if Root.self == Value.self {
-    return { $0 as! Value? }
-  }
-
+func optionalPromotedExtractHelp<Root, Value>(
+  _ embed: @escaping (Value) -> Root?
+) -> (Root?) -> Value? {
+  guard Root.self != Value.self else { return { $0 as! Value? } }
   guard
-    EnumMetadata(Root.self)?.typeDescriptor.fieldDescriptor != nil
+    let metadata = EnumMetadata(Root.self),
+    metadata.typeDescriptor.fieldDescriptor != nil
   else {
     assertionFailure("embed parameter must be a valid enum case initializer")
     return { _ in nil }
@@ -145,7 +172,6 @@ public func extract<Root, Value>(_ embed: @escaping (Value) -> Root?) -> (Root?)
   return { optionalRoot in
     guard let root = optionalRoot else { return nil }
 
-    let metadata = EnumMetadata(assumingEnum: Root.self)
     let rootTag = metadata.tag(of: root)
 
     if let cachedTag = cachedTag, let cachedStrategy = cachedStrategy {
@@ -170,12 +196,40 @@ public func extract<Root, Value>(_ embed: @escaping (Value) -> Root?) -> (Root?)
   }
 }
 
-// MARK: - Private Helpers
+func extractVoidHelp<Root>(_ root: Root) -> (Root) -> Void? {
+  guard
+    let metadata = EnumMetadata(Root.self),
+    metadata.typeDescriptor.fieldDescriptor != nil
+  else {
+    assertionFailure("value must be a valid enum case")
+    return { _ in nil }
+  }
+
+  let cachedTag = metadata.tag(of: root)
+  return { root in metadata.tag(of: root) == cachedTag ? () : nil }
+}
+
+func optionalPromotedExtractVoidHelp<Root>(_ root: Root?) -> (Root?) -> Void? {
+  guard
+    let root = root,
+    let metadata = EnumMetadata(Root.self),
+    metadata.typeDescriptor.fieldDescriptor != nil
+  else {
+    assertionFailure("value must be a valid enum case")
+    return { _ in nil }
+  }
+
+  let cachedTag = metadata.tag(of: root)
+  return { root in root.flatMap(metadata.tag(of:)) == cachedTag ? () : nil }
+}
+
+// MARK: - Runtime reflection
 
 private enum Strategy<Enum, Value> {
   case direct
   case existential(extract: (Enum) -> Any?)
   case indirect
+  case optional(extract: (Enum) -> Value?)
   case unimplemented
   case void
 }
@@ -224,6 +278,18 @@ extension Strategy {
       let anyStrategy = Strategy<Enum, Any>(nonExistentialTag: tag)
       self = .existential { anyStrategy.extract(from: $0, tag: tag) }
 
+    } else if EnumMetadata(avType)?.isOptional() ?? false {
+      // Maybe the embed function's argument was promoted out
+      // of Optional, e.g. (String?) -> Result<String?, Error>
+      // was promoted to (String) -> Result<String?, Error>.
+
+      let wrappedStrategy = Strategy<Enum, Value?>(tag: tag, assumedAssociatedValueType: avType)
+      if case .unimplemented = wrappedStrategy {
+        self = .unimplemented
+      } else {
+        self = .optional { wrappedStrategy.extract(from: $0, tag: tag).flatMap { $0 } }
+      }
+
     } else {
       self = .unimplemented
     }
@@ -257,6 +323,9 @@ extension Strategy {
           .load(as: Value.self)
       }
 
+    case let .optional(extract):
+      return extract(root)
+
     case .unimplemented:
       return nil
 
@@ -384,8 +453,6 @@ extension EnumMetadata {
   }
 }
 
-// MARK: -
-
 private struct EnumTypeDescriptor: Equatable {
   let ptr: UnsafeRawPointer
 
@@ -411,8 +478,6 @@ extension EnumTypeDescriptor {
   }
 }
 
-// MARK: -
-
 private struct TupleMetadata: Metadata {
   let ptr: UnsafeRawPointer
 
@@ -467,8 +532,6 @@ extension TupleMetadata {
   }
 }
 
-// MARK: -
-
 private struct ExistentialMetadata: Metadata {
   let ptr: UnsafeRawPointer
 
diff --git a/Sources/CasePaths/Operators.swift b/Sources/CasePaths/Operators.swift
index 1ecee6b..e6e04f3 100644
--- a/Sources/CasePaths/Operators.swift
+++ b/Sources/CasePaths/Operators.swift
@@ -23,13 +23,19 @@ public func ~= <Root, Value>(pattern: CasePath<Root, Value>, value: Root) -> Boo
 public prefix func / <Root, Value>(
   embed: @escaping (Value) -> Root
 ) -> CasePath<Root, Value> {
-  .case(embed)
+  .init(embed: embed, extract: extractHelp(embed))
 }
 
+/// Returns a case path for the given embed function.
+///
+/// - Note: This operator is only intended to be used with enum cases that have no associated
+///   values. Its behavior is otherwise undefined.
+/// - Parameter embed: An embed function.
+/// - Returns: A case path.
 public prefix func / <Root, Value>(
   embed: @escaping (Value) -> Root?
 ) -> CasePath<Root?, Value> {
-  .optionalCase(embed)
+  .init(embed: embed,  extract: optionalPromotedExtractHelp(embed))
 }
 
 /// Returns a void case path for a case with no associated value.
@@ -41,7 +47,19 @@ public prefix func / <Root, Value>(
 public prefix func / <Root>(
   root: Root
 ) -> CasePath<Root, Void> {
-  .case(root)
+  .init(embed: { root }, extract: extractVoidHelp(root))
+}
+
+/// Returns a void case path for a case with no associated value.
+///
+/// - Note: This operator is only intended to be used with enum cases that have no associated
+///   values. Its behavior is otherwise undefined.
+/// - Parameter root: A case with no an associated value.
+/// - Returns: A void case path.
+public prefix func / <Root>(
+  root: Root?
+) -> CasePath<Root?, Void> {
+  .init(embed: { root }, extract: optionalPromotedExtractVoidHelp(root))
 }
 
 /// Returns the identity case path for the given type. Enables `/MyType.self` syntax.
@@ -79,13 +97,36 @@ public prefix func / <Root>(
 ///
 /// - Note: This operator is only intended to be used with enum case initializers. Its behavior is
 ///   otherwise undefined.
-/// - Parameter case: An enum case initializer.
+/// - Parameter embed: An enum case initializer.
 /// - Returns: A function that can attempt to extract associated values from an enum.
 @_disfavoredOverload
 public prefix func / <Root, Value>(
-  case: @escaping (Value) -> Root
+  embed: @escaping (Value) -> Root
 ) -> (Root) -> Value? {
-  extract(`case`)
+  (/embed).extract(from:)
+}
+
+/// Returns a function that can attempt to extract associated values from the given enum case
+/// initializer.
+///
+/// Use this operator to create new transform functions to pass to higher-order methods like
+/// `compactMap`:
+///
+/// ```swift
+/// [Result<Int, Error>.success(42), .failure(MyError()]
+///   .compactMap(/Result.success)
+/// // [42]
+/// ```
+///
+/// - Note: This operator is only intended to be used with enum case initializers. Its behavior is
+///   otherwise undefined.
+/// - Parameter embed: An enum case initializer.
+/// - Returns: A function that can attempt to extract associated values from an enum.
+@_disfavoredOverload
+public prefix func / <Root, Value>(
+  embed: @escaping (Value) -> Root?
+) -> (Root?) -> Value? {
+  (/embed).extract(from:)
 }
 
 /// Returns a void case path for a case with no associated value.
@@ -98,7 +139,7 @@ public prefix func / <Root, Value>(
 public prefix func / <Root>(
   root: Root
 ) -> (Root) -> Void? {
-  (/root).extract
+  (/root).extract(from:)
 }
 
 precedencegroup CasePathCompositionPrecedence {
@@ -135,7 +176,7 @@ extension CasePath {
     lhs: CasePath,
     rhs: @escaping (AppendedValue) -> Value
   ) -> CasePath<Root, AppendedValue> {
-    lhs.appending(path: .case(rhs))
+    lhs.appending(path: /rhs)
   }
 }
 
@@ -158,5 +199,5 @@ public func .. <Root, Value, AppendedValue>(
   lhs: @escaping (Root) -> Value?,
   rhs: @escaping (AppendedValue) -> Value
 ) -> (Root) -> AppendedValue? {
-  return { root in lhs(root).flatMap(extract(rhs)) }
+  return { root in lhs(root).flatMap((/rhs).extract(from:)) }
 }


Test patch:
diff --git a/Tests/CasePathsTests/CasePathsTests.swift b/Tests/CasePathsTests/CasePathsTests.swift
index 8381e4b..fd0fc08 100644
--- a/Tests/CasePathsTests/CasePathsTests.swift
+++ b/Tests/CasePathsTests/CasePathsTests.swift
@@ -450,23 +450,7 @@ final class CasePathsTests: XCTestCase {
     }
   }
 
-  func testDirectExtractFromOptionalRoot() {
-    // https://github.com/pointfreeco/swift-case-paths/issues/40
-
-    enum Authentication {
-      case authenticated(token: String)
-      case unauthenticated
-    }
-
-    let root: Authentication? = .authenticated(token: "deadbeef")
-    let embed: (String) -> Authentication? = Authentication.authenticated
-    let actual = extract(case: embed, from: root)
-    XCTAssertEqual(actual, "deadbeef")
-  }
-
   func testPathExtractFromOptionalRoot() {
-    // https://github.com/pointfreeco/swift-case-paths/issues/40
-
     enum Authentication {
       case authenticated(token: String)
       case unauthenticated
@@ -766,8 +750,8 @@ final class CasePathsTests: XCTestCase {
   }
 
   func testExample() {
-    XCTAssertEqual("Blob", extract(case: Result<String, Error>.success, from: .success("Blob")))
-    XCTAssertNil(extract(case: Result<String, Error>.failure, from: .success("Blob")))
+    XCTAssertEqual("Blob", (/Result<String, Error>.success).extract(from: .success("Blob")))
+    XCTAssertNil((/Result<String, Error>.failure).extract(from: .success("Blob")))
 
     XCTAssertEqual(42, (/Int??.some .. Int?.some).extract(from: Optional(Optional(42))))
   }
@@ -831,6 +815,15 @@ final class CasePathsTests: XCTestCase {
     )
   }
 
+  func testOptionalInsideResult() {
+    let result: Result<String?, Error> = .success("hello, world")
+    let path: CasePath<Result<String?, Error>, String> = /Result.success
+    let actual = path.extract(from: result)
+    XCTAssertEqual(
+      actual,
+      "hello, world")
+  }
+
   func testExtractFromOptionalRoot() {
     enum Foo {
       case foo(String)