Getting rid of the existing void parameter bugs is suprisingly hard. It also seems unsound from a type theory perspective, Scala uses a Unit type instead.
Steps:
void as a parameter type.void. Thread[void] and FlowVar[void] and Future[void] where required.void with the empty tuple type.This is great, I never understood why the need to consider void as a separate type!
Incidentally, if void becomes a type like any other, not specifying the return type of a proc can become the same as returning auto, without breaking retrocompatibility.
# used to return `void`
proc foo() = ...
# now becomes equivalent to
proc foo(): auto = ...
# which infers the type `void` anyway
This is a common source of complaint (at least among my colleagues) - why use type inference by default for variables but not for the return type of procs?
I would volunteer to fix all void bugs if this will save the type :)
Personally, I'd like to see even more applications of void. Consider implementing a generic sorted set type. The idiomatic way to do this in Nim would be to mixin a proc like cmp or < that should be responsible for comparing the elements of the set.
But what if the user wants the sort order to depend on a function with additional state? (e.g. the user may want to sort 2D points by their distance from a given anchor point)
In C++, you'll be covered because the Cmp functor type used by the set is stored as a field and it can have additional fields as necessary. In Nim, the mixed in procs must be free functions and this is not possible.
void types can solve this in backwards-compatible way. Here is how the definitions of the set can be modified:
type
SortedSet[T, ComparisonState = void] = object
elements, etc: ...
cmpState: ComparisonState # when this is void, the field is removed
proc insert(s: SortedSet, elem: s.T) =
mixin cmp
...
if cmp(a, b, s.cmpState):
...
When cmpState is void, the call to cmp in insert is resolved to a call to a normal cmp(lhs, rhs) proc, but when the state is not void, the user must supply a cmp proc taking 3 arguments:
proc cmp(p1, p2, anchor: Point2d)
var s: SortedSet[Point2d, Point2d]
s.insert(Point2d(...))
@zah That is already covered by closures. If you don't like closures because they mean GC activity, there should be a different proposal to sort it out. Functors in C++ didn't save C++ from having to add lambdas either.
@zah Nothing against void per se. I just think that it should be a type like any other - that is, no special rules should apply, even though the backend may optimize it away. In scala, there is jsut the Unit type and it has no special connotation
Well, how is it covered by closures? You'll need to have an entirely different SortedSet implementation that stores a closure as field. As I explained, the idiomatic Nim way is to mixin a global cmp proc, so the code will be different in the two implementations and I'm sure only the idiomatic version will make it in the standard library.
Can we tag the issues that motivate this with a "void" tag, so I can take a look at them?
@andreaferretti,without the special rules, my nice solution above cannot work. Another nice property of the solution is that you can use the ComparisonState param to select an alternative implementation of cmp to be mixed in (you can provide an empty tag type as ComparisonState and a corresponding cmp overload).
Incidentally, if void becomes a type like any other, not specifying the return type of a proc can become the same as returning auto, without breaking retrocompatibility.
Please no. Doing this will affect code readability considerably, I don't want to analyse the body of a function to figure out its return type.
Voidness of argument should not affect function arity, imo. @zah, your case can be solved as easy as:
type
SortedSet[T, ComparisonState = void] = object
elements, etc: ...
cmpState: ComparisonState # when this is void, the field is removed
proc insert(s: SortedSet, elem: s.T) =
mixin cmp
...
let cmpRes = when s.cmpState is void:
cmp(a, b)
else:
cmp(a, b, s.cmpState)
if cmpRes:
...
@zah It's not about issues - I am not aware of any in particular - it is about regularity of the language. In many other languages, not returning anything is the same as returning a Unit type, which does not have any special rules or behaviour. Of course, the backend can have special rules, like removing void fields in generics, but this should not surface in the frontend language.
@dom96 Nothing would change in particular - one would still be able to decalre the return type of a function, and in particular state that it is void. It is just that the case where one wants type inference (which is very, very common) would get a slightly nicer syntax.
@yglukhov +1
Well, yes. You can always erase void everywhere with when statements manually. Essentially, you'll be the doing the work the compiler is currently doing, but is it worth removing a convenient short-cut and breaking backwards-compatibility just to simplify the compiler front-end a bit? Can you give me some examples that demonstrate the unsoundness of the type?
@dom96 Nothing would change in particular - one would still be able to decalre the return type of a function, and in particular state that it is void. It is just that the case where one wants type inference (which is very, very common) would get a slightly nicer syntax.
This change is small, but it will have a massive effect. I don't want programmers to be encouraged to use type inference for this case.
That may be your preference, but almost all programmers I know use this style regularly, especially for private functions
Incidentally, if void becomes a type like any other, not specifying the return type of a proc can become the same as returning auto, without breaking retrocompatibility.
How would that work with discard and .discardable?
I don't think there would be any difference. If not specifying a return type becomes return auto, all types should stay the same (the only change is for procs that do not specify a return type - currently this means void, with this change void would be inferred anyway).
Users would still be able to discard the result of a proc, or declare a proc which does not return void to be discardable. Since this concepts currently only make sense for procs that do not return void, and the change would only affect proc that do return void, the two things are orthogonal
@andreaferretti here is an annoying case with {.discardable.}:
proc foo(): int {.discardable.} = discard
# Does bar return void or int?
# Currently it will return void, but with auto it will return int.
proc bar() = foo()
I see, I did not think of this case. (in the example, I would say it does make more sense to return int, but this breaks retrocompatibility nonetheless)
I got here from #7397. How is Deprecate void as a parameter type and unify void with the empty tuple type not a conflict?
Regarding Deprecate explicit annotations with void., except from you who obviously doesn't like it, I can't see anything objectively bad with explicit void as return type. It tells me, as the reader of my own code, that the return type of a function is decided to be nothing, in contrast to nothing, which means, I didn't decide on the return type yet, or I didn't write it yet and still have to do so. Sometimes this distinction is important to me.
How do you want to add overloads of Thread[void], FlowVar[void] and Future[void] when void parameters cease to exist?
Will the empty tuple (void) be implicitly discardable?
.
@krux02 I think you linked the wrong PR
In any case - if I understand correctly the point of this issue, but @Araq can speak for himself - this is not about deprecating void in the sense that void goes away from the language. It is about void not being special cased anymore - just letting it become a type like any other, with a single value (same as the empty tuple type). Since there is only one value, it concretely takes 0 bytes as an object field, on the stack as a function parameter and so on. From another point of view, the backend can just remove it. But the language becomes simpler not having to treat void as a special type at all
Most helpful comment
I would volunteer to fix all
voidbugs if this will save the type :)Personally, I'd like to see even more applications of
void. Consider implementing a generic sorted set type. The idiomatic way to do this in Nim would be to mixin a proc likecmpor<that should be responsible for comparing the elements of the set.But what if the user wants the sort order to depend on a function with additional state? (e.g. the user may want to sort 2D points by their distance from a given anchor point)
In C++, you'll be covered because the
Cmpfunctor type used by the set is stored as a field and it can have additional fields as necessary. In Nim, the mixed in procs must be free functions and this is not possible.voidtypes can solve this in backwards-compatible way. Here is how the definitions of the set can be modified:When
cmpStateisvoid, the call tocmpininsertis resolved to a call to a normalcmp(lhs, rhs)proc, but when the state is not void, the user must supply acmpproc taking 3 arguments: