DIP 1017--Add Bottom Type--Final Review

Neia Neutuladh neia at ikeran.org
Wed Jan 16 07:04:37 UTC 2019


There are at least five parts to this proposal:

a. Add a way to indicate that a function doesn't return.
b. Use a special return type to indicate that.
c. Make that type implicitly convert to absolutely every other type.
d. Make it a compile error to use that type in most ways.
e. Stop running catch/finally after assert errors, throwing exceptions, 
etc. I believe this was poor phrasing instead of a real part of the 
suggestion.

The proposal should be explicit about having these parts. I feel like the 
proposal came this far with as little objection as it did in part because 
it was not clearer.

The only points that are at all defended are (a) and (b), and the defense 
for (b) was sorely lacking.

> It's necessary to be competitive with other systems programming
> languages.

If we add features merely because other languages do so, we are claiming 
that the designers of those languages are authorities to be blindly 
trusted. At that point, we can give up on the language and just use Rust 
or C++ or whatnot.

That said, it's worthwhile to examine how other languages implement 
similar features. We can see what works for them and what doesn't, and we 
can see how that might work for D.

The fact that the DIP does not have this analysis is surprising. Let me 
make up for this lack slightly.

Rust has a token called never, written !. It is not a type aside from 
syntactically. It has two functions: first, it indicates that the function 
doesn't return ("diverging", the documentation calls it). Second, it tells 
the compiler to infer the type from its later contextual usage. (Or at 
least, that's how it worked circa 2014. It might have some other mechanism 
for not showing type errors today.)

    #![feature(never_type)]
    fn temp() -> ! {
        panic!("I am panicking");
    }
    fn main() {
        let mut x : ! = temp();
        println!("x = {}", x);
        x = temp();
        println!("hello  world");
    }

Swift has a type called Never. It's used to indicate that a function 
doesn't return -- it panics or throws an exception. It doesn't have any 
implicit conversions. You can have variables of its type. You can pass it 
to functions. You can assign it to things. It doesn't support the ternary 
expression logic that this DIP proposes.

    enum HelloError : Error {
        case badName
    }
    func sayHello(name: String) throws -> Never {
        throw HelloError.badName
    }
    struct Foo {
        var never : Never
    }
    do {
        var n : Never
        n = try sayHello(name: "Todd")
        var foo : Foo = Foo(never: n)
    } catch {
    }

Kotlin has a type called Nothing. It doesn't do implicit conversions. It 
supports the ternary logic that this DIP proposes as a side effect (you 
can use throw and return in the same way). You can have variables of this 
type, pass it to functions, assign it to things, etc.

    fun foo(): Nothing {
        throw Exception("this doesn't work")
    }
    fun foo2(n: Nothing) {}
    fun bar() {
        try {
            var n: Nothing = foo()
            foo2(n)
            var n2 = if (true) { 12 } else { foo() }
        } catch (e: Exception) {}
    }

C++ uses an attribute and does nothing special with the function's return 
type.

Nim uses an attribute, likewise.

Go lets you add a documentation comment, but that's for humans, not the 
compiler. There may be internal, undocumented mechanism to tell the 
compiler that a particular function doesn't return, intended to be used 
only within the runtime; that's the sort of thing the Go team does.

I'd expect the proposal to also include the languages mentioned on the 
Wikipedia page for Bottom type that actually have a bottom type. (It has 
some egregious inaccuracies, such as claiming that Javascript has a bottom 
type named undefined.)

It seems like forbidding variables, function parameters, fields, etc of 
the no-return type would be unique for D. It's also mostly unique *within* 
D; only void works similarly. Had the author done this sort of review, it 
would have been an opportunity to notice that the proposal is unique in 
this way and reflect on the advantages and disadvantages.

> RAII cleanup, scope, finally, and catch clauses need not be generated
> for such branches.

This isn't true currently:

void main()
{
    scope (exit) writeln("scope exit");
    assert(0);
}

This prints "scope exit" as you'd expect. Destructors and catch/finally 
clauses are run too.

Is this intended as part of the proposal? If so, that would be a 
nontrivial change that would almost certainly be rejected. Or is it 
talking about code after the noreturn function? If so, that should be 
included in point 1, and it should be rephrased to clarify what "such 
branches" means.


More information about the Digitalmars-d mailing list