Rethrows in method of struct with a throwing property

Hello,

I don't know, how clear the title is, but what I want is basically the following:

struct Foo {
    let maybeThrowing: () throws -> ()
    
    init(_ maybeThrowing: () throws -> ()) {
        self.maybeThrowing = maybeThrowing
    }

    func bar() rethrows {
        try maybeThrowing()
    }
}

let foo1 = Foo { print("Foo") }
foo1.bar()

let foo2 = Foo { throw Error() }

do {
    try foo2.bar()
} catch {
    print("Error")
}

Obviously, this does not compile. I'm getting the error 'rethrows' function must take a throwing function argument. Is there somehow a possibility, to initialise a struct with a function property, that may be throwing, and if it does so, then a method of that struct becomes throwing?

1 Like

No, there isn't. Since you could have a var foo: Foo, the throwing-ness would have to show up in the type of the struct somehow, so that you can't accidentally copy a throwing-Foo into a non-throwing-Foo. But there's not a way to do that today (and no obvious way to add it to the language).

Yeah I thought so. Thanks for the clarification.

It's a bit of a shame, that we are still hold back sometimes by the language features, but on the other hand, it's like you've said...there is not really a straightforward way to put such a feature in the type-system.

1 Like

There were some discussions about typed throws:

With types throws, you could use generics:

struct Foo<E: any Error> {
    let maybeThrowing: () throws E -> ()
    
    init(_ maybeThrowing: () throws E -> ()) {
        self.maybeThrowing = maybeThrowing
    }

    func bar() throws E {
        try maybeThrowing()
    }
}

var foo1 = Foo { print("Foo") } // Foo<Never>
foo1.bar()

let foo2 = Foo { throw Error() } // Foo<Error>

do {
    try foo2.bar()
} catch {
    print("Error")
}

foo1 = foo2 // error: Cannot assign value of Foo<Error> to variable of type Foo<Never>

Also note that existential type Error (aka any Error) does not conform to Error. So
for the Foo<Error> to work, we would need another feature, which is not yet implemented - Existential subtyping as generic constraint.

while there is the rule that existentials don't conform to their protocols, Error is an exception to that rule - it conforms to Error. It was changed when we added Result enum to the standard library https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md#adding-swifterror-self-conformance

protocol MyError { }
struct Foo<T> { }
extension Foo: Error where T: Error { }
extension Foo: MyError where T: MyError { }
print(Foo<Error>() is Error) // true
print(Foo<MyError>() is MyError) // false
4 Likes

Thanks for correcting.

I'm glad to learn that this piece is already working, but on the other hand hardcoding magical behaviour for built-in protocols is disappointing. I think existential subtyping models this problem better.

Looks like I've closed my MR for adding existential subtyping to the roadmap by mistake. I've created a new one and would love to continue discussion there - [Generics Manifesto] Existential subtyping as generic constraint by nickolas-pohilets · Pull Request #33552 · apple/swift · GitHub.

1 Like

Agreed, especially in a language like swift which tries to do as much as possible using normal code (we don't even have builtin types, which is awesome)

1 Like

You might be able to use overloading, as is necessary for your problem when it comes to functions.

It is terrible, yes, but I can't think of a way to avoid copying everything that uses one of the two closure options. I think a generic type is better than two manual concrete types if there's other common functionality that doesn't rely on the closure.

struct Foo<Closure> {
  private let closure: Closure
}
extension Foo where Closure == () -> Void {
  init(_ closure: Closure) {
    self.closure = closure
  }

  func callAsFunction() {
    closure()
  }
}
extension Foo where Closure == () throws -> Void {
  init(_ closure: Closure) {
    self.closure = closure
  }

  func callAsFunction() throws {
    try closure()
  }
}
let foo1 = Foo { print("Foo") }
foo1()

let foo2 = Foo {
  struct Error: Swift.Error { }
  throw Error()
}

do {
  try foo2()
} catch {
  print("Error")
}
1 Like

Actually, this is quite a genius workaround. I wouldn't have come up with that. It has some disadvantages, but hey...it's a workaround.
I still like the typed throws approach more and will bring up the discussion about Never in that thread again.

Self-conformance is limited to Swift.Error for now because it's a permanent limitation on the evolution of the protocol that we don't otherwise know how to express. When we figure out how to express it for other protocols, we can allow self-conformance then.

1 Like

Would we need it for other protocols besides Swift.Error? If the answer is yes, then I don't fully understand the concept yet.

It's not usually the right tool for most jobs, no.