Nim: Change how ``void`` parameters work

Created on 19 Mar 2018  路  18Comments  路  Source: nim-lang/Nim

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:

  • Deprecate void as a parameter type.
  • Deprecate explicit annotations with void.
  • Add overloads for Thread[void] and FlowVar[void] and Future[void] where required.
  • Consider to unify void with the empty tuple type.
  • Backends should optimize away the void values, ideally it shouldn't be a frontend rewrite.
RFC

Most helpful comment

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(...))

All 18 comments

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mratsim picture mratsim  路  38Comments

zielmicha picture zielmicha  路  37Comments

Araq picture Araq  路  45Comments

dom96 picture dom96  路  47Comments

dom96 picture dom96  路  30Comments