From 47740c012943020aa89df93129b4fc2f33618c00 Mon Sep 17 00:00:00 2001 From: Jon Shier Date: Sun, 2 Dec 2018 22:15:02 -0500 Subject: [PATCH] Add update Result type to standard library. (cherry picked from commit 33dcfc462f3c92add982f91bc5f7424e474e0a3c) --- stdlib/public/core/CMakeLists.txt | 1 + stdlib/public/core/GroupInfo.json | 3 + stdlib/public/core/Result.swift | 168 ++++++++++++++++ .../typealias/two-modules/main.swift | 1 + test/stdlib/Result.swift | 188 ++++++++++++++++++ 5 files changed, 361 insertions(+) create mode 100644 stdlib/public/core/Result.swift create mode 100644 test/stdlib/Result.swift diff --git a/stdlib/public/core/CMakeLists.txt b/stdlib/public/core/CMakeLists.txt index 1e4500ebe413d..169941c01b15e 100644 --- a/stdlib/public/core/CMakeLists.txt +++ b/stdlib/public/core/CMakeLists.txt @@ -116,6 +116,7 @@ set(SWIFTLIB_ESSENTIAL ReflectionMirror.swift Repeat.swift REPL.swift + Result.swift Reverse.swift Runtime.swift.gyb RuntimeFunctionCounters.swift diff --git a/stdlib/public/core/GroupInfo.json b/stdlib/public/core/GroupInfo.json index cd66c3e85103d..55d01d5ac2910 100644 --- a/stdlib/public/core/GroupInfo.json +++ b/stdlib/public/core/GroupInfo.json @@ -223,5 +223,8 @@ "Comparable.swift", "Codable.swift", "MigrationSupport.swift" + ], + "Result": [ + "Result.swift" ] } diff --git a/stdlib/public/core/Result.swift b/stdlib/public/core/Result.swift new file mode 100644 index 0000000000000..fdd742e9932ca --- /dev/null +++ b/stdlib/public/core/Result.swift @@ -0,0 +1,168 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2018 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// A value that represents either a success or a failure, including an +/// associated value in each case. +@_frozen +public enum Result { + /// A success, storing a `Success` value. + case success(Success) + + /// A failure, storing a `Failure` value. + case failure(Failure) + + /// Returns a new result, mapping any success value using the given + /// transformation. + /// + /// Use this method when you need to transform the value of a `Result` + /// instance when it represents a success. The following example transforms + /// the integer success value of a result into a string: + /// + /// func getNextInteger() -> Result { ... } + /// + /// let integerResult = getNextInteger() + /// // integerResult == .success(5) + /// let stringResult = integerResult.map({ String($0) }) + /// // stringResult == .success("5") + /// + /// - Parameter transform: A closure that takes the success value of this + /// instance. + /// - Returns: A `Result` instance with the result of evaluating `transform` + /// as the new success value if this instance represents a success. + public func map( + _ transform: (Success) -> NewSuccess + ) -> Result { + switch self { + case let .success(success): + return .success(transform(success)) + case let .failure(failure): + return .failure(failure) + } + } + + /// Returns a new result, mapping any failure value using the given + /// transformation. + /// + /// Use this method when you need to transform the value of a `Result` + /// instance when it represents a failure. The following example transforms + /// the error value of a result by wrapping it in a custom `Error` type: + /// + /// struct DatedError: Error { + /// var error: Error + /// var date: Date + /// + /// init(_ error: Error) { + /// self.error = error + /// self.date = Date() + /// } + /// } + /// + /// let result: Result = ... + /// // result == .failure() + /// let resultWithDatedError = result.mapError({ e in DatedError(e) }) + /// // result == .failure(DatedError(error: , date: )) + /// + /// - Parameter transform: A closure that takes the failure value of the + /// instance. + /// - Returns: A `Result` instance with the result of evaluating `transform` + /// as the new failure value if this instance represents a failure. + public func mapError( + _ transform: (Failure) -> NewFailure + ) -> Result { + switch self { + case let .success(success): + return .success(success) + case let .failure(failure): + return .failure(transform(failure)) + } + } + + /// Returns a new result, mapping any success value using the given + /// transformation and unwrapping the produced result. + /// + /// - Parameter transform: A closure that takes the success value of the + /// instance. + /// - Returns: A `Result` instance with the result of evaluating `transform` + /// as the new failure value if this instance represents a failure. + public func flatMap( + _ transform: (Success) -> Result + ) -> Result { + switch self { + case let .success(success): + return transform(success) + case let .failure(failure): + return .failure(failure) + } + } + + /// Returns a new result, mapping any failure value using the given + /// transformation and unwrapping the produced result. + /// + /// - Parameter transform: A closure that takes the failure value of the + /// instance. + /// - Returns: A `Result` instance, either from the closure or the previous + /// `.success`. + public func flatMapError( + _ transform: (Failure) -> Result + ) -> Result { + switch self { + case let .success(success): + return .success(success) + case let .failure(failure): + return transform(failure) + } + } + + /// Returns the success value as a throwing expression. + /// + /// Use this method to retrieve the value of this result if it represents a + /// success, or to catch the value if it represents a failure. + /// + /// let integerResult: Result = .success(5) + /// do { + /// let value = try integerResult.get() + /// print("The value is \(value).") + /// } catch error { + /// print("Error retrieving the value: \(error)") + /// } + /// // Prints "The value is 5." + /// + /// - Returns: The success value, if the instance represent a success. + /// - Throws: The failure value, if the instance represents a failure. + public func get() throws -> Success { + switch self { + case let .success(success): + return success + case let .failure(failure): + throw failure + } + } +} + +extension Result where Failure == Swift.Error { + /// Creates a new result by evaluating a throwing closure, capturing the + /// returned value as a success, or any thrown error as a failure. + /// + /// - Parameter body: A throwing closure to evaluate. + @_transparent + public init(catching body: () throws -> Success) { + do { + self = .success(try body()) + } catch { + self = .failure(error) + } + } +} + +extension Result: Equatable where Success: Equatable, Failure: Equatable { } + +extension Result: Hashable where Success: Hashable, Failure: Hashable { } diff --git a/test/multifile/typealias/two-modules/main.swift b/test/multifile/typealias/two-modules/main.swift index bcab55241befa..a48c80ae43d85 100644 --- a/test/multifile/typealias/two-modules/main.swift +++ b/test/multifile/typealias/two-modules/main.swift @@ -10,6 +10,7 @@ // RUN: %target-build-swift -g %S/main.swift %t/linker/library.o -I %t/linker/ -L %t/linker/ -o %t/linker/main import library +import enum library.Result func testFunction(withCompletion completion: (Result) -> Void) { } testFunction { (result: GenericResult) in } diff --git a/test/stdlib/Result.swift b/test/stdlib/Result.swift new file mode 100644 index 0000000000000..d584f6ce28d29 --- /dev/null +++ b/test/stdlib/Result.swift @@ -0,0 +1,188 @@ +// RUN: %target-run-simple-swift +// REQUIRES: executable_test + +import StdlibUnittest +import Swift + +let ResultTests = TestSuite("Result") + +fileprivate enum Err: Error, Equatable { + case err + case derr +} + +fileprivate let string = "string" + +fileprivate extension Result { + var success: Success? { + switch self { + case let .success(success): + return success + case .failure: + return nil + } + } + + var failure: Failure? { + switch self { + case .success: + return nil + case let .failure(failure): + return failure + } + } +} + +ResultTests.test("Construction") { + let result1: Result = .success(string) + let result2: Result = .failure(.err) + let string1: String? = { + switch result1 { + case let .success(string): + return string + case .failure: + expectUnreachable() + return nil + } + }() + let error: Err? = { + switch result2 { + case let .failure(failure): + return failure + case .success: + expectUnreachable() + return nil + } + }() + + expectEqual(string, string1) + expectEqual(.err, error) +} + +ResultTests.test("Throwing Initialization and Unwrapping") { + func notThrowing() throws -> String { + return string + } + + func throwing() throws -> String { + throw Err.err + } + + let result1 = Result { try throwing() } + let result2 = Result { try notThrowing() } + + expectEqual(result1.failure as? Err, Err.err) + expectEqual(result2.success, string) + + do { + _ = try result1.get() + } catch let error as Err { + expectEqual(error, Err.err) + } catch { + expectUnreachable() + } + + do { + let unwrapped = try result2.get() + expectEqual(unwrapped, string) + } catch { + expectUnreachable() + } + + // Test unwrapping strongly typed error. + let result3 = Result.failure(Err.err) + do { + _ = try result3.get() + } catch let error as Err { + expectEqual(error, Err.err) + } catch { + expectUnreachable() + } +} + +ResultTests.test("Functional Transforms") { + func transformDouble(_ int: Int) -> Int { + return 2 * int + } + + func transformTriple(_ int: Int) -> Int { + return 3 * int + } + + func transformError(_ err: Err) -> Err { + if err == .err { + return .derr + } else { + return .err + } + } + + func resultValueTransform(_ int: Int) -> Result { + return .success(transformDouble(int)) + } + + func resultErrorTransform(_ err: Err) -> Result { + return .failure(transformError(err)) + } + + let result1: Result = .success(1) + let newResult1 = result1.map(transformDouble) + + expectEqual(newResult1, .success(2)) + + let result2: Result = .failure(.err) + let newResult2 = result2.mapError(transformError) + + expectEqual(newResult2, .failure(.derr)) + + let result3: Result = .success(1) + let newResult3 = result3.flatMap(resultValueTransform) + + expectEqual(newResult3, .success(2)) + + let result4: Result = .failure(.derr) + let newResult4 = result4.flatMapError(resultErrorTransform) + + expectEqual(newResult4, .failure(.err)) +} + +ResultTests.test("Equatable") { + let result1: Result = .success(1) + let result2: Result = .failure(.err) + + expectEqual(result1, .success(1)) + expectNotEqual(result1, .success(2)) + expectNotEqual(result1, .failure(.err)) + expectNotEqual(result1, .failure(.derr)) + + expectNotEqual(result2, .success(1)) + expectNotEqual(result2, .success(2)) + expectEqual(result2, .failure(.err)) + expectNotEqual(result2, .failure(.derr)) + + let confusables: [Result] = [ + .success(.err), + .success(.derr), + .failure(.err), + .failure(.derr) + ] + + checkEquatable(confusables, oracle: { $0 == $1 }) +} + +ResultTests.test("Hashable") { + let result1: Result = .success(1) + let result2: Result = .success(2) + let result3: Result = .failure(.err) + checkHashable([result1, result2, result3], equalityOracle: { $0 == $1 }) + + let confusables: [Result] = [ + .success(.err), + .success(.derr), + .failure(.err), + .failure(.derr) + ] + checkHashable(confusables, equalityOracle: { $0 == $1 }) +} + +runAllTests()