Convenience member on Result when when Success is Void

This is a tiny refinement to the std lib that to provide a convenience member on Result when when Success is Void.

Result types with a success type of Void are commonly used when no further information about the result of the operation are required – other than to know it was successful. In these cases, it is necessary to return a Result via Result.success(()).

The double parentheses can sometimes become an impediment to legibility – especially if wrapped within another set of parentheses, e.g. as an argument within a function call.

 someHandler(.success(()))

I propose a tiny addition to the std-lib to iron out the kink:

extension Result where Success == Void {
    public static var success: Result { .success(()) }
}

Which smoothes out the above example to:

 someHandler(.success)

Whilst I can't think of elsewhere in the Swift std-lib where this kind of specialisation of types with Void type arguments exists, there is prior history elsewhere in the Apple eco-system. Specifically Combine's PassthroughSubject.send() which presumably added the syntactic sugar to clean-up the SwiftUI api.

18 Likes

I understand the pitch, but isnβ€˜t the usage of SomeError? preferred over Result<Void, SomeError>?

Often it makes sense for your API to consistently return Results over chopping and changing between Optionals and Results. That's because Results are composable with flatMap, etc.

12 Likes

In lieu of an addition to the standard library, why not use the spelling that's preferred in Swift: .success(Void)? This is not the only circumstance in which ()-as-Void is less than readable, and it's precisely for that reason that Void is preferred.

1 Like

Hmmm, I thought it was the other way round: Void should be used as the return type and '()' as the argument type?

In any case, I get the following error with that approach: 'Cannot convert value of type 'Void.Type' to expected argument type '()''

2 Likes

That won't even compile here as Void is a type, while () is a value of type Void (or the typealias Void = (), which you refer as less readable).

7 Likes

Whoops, brainfart. :slight_smile:

5 Likes

+1 I have been reaching for .success(()) personally when facing this. One of my coworkers prefers the more verbose .success(Void()). I would prefer to just use this extension instead. I might even consider adding this extension in the meantime.

1 Like

This extension would make .success ambiguous absent any type context (such as a call).

1 Like

Yes, that’s true – I'd missed that.

So instead:

extension Result where Success == Void {
    public static func success() -> Self { .success(()) }
}

Which hopefully is still a big improvement on ergonomics/legibility.

 someHandler(.success())

Side note: maybe there is eventually room for a more wide-ranging, complex proposal: any function with Void parameters would have a counterpart function generated where the default ()s are specified/provided.

e.g. with Combine's PassthroughSubject.send(_:), this would automatically derive PassthroughSubject.send() when PassthroughSubject's Output type is Void. For enums, it would automatically generate the appropriate counterpart static var (as described above for Result.) Of course, if a user-defined equivalent already exists – that would take precedence.

7 Likes

This is a pretty common issue with generics and PATs, and can be hard to work around; I’d really like to see a language-level solution if one is feasible.

One way to think of it would be that for value parameters of a generic/associated type that resolve to Void, there should be a synthesized default value of ().

7 Likes

There is already this pull request but missing a proposal.

I started writing a proposal, including more improvements to the Result type.

@tcldr feel free to contribute :slight_smile:

1 Like

What's the issue with having .success ambiguous without any type context?
i.e. can you provide an example that twists the programmer's arm while coding, in order to favor .success() over .success?

Thanks John!

1 Like

I just found this thread a year later because I ran into this issue myself. Did anything ever come of this idea? I'm also curious about the answer to @ronyfadel `s question.

2 Likes

Thanks for this pitch. We are already using such extension in our project, and will be glad to see it in Swift.

  public func validate(_ text: String) -> Result<Void, Error> {
    switch luhnAlgorithm(value: text) {
    case true: return .success
    case false: return .failure(error)
    }
  }

return .success is much more readable than return .success(Void()) or return .success(())

But as @John_McCall wrote, sometimes it causes an compiler error:

static func validate<ErrorType: Error>(text: String,
                                       regEx: String,
                                       error: ErrorType) -> Result<Void, ErrorType> {
    let predicate = NSPredicate(format: "SELF MATCHES %@", regEx)
    let isValid = predicate.evaluate(with: text)

    switch isValid {
    case true: return .success // Compiler error: Member 'success' expects argument of type 'Void'
    case false: return .failure(error)
    }
  }

public func allSuccessOrFirstFailure<Failure: Error>(_ results: [Result<Void, Failure>]) -> Result<Void, Failure> {
  results.first { $0.isFailure } ?? .success // Compiler error: Member 'success' expects argument of type 'Void'
}

func processResult(_ result: Result<Void, DecodingError>) { ... }
processResult(.success) // Compiler error: Member 'success' expects argument of type 'Void'

This error reason is clear, but from the point of view of human perception return .success looks better.

I don't know if it's because the compiler got more advanced, but all the examples you posted don't emit a compilation error, at least not on Xcode 14.2.
Is there any other ambiguity caused by declaring .success without parenthesis?

2 Likes