Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions stdlib/public/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ set(SWIFTLIB_ESSENTIAL
ReflectionMirror.swift
Repeat.swift
REPL.swift
Result.swift
Reverse.swift
Runtime.swift.gyb
RuntimeFunctionCounters.swift
Expand Down
3 changes: 3 additions & 0 deletions stdlib/public/core/GroupInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,8 @@
"Comparable.swift",
"Codable.swift",
"MigrationSupport.swift"
],
"Result": [
"Result.swift"
]
}
131 changes: 131 additions & 0 deletions stdlib/public/core/Result.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//===----------------------------------------------------------------------===//
//
// 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 failure, capturing associated
/// values in both cases.
@_frozen
public enum Result<Success, Failure: Error> {
/// A success, storing a `Success` value.
case success(Success)

/// A failure, storing a `Failure` value.
case failure(Failure)

/// Evaluates the given transform closure when this `Result` instance is
/// `.success`, passing the value as a parameter.
///
/// Use the `map` method with a closure that returns a non-`Result` value.
///
/// - Parameter transform: A closure that takes the successful value of the
/// instance.
/// - Returns: A new `Result` instance with the result of the transform, if
/// it was applied.
public func map<NewSuccess>(
_ transform: (Success) -> NewSuccess
) -> Result<NewSuccess, Failure> {
switch self {
case let .success(success):
return .success(transform(success))
case let .failure(failure):
return .failure(failure)
}
}

/// Evaluates the given transform closure when this `Result` instance is
/// `.failure`, passing the error as a parameter.
///
/// Use the `mapError` method with a closure that returns a non-`Result`
/// value.
///
/// - Parameter transform: A closure that takes the failure value of the
/// instance.
/// - Returns: A new `Result` instance with the result of the transform, if
/// it was applied.
public func mapError<NewFailure>(
_ transform: (Failure) -> NewFailure
) -> Result<Success, NewFailure> {
switch self {
case let .success(success):
return .success(success)
case let .failure(failure):
return .failure(transform(failure))
}
}

/// Evaluates the given transform closure when this `Result` instance is
/// `.success`, passing the value as a parameter and flattening the result.
///
/// - Parameter transform: A closure that takes the successful value of the
/// instance.
/// - Returns: A new `Result` instance, either from the transform or from
/// the previous error value.
public func flatMap<NewSuccess>(
_ transform: (Success) -> Result<NewSuccess, Failure>
) -> Result<NewSuccess, Failure> {
switch self {
case let .success(success):
return transform(success)
case let .failure(failure):
return .failure(failure)
}
}

/// Evaluates the given transform closure when this `Result` instance is
/// `.failure`, passing the error as a parameter and flattening the result.
///
/// - Parameter transform: A closure that takes the error value of the
/// instance.
/// - Returns: A new `Result` instance, either from the transform or from
/// the previous success value.
public func flatMapError<NewFailure>(
_ transform: (Failure) -> Result<Success, NewFailure>
) -> Result<Success, NewFailure> {
switch self {
case let .success(success):
return .success(success)
case let .failure(failure):
return transform(failure)
}
}

/// Attempts to get the `success` value as a throwing expression.
///
/// - Returns: The success value, if the instance is a success.
/// - Throws: The failure value, if the instance is 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 {
/// Create an instance by capturing the output of a throwing closure.
///
/// - Parameter catching: A throwing closure to evaluate.
@_transparent
public init(catching body: () throws -> Success) {
do {
let value = try body()
self = .success(value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why split these lines instead of just

self = .success(try body())

? I'm curious, as that's something I do a lot in my code

} catch {
self = .failure(error)
}
}
}

extension Result : Equatable where Success : Equatable, Failure : Equatable { }

extension Result : Hashable where Success : Hashable, Failure : Hashable { }
171 changes: 171 additions & 0 deletions test/stdlib/Result.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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<String, Err> = .success(string)
let result2: Result<String, Err> = .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(string1, string)
expectEqual(error, .err)
}

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<String, Err>.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<Int, Err> {
return .success(transformDouble(int))
}

func resultErrorTransform(_ err: Err) -> Result<Int, Err> {
return .failure(transformError(err))
}

let result1: Result<Int, Err> = .success(1)
let newResult1 = result1.map(transformDouble)

expectEqual(newResult1, .success(2))

let result2: Result<Int, Err> = .failure(.err)
let newResult2 = result2.mapError(transformError)

expectEqual(newResult2, .failure(.derr))

let result3: Result<Int, Err> = .success(1)
let newResult3 = result3.flatMap(resultValueTransform)

expectEqual(newResult3, .success(2))

let result4: Result<Int, Err> = .failure(.derr)
let newResult4 = result4.flatMapError(resultErrorTransform)

expectEqual(newResult4, .failure(.err))
}

ResultTests.test("Equatable") {
Copy link
Contributor

@moiseev moiseev Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@jshier jshier Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, as I mostly cribbed these tests by looking at those from Optional and earlier feedback. I didn't know that existed. Can you provide an example of what they would simplify here?

In general, is there any documentation around this special StdlibUnittest API or testing requirements for the standard library?

Copy link
Contributor

@moiseev moiseev Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just thinking out loud really. These conformances are synthesized by the compiler, and should be working. Otherwise we're in trouble.

In general, is there any documentation around this special StdlibUnittest API or testing requirements for the standard library?

Unfortunately no.

Copy link
Contributor

@moiseev moiseev Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide an example of what they would simplify here?

It's not about simplification. Just that checkEquatable tests for the cases you and I might not necessarily think about, like whether == is transitive or not, or whether the implementation is correct in that == is the opposite of !=.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a use of checkEquatable, in additional to the tests that were already there. Let me know if there're any other scenarios you want tested, or if you feel the tests are now redundant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: checkHashable starts by calling checkEquatable -- so it is okay to omit the checkEquatable test if we have tests elsewhere with checkHashable.

let result1: Result<Int, Err> = .success(1)
let result2: Result<Int, Err> = .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))
}

ResultTests.test("Hashable") {
let result1: Result<Int, Err> = .success(1)
let result2: Result<Int, Err> = .success(2)
let result3: Result<Int, Err> = .failure(.err)
checkHashable([result1, result2, result3], equalityOracle: { $0 == $1 })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be useful to have a hashing check with potentially colliding Success/Failure hash encodings:

Suggested change
checkHashable([result1, result2, result3], equalityOracle: { $0 == $1 })
checkHashable([result1, result2, result3], equalityOracle: { $0 == $1 })
let confusables: [Result<Err, Err>] = [
.success(.err)
.success(.derr)
.failure(.err)
.failure(.derr)
]
checkHashable(confusables, equalityOracle: { $0 == $1 })

checkHashable verifies that all four of these cases have different hash encodings. (The original implementation of hash(into:) had an issue with this, so it's not a theoretical concern.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

}

runAllTests()