On many programming subreddits, there are arguments about using square brackets[] for generics instead of the cumbersome angle<> brackets, particularly in Rust and Go. The most common point brought up is that expressions like foo[x]() would be ambiguous — is it a generic function call, or a use of indexing followed by calling the result? However, Nim doesn't seem to have a problem with this; the following code works as expected:
const zero = 0
let arrWithProc = [proc(): string = "Hello world!"]
echo arrWithProc[zero]()
func returnGenericArg[num: static int](): int = num
echo returnGenericArg[zero]()
Output:
Hello world!
0
So my question is, how is this possible in Nim? Could other languages take inspiration from it, or does it depend on a feature of Nim that e.g. Rust and Go don't have?
IMHO, in a meta-programming-heavy language like Nim, there shouldn't even be a special type of bracket used for generic type parameters, () is good enough. A "generic" just returns a concrete type or proc when called with type parameters at compile time. The syntax element for call parameters is parentheses. We don't use special syntax elements for compile time code in other cases, so why do it just for generic type parameters?
type
A[T: SomeNumber] = object # this is just a type constructor..
x: T
B = A[int] # called at compile time..
A(T: SomeNumber) = object # so why not write..
x: T
B = A(T: int) # this instead..
B = A(int) # or this for convenience?
let b = B(x: 2)
proc printSquared[T](o: A[T]) = # printSquared will produce a concrete proc
echo o.x * o.x # for a type parameter T at compile time..
printSquared[int](b) # like this..
proc printSquared(T)(o: A[T]) = # so why not write it like this
echo o.x * o.x
printSquared(int)(b) # it's just a call returning a proc,
# so why use brackets for the type parameter?
# we don't do that for compile-time procs
# and templates either.
Of course the type parameter in the call to printSquared is redundant because of type inference, it was kept in there for clarity.
Disclaimer: I'm not a compiler sage, there may be a good reason to use [] I never heard about.
Solitude is right, but there is another problem with square brackets together with UFCS in Nim - a.bT doesn't work, for that a new syntax extension was introduced - a.b:T so it's not ambiguou
The problem with a.b[T]() is not that it ambiguous, it isn't, but the precedence is wrong for generics, it's always interpreted as (a.b)[T]() (which is what you want for array indexing!). But you don't want to instantiate a.b.
These problems are really symptoms of the ASCII age that we never left behind.
The most common point brought up is that expressions like foox would be ambiguous
The angle bracket version of this isn't great either, in Rust you have to do foo::<x>(), and Java requires you to do Class.<x>foo(). Nim could easily enforce foo[:x]() which looks better than both of those but it doesn't have to (though it might be nice if this was allowed normally and you could lint it/a compile option enforced it). Generally if you are using a C inspired language then you should use <> because of the familiarity, since that's what people look for in those languages. But in Go for example both <> and [] are out of place. In Nim there's not much familiarity to C to be desired so you don't have to bother with <>, and it's supposed to have syntax that's not as anal as Rust's, so you can spare some syntactic clarity for not having to do foo[:x]().
Well you can call printSqured(b) and have its T inferred and that makes all the difference.
True. Two different ways around this:
proc PrintSquared(T)(o: A(T)) =
echo o.x * o.x
in analogy to the "upper-case names for types (i.e. classes of concrete instances ), lower-case names for values (i.e. concrete instances)" rule. If we don't want to rely on just identifier case, a new keyword would help:
procclass PrintSquared(T)(o: A(T)) =
echo o.x * o.x
More typing, but only in the definition.
None of these are perfect, but "() is for parameter tuples, [] is for dereferencing by index" is just too nice to lose it to generics.
As for generic types, independently of generic procs, parentheses instead of brackets could be used right away.
None of these are perfect, ...
So why change anything... ;-)
That's one of these D myths as far as I'm concerned. It's still ambiguous, consider (Y) ! x, is that a type cast plus a unary ! operator or a generic instantiation?
(Plus the syntax is ugly.)
That's one of these D myths as far as I'm concerned. It's still ambiguous, consider (Y) ! x, is that a type cast plus a unary ! operator or a generic instantiation?
it's not ambiguous.
void fun(T)(int a){}
void main(string[]args){
(fun)!double(3); // Error: C style cast illegal, use cast(fun)!double(3)
}
from the horse's mouth: https://dlang.org/articles/templates-revisited.html
There's got to be a better way. D solves it by noticing that ! is not used as a binary operator, so replacing: a<b,c> with a!(b,c) is syntactically unambiguous. This makes it easy to parse, easy to generate reasonable error messages for, and makes it easy for someone inspecting the code to determine that yes, a must be a template.
(Plus the syntax is ugly.)
that's entirely subjective, and I'll take an ugly solution (which it's not) any time over an ambiguous one.
It allows you to know at parse time that it's a generic instantiation, instead of after semantic phase. This is a big deal, as it would allow macros to know that some AST refers to a generic instantiation vs array indexing, whereas all you get now is the ambiguous nnkBracketExpr
This is totally not a big deal. There are other ambiguities that you don't care about, for example x.f can also mean f(x) and T(x) (type conversion) is often an nnkCall...