Aside from the boilerplate overhead, changing error representation by successively wrapping in new sum types also has runtime overhead even if you make it implicit. I'm not sure if I'd actually recommend the following approach yet, given that Swift doesn't make it ergonomic, but an idea to explore might be to group errors together under one generic enum, using the existence or lack thereof of the generic arguments to select what set of cases are available in different situations:
protocol Flag { init() }
extension Never: Flag { init() { fatalError() } }
struct Always: Flag { init() { } }
enum MyError<Load: Flag, Compile: Flag>: Error {
case compileError(_: Compile = Compile())
case loadError(_: Load = Load())
}
By binding one or the other flag type to Never
, the associated error cases become impossible:
// If we only want to deal with load errors, we can bind Compile == Never
typealias LoadError = MyError<Always, Never>
// Or if we only want to deal with compile errors, we can bind Load == Never
typealias LoadError = MyError<Never, Always>
// If we need to deal with both kinds of error:
typealias BuildError = MyError<Always, Always>
func foo(x: LoadError) {
// We only need to handle load errors here
switch x {
case .loadError:
print("butz")
}
}
func bar(x: CompileError) {
// We only need to handle compile errors here
switch x {
case .compileError:
print("butz")
}
}
func baz(x: BuildError) {
switch x {
case .loadError:
print("butz")
case .compileError:
print("butz")
}
}
To indicate that a function only produces a subset of cases, while allowing it to be used within a computation that can also produce the other cases, it could theoretically be generic over the cases it doesn't produce:
func load<Compile: Flag>() throws(MyError<Always, Compile>) { ... }
func compile<Load: Flag>() throws(MyError<Load, Always>) { ... }
func loadAndCompile() throws(BuildError) {
try load()
try compile()
}
Unfortunately, as it stands, it looks like the implementation of typed throws
doesn't involve the thrown error type in generic type inference, and complains:
Compiler errors
/Users/jgroff/foo.swift:33:11: error: generic parameter 'Compile' is not used in function signature
31 | }
32 |
33 | func load<Compile: Flag>() throws(MyError<Always, Compile>) {
| `- error: generic parameter 'Compile' is not used in function signature
34 | }
35 |
/Users/jgroff/foo.swift:36:14: error: generic parameter 'Load' is not used in function signature
34 | }
35 |
36 | func compile<Load: Flag>() throws(MyError<Load, Always>) {
| `- error: generic parameter 'Load' is not used in function signature
37 | }
38 |
/Users/jgroff/foo.swift:40:9: error: generic parameter 'Compile' could not be inferred
31 | }
32 |
33 | func load<Compile: Flag>() throws(MyError<Always, Compile>) {
| `- note: in call to function 'load()'
34 | }
35 |
:
38 |
39 | func loadAndCompile() throws(MyError<Always, Always>) {
40 | try load()
| `- error: generic parameter 'Compile' could not be inferred
41 | try compile()
42 | }
/Users/jgroff/foo.swift:41:9: error: generic parameter 'Load' could not be inferred
34 | }
35 |
36 | func compile<Load: Flag>() throws(MyError<Load, Always>) {
| `- note: in call to function 'compile()'
37 | }
38 |
39 | func loadAndCompile() throws(MyError<Always, Always>) {
40 | try load()
41 | try compile()
| `- error: generic parameter 'Load' could not be inferred
42 | }
43 |
so we'd have to make an already weird technique even uglier to cope with dummy type parameters:
func load<Compile: Flag>(_: Compile.Type = Never.self) throws(MyError<Always, Compile>) {
}
func compile<Load: Flag>(_: Load.Type = Never.self) throws(MyError<Load, Always>) { }
func loadAndCompile() throws(MyError<Always, Always>) {
try load(Always.self)
try compile(Always.self)
}