Related to the Rich Errors proposal, will it be po...
# language-evolution
c
Related to the Rich Errors proposal, will it be possible to combine errors generically? That is,
Copy code
error object First
error object Second

val a: Int|First = TODO()
val b: Int|First|Second = foo(a)

fun <E : Error> foo(e: Int|E): Int|E|Second // ?
Will this be legal?
a
a and b definitely (the general form of types is one value type + as many errors as you want) About foo, this should be allowed. However, the design with generics is not fully finalized, so there may be some constraints imposed on it
c
Do you have a general idea of what such constraints would look like? For context, I have a DSL to declare HTTP endpoints;
Copy code
val logIn by post("/login")
    .request<LogInInfo>()
    .response<LogInResponse>()
which gives a
Endpoint<LogInInfo, LogInResponse>
. I wonder if in the future it could be extended to:
Copy code
val logIn by post("/login")
    .request<LogInInfo>()
    .response<LogInResponse>()
    .failure<InvalidCredentials>()
    .failure<AccountBlocked>()
which would give a
Endpoint<LogInInfo, LogInResponse, InvalidCredentials|AccountBlocked>
(where
InvalidCredentials
and
AccountBlocked
are both marked
error
, which I understand to be allowed?). However, this requires the ability to combine errors generically together, hence my question.
❤️ 1
m
Right now
foo
won't be supported, but the case is clear, it appears in a few of our functions as well. In general, here we need some sort of negative types to declare the type parameter
E
here doesn't contain
Second
c
What is the issue if
E
does already contain
Second
? I would expect that
Copy code
val a: Int|First|Second = TODO()
val b: Int|First|Second = foo(a)
In my mind, the signature
Copy code
fun <E : Error> foo(e: Int|E): Int|E|Second // ?
describes that the function can fail in all ways
e
can fail +
Second
. The only things
foo
can do then are to ① handle the error or ② propagage it in the return type. I don't think either cause a problem?
m
The problem is in ambiguity: one can expect that
Int | E
in the function
foo
doesn't contain the error
Second
, because it appears as a separate type in the return position. However, from the type system perspective, it's not the case and
E
can be instantiated with
Second
as well. And we either should have some general rules for inference or special syntax. It might work for a function that has a signature like
foo
, but I don't believe it's a good idea to make it work only for some special functions when in general case it will work differently:
Copy code
fun <E : Error> foo(e1: E | SpecificError1, e2: E): E = e

// call 
foo(SpecificError1 | SpecificError2, SpecificError1) 

// Constraint system for the type inference
SpecificError1 | SpecificError2 <: E | SpecificError1
SpecificError1 <: E
=>
SpecificErro1 <: E | SpecificError1 AND SpecificError2 <: E | SpecificErro1
SpecificError1 <: E
=>
SpecificError1 <: E OR trivial
SpecificError2 <: E OR trivial
SpecificError1 <: E
=> 
E := CST(SpecificErro1, SpecificErro2) := SpecificErro1 | SpecificError2
c
What is the problem here? With your example, I would expect
Copy code
foo(SpecificError1, SpecificError1)
to be legal and have the return type
SpecificError1
m
The problem that for the call
foo(SpecificError1 | SpecificError2, SpecificError1)
the return type becomes
SpecificError1 | SpecificError2
, while some may expect it to be just
SpecificError2
c
How could the return type be
SpecificError2
? The second parameter
e2: E
has the value
SpecificError1
, so
E
must include
SpecificError1
(or maybe the code could not compile)
m
Please see my comment above — that’s how our type inference engine works today
c
I understand why the return type would be
SpecificError1 | SpecificError2
, which is what your message explains and the output I expected. However, you mention that 'some may expect it to be just
SpecificError2
', and that I don't understand. If
E
was
SpecificError2
, then the second argument in your example would not compile, as the provided value is
SpecificError1
.