Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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"
]
}
142 changes: 142 additions & 0 deletions stdlib/public/core/Result.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 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<Value, Error: Swift.Error> {
/// A success, storing a `Value`.
case value(Value)

/// A failure, storing an `Error`.
case error(Error)

/// Evaluates the given transform closure when this `Result` instance is
/// `.value`, 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<NewValue>(
_ transform: (Value) -> NewValue
) -> Result<NewValue, Error> {
switch self {
case let .value(value):
return .value(transform(value))
case let .error(error):
return .error(error)
}
}

/// Evaluates the given transform closure when this `Result` instance is
/// `.error`, 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<NewError>(
_ transform: (Error) -> NewError
) -> Result<Value, NewError> {
switch self {
case let .value(value):
return .value(value)
case let .error(error):
return .error(transform(error))
}
}

/// Evaluates the given transform closure when this `Result` instance is
/// `.value`, 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<NewValue>(
_ transform: (Value) -> Result<NewValue, Error>
) -> Result<NewValue, Error> {
switch self {
case let .value(value):
return transform(value)
case let .error(error):
return .error(error)
}
}

/// Evaluates the given transform closure when this `Result` instance is
/// `.error`, 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<NewError>(
_ transform: (Error) -> Result<Value, NewError>
) -> Result<Value, NewError> {
switch self {
case let .value(value):
return .value(value)
case let .error(error):
return transform(error)
}
}

/// Unwraps the `Result` into a throwing expression.
///
/// - Returns: The success value, if the instance is a success.
/// - Throws: The error value, if the instance is a failure.
public func unwrapped() throws -> Value {
switch self {
case let .value(value):
return value
case let .error(error):
throw error
}
}
}

extension Result where Error == 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 -> Value) {
do {
let value = try body()
self = .value(value)
} catch {
self = .error(error)
}
}
}

extension Result : Equatable where Value : Equatable, Error : Equatable { }

extension Result : Hashable where Value : Hashable, Error : Hashable {
public func hash(into hasher: inout Hasher) {
switch self {
case let .value(value):
hasher.combine(value)
hasher.combine(Optional<Error>.none)
case let .error(error):
hasher.combine(Optional<Value>.none)
hasher.combine(error)
}
}
}
178 changes: 178 additions & 0 deletions test/stdlib/Result.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 value: Value? {
switch self {
case let .value(value):
return value
case .error:
return nil
}

var error: Error? {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is nested inside var value: Value?, which is why you aren't able to call result.error. IMO, it's better to just remove this extension and remove those tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed in 7fb5905. I'd like to leave the tests for now, as they're the only ones testing the unwrapping. I can remove the property extension if you think that's important.

switch self {
case .value:
return nil
case let .error(error):
return error
}
}
}
}

ResultTests.test("Construction") {
let result1: Result<String, Err> = .value(string)
let result2: Result<String, Err> = .error(.err)
let string1: String? = {
switch result1 {
case let .value(string):
return string
case .error:
expectUnreachable()
return nil
}
}()
let error: Err? = {
switch result2 {
case let .error(error):
return error
case .value:
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() }

// Kept getting enum case 'error' cannot be used as an instance member, so get value manually.
switch result1 {
case let .error(error):
expectEqual(error as? Err, Err.err)
case .value:
expectUnreachable()
}

expectEqual(result2.value, string)

do {
_ = try result1.unwrapped()
} catch let error as Err {
expectEqual(error, Err.err)
} catch {
expectUnreachable()
}

do {
let unwrapped = try result2.unwrapped()
expectEqual(unwrapped, string)
} catch {
expectUnreachable()
}

// Test unwrapping strongly typed error.
let result3 = Result<String, Err>.error(Err.err)
do {
_ = try result3.unwrapped()
} 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 .value(transformDouble(int))
}

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

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

expectEqual(newResult1, .value(2))

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

expectEqual(newResult2, .error(.derr))

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

expectEqual(newResult3, .value(2))

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

expectEqual(newResult4, .error(.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> = .value(1)
let result2: Result<Int, Err> = .error(.err)

expectEqual(result1, .value(1))
expectNotEqual(result1, .value(2))
expectNotEqual(result1, .error(.err))
expectNotEqual(result1, .error(.derr))

expectNotEqual(result2, .value(1))
expectNotEqual(result2, .value(2))
expectEqual(result2, .error(.err))
expectNotEqual(result2, .error(.derr))
}

ResultTests.test("Hashable") {
let result1: Result<Int, Err> = .value(1)
let result2: Result<Int, Err> = .value(2)
let result3: Result<Int, Err> = .error(.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()