Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change how void parameters work #508

Open
Araq opened this issue Mar 19, 2018 · 18 comments
Open

Change how void parameters work #508

Araq opened this issue Mar 19, 2018 · 18 comments
Assignees

Comments

@Araq
Copy link
Member

Araq commented Mar 19, 2018

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.
@andreaferretti
Copy link

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?

@zah
Copy link
Member

zah commented Mar 21, 2018

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

@Araq
Copy link
Member Author

Araq commented Mar 22, 2018

@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.

@andreaferretti
Copy link

@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

@zah
Copy link
Member

zah commented Mar 22, 2018

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.

@zah
Copy link
Member

zah commented Mar 22, 2018

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

@dom96
Copy link
Contributor

dom96 commented Mar 22, 2018

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.

@yglukhov
Copy link
Member

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:
    ...

@andreaferretti
Copy link

@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

@zah
Copy link
Member

zah commented Mar 23, 2018

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
Copy link
Contributor

dom96 commented Mar 23, 2018

@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.

@andreaferretti
Copy link

That may be your preference, but almost all programmers I know use this style regularly, especially for private functions

@Araq
Copy link
Member Author

Araq commented Mar 24, 2018

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?

@andreaferretti
Copy link

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

@GULPF
Copy link
Member

GULPF commented Mar 26, 2018

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

@andreaferretti
Copy link

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)

@andreaferretti
Copy link

@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

@ringabout
Copy link
Member

See also nim-lang/Nim#20609

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

No branches or pull requests

7 participants