`Void` is not first-class

Void is a synonym for Nil. This is an aspect of the Crystal language which I find confusing, asymmetrical, or less than optimal. It is perfectly valid to take the value of a function returning Void and call methods upon it, as in:

def foo : Void
end
p foo.nil?

My feeling is that a first-class Void would not be a value and that it would be an error to assign it or call operators or methods upon it, and that this would contribute to code-correctness. This would also be useful in other contexts, for example declaring the top-level self to be Void would handle what seems to be single-purpose code today in a more generalized way.

I think Crystal has inherited some confusion from C, in that C void is a declaration that a function returns no value, and the C void * is pointer to untyped memory, and the reference to it can be cast or initialized into something with a value. This is worse in Crystal because references are implicit. IMO Void should be first-class and Untyped should be its own thing.

More confusing: Crystal uses NoReturn to refer to an invalid value and to mark functions as ending the flow-of-control.

1 Like

Interesting. I didn’t know Void was part of the language. I had to look it up. I think void has to do with being able to work with LLVM as enumerated in https://crystal-lang.org/api/0.35.1/LLVM/Type/Kind.html#Void .

@drhuffman12 The LLVM Void type will come up in the compiler internals, and in the inter-language-calling protocol, but it doesn’t really prevent Crystal types from having their own semantics. IMO in a perfect world the inter-language-calling protocol would use a reference to Untyped to refer to C void * and Void would only be used to mean not-a-value.

For reference, the discussion initialized in this issue: https://github.com/crystal-lang/crystal/issues/10146#issuecomment-751814470

I’m copying my reply over here:

There’s no concept of void in Crystal proper. The sole purpose of that type it to used in C bindings. If Crystal didn’t have C bindings, it wouldn’t have Void . And the C-meaning maps very well to Crystal’s Nil .

Crystal inherits Ruby’s “everything is an object”. Something that is not even a value is even less than not-an-object. I’m not saying to categorically exclude this idea, but it doesn’t really fit either.

1 Like

I understand everything is an object, but I submit that we are going beyond that to every function must return an object or maybe even every expression except raise must return an object. So, we use Nil in places where a first-class Void would be more appropriate. Nil as the sole, non-union, type of methods that return nothing mostly does the job, in that code assuming another value gets caught in method calls. This is, however, indirect and can confuse the programmer because Nil can be passed very far from the offending code before being caught. If we want to point out the error to the programmer, it’s best to catch passing the value or assigning it, directly.

Crystal has an overall problem that error messages can be very confusing due to their indirectness. This increases the learning curve, and creates resistance to the language due to a perception of the compilers opacity to newbies. Although that is worst about macros, this is another example.

It is also not clear to me that the implicit pointer used for any variable that can contain Reference is an object.

Any possible way to make using Void not possible outside bindings, or a warning (not as good)? Maybe, invalid outside fun, type, struct, alias declarations and Pointer. Or, another lint rules to add to ameba.

I’ve already seen it incorrectly used in place of Nil.

This will also fix the https://github.com/crystal-lang/crystal/issues/10146 issue indirectly.

1 Like

I opened a proposal to disallow Void for most type restrictions at Disallow `Void` type in type restrictions except for C bindings · Issue #10657 · crystal-lang/crystal · GitHub

1 Like