On Feb 20, 2017, at 11:14 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:
2017-02-20 18:23 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:
On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:
But that raises another concern. In a previous discussion, it was taken
for granted that Never should conform to all protocols
Do you have a pointer to this discussion? I must have missed it.
Here
<http://discourse.natecook.com/t/idea-change-noreturn-func-f-to-func-f-noreturn/1000>
is the discussion where the idea of “empty” type originated.
Some messages on the topic ended up being there
<http://discourse.natecook.com/t/idea-repurpose-void/1406>\.
This <http://discourse.natecook.com/t/idea-repurpose-void/1406> is the
earliest mention of usage of this empty type for rethrows I could find.
Some related messages are here
<http://discourse.natecook.com/t/draft-change-noreturn-to-unconstructible-return-type/1765/16>
as well.
We called this type NoReturn and meant it to be the *bottom type*, i.e.
subtype of all types, meaning that if you have an instance of NoReturn—which
can only happen in unreachable sections of code—then you can convert it to
any type. It should have worked like this:
func fatalError() -> Never
func divide(a: Int, b: Int) -> Int {
if b == 0 {
let n: Never = fatalError()
return n as Int
}
return a / b
}
I pushed the idea of replacing rethrows with Never, inspired by Haskell.
Although Haskell doesn’t have static function requirements and initializer
requirements.
Thanks for the links. I scanned through them somewhat quickly and didn’t
see anything that specifically said `Never` should conform to all
protocols. Did you see that specifically? I only saw mentions of it being
a bottom type and therefore a subtype of all types, which I think is a bit
different.
I think a big part of the confusion here revolves around the distinction
between a type `T` being a subtype of another type `U` and `Type<T>` being
a subtype of `Type<U>` (using the syntax in your metatype refactoring
proposal). I’m not an expert in this area, but I suspect that `Never` can
be a subtype of all existential types but without requiring it to actually
*conform* to all protocols. Any non-instance protocol requirements are not
available on existentials (afaik).
, because if one obtains an instance of Never (and they won’t), then
everything is possible. But now we say that Never can’t conform to
Default, because this would break its very invariant. Also it can’t
conform to any protocol with static members or initializers.
It seems highly problematic to me to say that never conforms to any
protocol with non-instance requirements.
Here is an example with instance requirements only:
protocol MakesPizza {
func cook() -> Pizza
}
extension Never : MakesPizza {
func cook() -> Pizza {
// this method will never be called anyway
burnThisComputer()
}
}
let maestroLaPizza = isHeAtWork ? validMaestro : (fatalError("something went wrong") as MakesPizza)
maestroLaPizza.cook()
In this way, Never can conform to any protocol with only instance
requirements.
Sure.
But then basically, Never trick can’t be used when we request anything
more than Error from generic error type (with static members or
initializers). So this approach turns out to be more limiting than
rethrows.
Can you elaborate here? If you require a function to throw an error type
that has non-instance requirements then you would necessarily be
restricting callers to provide a throwing function. It is not possible to
express such a function with `rethrows`. You can’t talk about the error
type at all. If you could talk about the error type and were able to
constrain it in this way `rethrows` would necessarily have to exhibit the
same behavior as the generic version. The behavior arises out of the
constraint you are applying, not the mechanism by which you forward the
type.
With rethrows approach:
protocol BaseError : Error {
init()
}
func seq<E1, E2>(f: () throws(E1) -> (), g: () throws(E2) -> ()) rethrows(BaseError)
where E1: BaseError, E2: BaseError { ... }
With Never approach, we have to create two separate functions for the
same effect, because Never does not fit in BaseError:
func seq<E1, E2>(f: () throws(E1) -> (), g: () throws(E2) -> ()) throws(BaseError)
where E1: BaseError, E2: BaseError {
// It never actually throws E1() or E2() itself, but this fact can't be reflected in the signature
}
func seq(f: () -> (), g: () -> ()) {
// repeat the body
}
That’s where loss of information (which I meantioned earlier) hurts: we
can’t apply magic and say “if E1 and E2 are Never then seq does not
throw. Because it *can* throw anyway.
Well, I’m just repeating myself, at least I gave a bit more complete
example :)
Yes, I understood the example and it’s a good one. What I’m wondering is
what benefit you actually get from this. There are two places where this
default initializer could be used:
1. In `seq` itself. But it seems highly dubious to throw an error you
know nothing about. Why does `seq` need the ability to construct an error
of the same type as a function given to it without knowing anything more
about that error type. Is there a use case for this?
2. In callers of `seq`. Why would the caller care if the error type that
`seq` can throw has a default initializer? Is there a use case for this?
In other words, why do you want to specify that the type of error that
might be thrown must have a default initializer? I can’t think of any
possible circumstance where this would be valuable.
The same question can be asked of any other static requirements. What are
the use cases? These seem highly theoretical to me. Maybe I’m missing
something, but if so I would like to see an example of how it is *used*,
not just how you would need to write an extra overload without `rethrows`.
There is a potentially more practical benefit of keeping rethrows. If a
function is declared with `rethrows` we know that the function itself does
not throw. It only throws if one of its arguments throw when it invokes
them. This is a subtle but important difference. For example, users
calling a rethrowing function know that *they* have control over whether or
not the call *actually* throws. The caller might pass a couple of
functions that *can* throw but in this particular case are known not to
throw. That could influence how the caller handles errors in the
surrounding scope.