-
Notifications
You must be signed in to change notification settings - Fork 23
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
[RFC] guidelines for when to use typedesc vs generics #40
Comments
This isn't true in general for Nim, since I think the strongest argument for generics is that most people are more familiar with them because of C#/Java/etc. Another argument is that there is a clear connection between type and type creation, e.g However, for some procs generics doesn't look nice syntactically. Examples: let a = new int
let b = new[int]()
let c = sizeof(int)
let d = sizeof[int]() For these single argument procs, generics introduce a lot of syntactic noise. |
For me the main distinction is that a typedesc parameter is just another parameter that can participate in overloading resolution, and I use them for ad-hoc polymorphism. I use generic parameters for generic polymorphism. An example taken from emmy is proc zero(T: typedesc[int]) = 0
proc zero(T: typedesc[float]) = 0.0
proc zero(T: typedesc[int32]) = 0'i32 and so on. I also use typedesc parameters for the cases where the type T appears in return position but not in argument position. In this case, the compiler would not be able to infer it anyway. An example taken from neo is proc zeros(n: int, T: typedesc[float64]): Vector[float64] = ...
proc zeros(n: int, T: typedesc[float32]): Vector[float32] = ... In this case the typedesc parameter is useful to disambiguate, so that I can call |
In my case, I use the generics syntax for types everywhere. This gives proc zeros[T: SomeReal](n: int): Vector[T] = ... Called with If the T is used only for return value type in a typedesc notation I would use the proc zeros(n: int, _: typedesc[float64]): Vector[float64] = ...
proc zeros(n: int, _: typedesc[float32]): Vector[float32] = ... |
why not simply |
@andreaferretti everything you mentioned in https://github.com/nim-lang/Nim/issues/7517#issuecomment-379183154 can be done with generics just as well (with simpler declarations even) proc zero[T: int]() = 0
proc zero[T: float]() = 0.0
proc zero[T: int32]() = 0'i32
proc zeros[T:float64](n: int): Vector[float64] = ...
proc zeros[T:float32](n: int): Vector[float32] = ...
updated OP with |
TLDR but I searched the word "deduced" and it doesn't appear in this thread. The biggest importance of generics is that they are automatically deduced at the call site. That serves the purpose of giving very readable duck typing, or tricks where Also today Nim doesn't appear to support partial auto deduction, if you have 2 generic parameters and you want to auto deduce one but explicit the other at the call site, it's not accepted. Allowing naming the parameters at call site would help too. (same case than normal Lastly, variadics. need. otherwise can't do a proper |
@timotheecour Your mileage may vary, but I find proc zero[T: int]() = 0
proc zero[T: float]() = 0.0
proc zero[T: int32]() = 0'i32 very odd. I did not even think to do this - I realize it can be done but it seems a little abuse to use type bounds with a concrete type. Then again, it is just a matter of preference |
No, both syntaxes would be supported. We wouldn't just force everyone to use On a side note, you use a lot of acronyms in your comments which I do not understand. I know you're writing a lot, but please try to expand them, examples that I saw include "bw", "IIUC", "IFTI", "cf" (this one I can deduce from context, but after looking it up it seems that it means "compare" which doesn't make sense to me, Wikipedia suggests "see" should be used to point to a source of information: https://en.wikipedia.org/wiki/Cf.). Of course, I must thank you for summarising the discussion. It's a great summary :)
The code below this doesn't compile. Seems that the pros/cons lists speak for themselves. The only real advantage The fact is that mixing values and types in a single list of arguments is messy and will undoubtedly lead to some strange issues. |
I'm on mobile so this will be brief. In my opinion it is dependent on usage. I use generics when my type is inferable or the same proc should be callable with the different types (not different procs with the same name). Typedesc should be used when it is meant that the user has to specify the type in order to get the correct result. As an example the An example of generics would be for different types, but which all shared say a So to sum up, generics for when the type can be inferred, typically from some input object, or the same procedure does the same for all inputs (barring possible conversions). Typedesc for when the user is meant to supply a type and/or the effect of the called procedure is not the same (again the |
@timotheecour: copy-paste issue, fixed in original post |
How my brain sees it: To me, call arguments change what the procedure does or returns, generics help with types. In the case of In the case of In the case of However, if In the case of In the case of In the case of Stuff like this shouldn't be important. Maybe some people have problems with Nim because stuff like this isn't regarded as important, but all I know is I don't care for it and I shouldn't have spent the time I did writing this comment. |
I had mentioned it but using a different name: IFTI (implicit function type instantiation) which was coined in D. I just edited OP to mention
Indeed, just filed https://github.com/nim-lang/Nim/issues/7529 ; thanks for raising this point; in fact, this is a blocker; if partial deduction isn't addressed,
feel free to open an RFC issue for that; (I can't think of a concrete use case for that but I could be wrong), so we don't conflate issues
let's not conflate issues :-) this is already addressed here https://github.com/nim-lang/Nim/issues/1019 |
I'll try to summarize my position here. At the moment, the support in the compiler for explicit generic params is very much incomplete and requires quite a lot of work to get right. Since I believe that typedesc params are superior, I don't think we should invest development time in fixing the issues, instead we should promote the use of Here are the issues: 1. The generic params don't support overloading properly.The simple reason is that they are not subjected to the same logic in proc zero[T: auto]() = 0 # handle any type
proc zero[T: int32]() = 0'i32 # provide a more specific overload for `int32` The above will fail with the error For the same reason, it would be hard to introduce default values and keyword arguments for the generic params. The compiler is just not written with the idea that overloading should be supported and the signature matching happens in a much more rudimentary fashion. 2. There is no good support for partial application of the generic paramsThis problem will be somewhat easier to solve, but keep in mind that I've known about it for several years, I had various issues with the implicit generic params that would have been easier to solve if partial application was possible, but nonetheless I never found the time to do the required refactoring. 3. The typedesc params are just better.When it comes to syntax, I think having a single uniform function call syntax that covers everything is just more elegant: var f = new Foo
var t = stream.read Transaction
var m = Matrix.identity Nim tries to feel like a scripting language and treating the types as regular values will be natural to people who have been exposed to Ruby or Python. Please note that typedesc and static params are considered implicit generic parameters. The user should be able to specify concrete values for them when obtaining a pointer to a proc featuring them: var identityProc = identity[Matrix] On a side note, the newer already supported way to define typedesc procs looks like this: proc zero(T: type int): T = 0
proc zero(T: type float): T = 0.0
proc zero(T: type int32): T = 0'i32 I plan to migrate all of the official docs to this style once it's supported for Considering all of the above, I think the guideline should simply be:
|
Why not: get rid of generics in the semantic pass. |
your post doesnt make sense to me, are you trying to present the issues generics have already or are you reasoning typedesc parameters being better with these issues? because your post was prefaced with "generics problems are not worth fixing because typedesc params are better", and to me it looks like you were trying to say typedesc params are better because they dont have those problems. yes, youre right, typedesc params have all the cool things that come with regular argument overloading that generics don't currently have. that does not mean generics don't have an idea behind them, like that it's not a good idea to mix a code generation facility with runtime arguments. in fact, in contrary to your statement, i think it's not elegant at all a thing to do that. it's more likely to confuse people coming from scripting languages than welcome them. i think this looks terrifying for a statically typed language: var t = stream.read Transaction "read transaction from stream"? to people from static languages this will look like voodoo. procedure calls should be procedural, there should be other constructs like type annotations and generics that the programmer, no matter what background, can understand only matters at compile time. compile time is something nim has, and to pander to users of scripting languages by ignoring it exists is not worth it. also i think this reads better than all other options: var t: Transaction
stream.read(t) these 2 things are very different ways of feeling like a scripting language: routes:
get "/":
resp "something" var foo = create object, Foo, "id", 3, "name", "John" |
@hlaaftana, the typedesc params are still a compile-time mechanism. When it comes to code generation, they behave exactly as regular generic params (the proc gets instantiated for each unique type). |
I know. That's why having them in the same argument list isn't a perfect idea. |
If we want to have consistent libraries, we must deprecate one of the mechanisms. Otherwise, people will always choose the mechanism they like more and this thread have demonstrated that aesthetic preferences vary. Here, I will argue that it would be much more painful to deprecate the On the other hand, deprecating the explicit generic params is easy. At the generic proc definition we can simply warn about any params that were not used within the signature: proc foo[A, B](x: B) # deprecation warning: 'A' is declared, but not used To make the solution complete, such a message should be displayed only when the proc is not already deprecated (this is what we'll do with the existing procs in the stdlib). |
+1 on this
I've said this a few times before but here goes again: in D, when there's a single template argument the analog short form syntax is:
here are some options: bar!T
bar?T
bar@T
bar~T
bar::T
bar%T
bar^T
bar<>T |
I'd just like to say that I don't really like how Also to be clear I have no problem with |
side question: just filed nim-lang/Nim#7596 ; is that a bug that can be easily fixed? if not, that makes generics more useful in that regard |
In my opinion it's bad to have an RFC for this at this point. The standard library is conservative for better or worse. Nimble packages need to take the lead and then report back what works best. I ensured I am not a fan of this recent trend of nailing down every single style aspect. Nim is not Perl but it's not Python either, differences in style are acceptable and almost inevitable in every program that has multiple authors. |
I think so.
Bugs should not be taken as an argument unless the number of bugs justifies an argument like "these bugs indicate an overly complex design". |
@Araq, my point expressed in some of the linked discussions is that this bias in the standard library is leading people to using this style in their own APIs, which is likely to create problems for them down the road (when they hit the overloading limitations). My other argument is that it's relatively easy to fix some of the current bugs, but implementing proper overloading/specialization for the generic params is a more significant refactoring that's not likely to happen anytime soon (and not necessary given that alternative solutions exists). |
Is it though? I argue that we need more evidence of this. I never needed to overload generics in the past and I am willing to bet that most users won't need to do it either. I prefer generics because there is a clear separation of types and values. I don't like the mixing that |
I often had problems with typedescs because they cannot be used for type conversions, see nim-lang/Nim#8403. Usually I would have preferred to use typedescs but I couldn't because of this limitation. I just noticed that this limitation isn't real, see the work-around in the linked issue. |
This RFC is stale because it has been open for 1095 days with no activity. Contribute a fix or comment on the issue, or it will be closed in 30 days. |
This issue keeps popping up in different places (forum, PR's, issues) [1]. Let's centralize discussion in one place specific to this question instead of scattering the discussion.
Choosing between typedesc vs generics affects how we use the APIs so having guidelines is good.
here's my understanding of what guidelines should be.
I'll keep this top-level post up to date w comments below
generics pros
with
proc foo[T](a: T)
callingfoo(1)
infersT=int
there is no such notion with typedesc, it wouldn't make sense (cf mentioned in nim-lang/Nim#3502 (comment))
that would not be possible with typedesc (short of resorting to lambda etc)
can potentially (with language support) be used with a shorthand for the common case of single template calls, as suggested here New unambiguous generics syntax (generic arguments syntax) Nim#3502 (comment) :
a.foo!int
(or whatever other symbol that would make it un-ambiguous)consistency/familiarity: that's how most (all?) languages with metaprogramming support work as mentioned here: https://github.com/nim-lang/Nim/issues/7430#issuecomment-379196859
D:
void foo(T:U)()
C++ :
template<typename T> class fun
andtemplate<> class fun<int>
and std::enable_if for more complex constraintsC#: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters:
public class foo<T> where T : U
swift: https://medium.com/developermind/generics-in-swift-4-4f802cd6f53c
func foo<T: U>()
generics cons
foo[T](x)
ambiguous (generic or array subscript depending onfoo
) , cf New unambiguous generics syntax (generic arguments syntax) Nim#3502NOTE: IIUC, the
ambiguity
mentioned is only ambiguous for the lexer (or syntax highlight), not for the compilerNOTE: this can be fixed by adding a syntax, eg
some!(T)(x)
as in D (nim-lang/Nim#3502 (comment)) orfoo[:T](x)
(nim-lang/Nim#3502 (comment)); unclear whether this will ever be changed though (lots of code would need to upgrade) EDITfoo[:T](x)
is now implementedEDIT no support for partial generic deduction yet (but see https://github.com/nim-lang/Nim/issues/7529 [RFC] partial generic deduction), which makes currently
input.to[T]
impossible, whereasinput.to(T)
worksEDIT can't be used with default values, tracked here https://github.com/nim-lang/Nim/issues/7516
typedesc pros
has advantages of behaving like a regular function parameter, eg:
int.foo
or with operator syntaxfoo int
; with generics this would not make sensetypedesc cons
read
proc to streams Nim#7481 (comment)can be hard to tell whether an argument is a type or a value, eg:
foo(foo.type, bar)
or worseEDIT can't be used at compile time, unlike generics, see typedesc[T] can't be used at compile time, unlike generics Nim#8486 ; however, a workaround is available via an intermediate generic, see that issue
same for generics and typedesc
read
proc to streams Nim#7481 (comment))proc foo[T:int|float]()
orproc foo(T: typedesc[int|float])
(New unambiguous generics syntax (generic arguments syntax) Nim#3502 (comment))proc foo[T:isSomeConcept]()
orproc foo(T: typedesc[isSomeConcept])
* neither can be used with default values, but see https://github.com/nim-lang/Nim/issues/7516, [RFC] default type parameter for generics and typedesc parameters #7516this is now fixed for typedescrecommendations ASSUMING https://github.com/nim-lang/Nim/issues/7529 is going to be fixed in near future
foo[:T](x)
syntax or D's foo!(T)(x); if possible add (nonambiguous) shorthand for common case of single template param (eg x.too!T or whatever's nonambiguous)* when typeT
is an output type (not an input type), use generic, eg:input.to[T]
question: curious whether there are cases where typedesc is truly preferred? (besides syntactic niceties of allowing UFCS and named parameters)
EDIT /cc @dom96
proposal: CT_typedesc (pending https://github.com/nim-lang/Nim/issues/7529) /
when a function is compile time only (eg no runtime parameters AND can be computed at compile time), usetypedesc
. In all other cases, usegenerics
. Eg:[1] examples of recent discussions on typedesc vs generics:
nim-lang/Nim#3502 (comment)
https://github.com/nim-lang/Nim/issues/7430#issuecomment-377412261
nim-lang/Nim#7481 (comment)
EDIT without such guidelines we end up with having both kinds of functions, eg nim-lang/Nim#7512
Add none[T]() as alias to none(T)
The text was updated successfully, but these errors were encountered: