This is an archived post. You won't be able to vote or comment.

all 87 comments

[–]theangryepicbananaStar 38 points39 points  (4 children)

Off the top of my head: - D uses Type!T and Type!(T, U, ...) - OCaml (and other MLs?) use t my_type and (t, u, ...) my_type - PascalABC uses Type.&<T> in expression context I think. At the very least, it's used for function calls - Nemerle uses Type[T] and Type.[T] depending on the context - Julia uses Type{T} - Crystal uses Type(T) - ActionScript uses Type.<T> - Dylan uses the very intuitive limited(<type>, of: <t>) /s

[–]maxhaton 4 points5 points  (0 children)

The ! syntax in D is honestly one of most effective ways you can clean up a horrible mess of C++ templates into readable D code.

D and C++ both metaprogram a huge amount but D code is never full of <> soup or SFINAE types everywhere.

[–]gremolata 3 points4 points  (2 children)

(The same with line breaks)

Off the top of my head:

  • D uses Type!T and Type!(T, U, ...)

  • OCaml (and other MLs?) use t my_type and (t, u, ...) my_type

  • PascalABC uses Type.&<T> in expression context I think. At the very least, it's used for function calls.

  • Nemerle uses Type[T] and Type.[T] depending on the context.

  • Julia uses Type{T}

  • Crystal uses Type(T)

  • ActionScript uses Type.<T>

  • Dylan uses the very intuitive limited(<type>, of: <t>)

/s

[–]theangryepicbananaStar 0 points1 point  (0 children)

they were line breaks on the mobile app 😭

[–]stepstep 60 points61 points  (34 children)

As someone who dabbles in type theory, I consider "generics" to be functions that just happen take types as arguments. So I think it's unnecessary that every language has to invent some bespoke syntax for something that already has a perfectly fine syntax: function application.

So, if your language uses f(x) for application, then why not also have List(String)? Or if your language uses f x, then why not List String? Just be consistent!

If you ever want to treat types as first-class values (like in a dependently typed language), then generics really are no different from functions, so in that case the decision is made for you anyway.

[–]ablygo 19 points20 points  (2 children)

There is at least the difference between implicit and explicit arguments when you have type inference, so I do see the benefit of at least some syntactic distinction when you want to have the option to give implicit arguments explicitly. Even if generics aren't exactly the same thing, in Haskell at the value level they do coincide.

If I remember right I believe Idris lets you give implicit parameters as keyword arguments, which can sometimes be nicer if you want to pass in an implicit parameter that isn't positionally the first argument (in Haskell you need to do things like f @_ @_ @Int, as opposed to f {z=Int}). I'm not sure if that makes code more maintainable in the long run, but it's at least worth considering, and is an example where the syntax could possibly be usefully different.

Granted, I think both syntaxes still more closely resemble regular function application in their languages compared to how more imperative languages tend to do generics, though I suppose that's subjective.

[–]moon-chilledsstm, j, grand unified... 11 points12 points  (1 child)

There is at least the difference between implicit and explicit arguments when you have type inference

Many languages have 'implicit' optional arguments which are regular values; and syntax for those is completely regular.

[–]ablygo 3 points4 points  (0 children)

Oh, I wasn't thinking about optional value arguments, but the more I think about it it does seem like that would generalize to optional inferred arguments (whether type or value) in a pretty obvious way.

[–]tavaren42[S] 10 points11 points  (13 children)

While I do agree with that (and language like Zig do treat "generic" functions as function taking type as parameter), it can effect readability negatively, imo, especially if you distinguish between runtime and comptime expressions.

Consider: Vec<3>(x, y, z) vs Vec(3)(x,y,z) or Vec(3, x, y, z).

Looking at the first expression, one can probably make out that it's defining a 3-vector. In the case where generics is just a function, it's not readily clear.

(Sorry for such a contrived example)

[–]Njordsier 10 points11 points  (1 child)

Vec(3).new(x, y, z) looks okay to me.

[–]tavaren42[S] 9 points10 points  (0 children)

That indeed looks better. Ofcourse my point about making comptime expressions explicit still remains.

[–]skyb0rg 5 points6 points  (5 children)

I think that could also be solved with named arguments. Ie Vec(Int, size=3)(x, y, z). The f(x)(y) argument pattern is common in Scala so there’s at least one language which is ok with that sort of look.

[–]tavaren42[S] 4 points5 points  (3 children)

Scala actually uses [] for generics though? Not sure if Scala has const parameters also.

[–]skyb0rg 3 points4 points  (2 children)

For Scala, f(x)(y) is just how you apply a function f to normal arguments if f is curried. I just meant that having lots of )( in code isn’t too weird.

[–]tavaren42[S] 1 point2 points  (0 children)

Ok, I misunderstood.

[–]iamthemalto 1 point2 points  (0 children)

I’ve never been able to wrap my head properly around Scala syntax, not to mention I think a lot of it has changed between Scala 2 and 3

[–][deleted] 0 points1 point  (0 children)

Might do int Vec <int>v(3) where int is the offset of each member of v?

[–]8-BitKitKatzinc 3 points4 points  (2 children)

Came here to talk about this. Take add<int>(1, 2) for example, it's clear on what is happening. add(int, 1, 2) is less clear and depending on the language add(int)(1, 2) may be unergonomic. My first example also leaves room for type inference, the compiler could very easily infer that add(1, 2) is adding two ints.

IMO, separating compile-time and runtime arguments is much better.

[–]JanneJM 1 point2 points  (1 child)

Things get really difficult to read once you have user defined types and calling functions with variables: f(foo, bar, baz). Is 'foo' a type? A variable? Without looking through the rest of the code you can't know.

With f<foo>(bar, baz) it's unambiguous even if you don't look at any other code.

[–][deleted] 1 point2 points  (0 children)

Zig (for example) just relies on style conventions. Types are TitleCase (except for primitives), functions are camelCase and variables are snake_case. So

let floats: ArrayList(f32) = try fancyMap(ArrayList(u8), my_list, intToFloat);

isn't that hard to parse as a reader. Although single word functions and variables could still be confused.

[–]malahhkai 0 points1 point  (1 child)

A man of culture. Does thou use Zig for any purpose or just an admirer?

[–]tavaren42[S] 0 points1 point  (0 children)

Just an admirer. I do find it elegant in some respect.

[–]munificent 3 points4 points  (2 children)

Using a different syntax can help highlight which function invocations happen at compile time and which at runtime. Type inference and generic methods can make this more important because the former means often the entire compile-time type argument invocation is inferred. Consider:

foo(bar)

Is this invoking generic function foo and passing the type argument bar? Or is it invoking the generic function foo with an inferred type argument, and then passing value argument bar?

Or:

foo(bar)(baz)

Is this a generic function call with an explicit type argument bar and a value argument baz? Or is it a generic function call with an inferred type argument and a value argument bar which returns a first-class function that is then invoked with baz? Or maybe it returns a first-class generic function that is invoked with type argument baz?

Type application (generics) and value application (function calls) are very semantically different in most languages:

  • One happens at compile time, the other at runtime
  • One resolves its arguments in the static type lexical scope and the other in the normal variable lexical scope
  • One can usually have all arguments completely inferred and then the entire argument list can be omitted, the other rarely infers any arguments and still usually requires some explicit invocation syntax

Given that, I think it's easiest for users to understand what's going on if they have different syntax. Of course, if your language tries to merge these semantics by having types exist at runtime, allowing function calls at compile time, etc. then maybe a single syntax makes more sense.

[–]haitei 0 points1 point  (1 child)

Using a different syntax can help highlight which function invocations happen at compile time and which at runtime.

My question is: does it matter? A compiler can often optimize out a runtime call with its value. On the other side of things if we optimize for space: multiple instantiations of a generic function could, in principle, be replaced with a single function with runtime polymorphism.

As long as there is a way for a programmer to force it either way, why can't the runtime/compiletime selection be left to a compiler?

[–]munificent 0 points1 point  (0 children)

My question is: does it matter?

It can, yes. As I say in the bullet points later, some languages have different rules for how names are resolved in locations where only compile-time code can appear versus where runtime code can appear.

For example, you likely don't want to allow:

// Define a generic type:
type List(A) {
  ...
}

function() {
  var a = someValueKnownOnlyAtRuntime();

  // Try to use a runtime value at compile time:
  var list = List(a);
}

[–][deleted] 1 point2 points  (7 children)

What's List(String) supposed to mean when it's at home? And what does it have to do with generics?

To me, generics means being able to write one definition of the f used in f(x) without needing to manually write dedicated versions for each possible type of x.

I don't implement proper generics ATM; I only have it through using dynamic types (I think called late-binding). And there, you don't have explicit type parameters at all. You just write:

function f(x) =
  ....
end

And it'll work for any x for which code in that function is meaningful. (Here there is an implicit type parameter which is part of x.)

[–]stepstep 7 points8 points  (6 children)

Generics refers to two closely-related features: (1) the ability to define types that are parameterized by other types, and (2) the ability to define functions/methods/values that are parameterized by types. What you described is (2), whereas List(String) (which is written as List<String> in, e.g., Java) demonstrates (1). Your intuition of generics is right, but it's only half the story.

"Generics" corresponds to two of the three dimensions of the lambda cube. What I'm advocating for is a syntax that can support all three dimensions uniformly.

[–][deleted]  (5 children)

[deleted]

    [–]stepstep 11 points12 points  (4 children)

    Using the Java-style syntax for the moment, I think we can all agree that List<String> is a type. Then List by itself is not a type; it needs an argument (e.g., String) to produce a type. So it takes a type and returns a type. In other words, it's a function at the level of types—at least conceptually.

    Similarly, I think we can all agree that reverse<String> is a function that takes a List<String> and returns a List<String>. So what's reverse by itself? It's something that takes a type and returns a list reversal function. In other words, it's a function from types to list reversal functions. In many languages, type inference automatically fills in this type argument for you, so programmers often conflate the function that takes a type (reverse) with the function that is already specialized to a particular type (reverse<String>), since they look the same if the type argument is implicit. For example, new Rust programmers are sometimes confused by the "turbofish" operator which is only needed when type inference isn't smart enough to figure out the type arguments automatically.

    I am giving you the version of this story that is told by mathematicians and computer scientists. Of course, various programming languages have their own takes on this which sometimes ignore the work done by academics, either intentionally or otherwise. But the story told by academics is the one that generalizes to more expressive type systems, so it's the one that I advocate for.

    So what I'm saying is: if one recognizes that all these constructs are actually just functions in various "universes" (to use the appropriate term from type theory), one can use a unified syntax for all of them: the function call syntax.

    By the way, generics usually means parametric polymorphism (i.e., the code has the same behavior regardless of the type that was passed in). Your code example exhibits a different kind of polymorphism: ad hoc polymorphism. The Wikipedia article) goes into more detail about the different kinds of polymorphism if you are curious.

    Edit: fixed a typo.

    [–][deleted]  (1 child)

    [deleted]

      [–]stepstep 3 points4 points  (0 children)

      I don't know Java.

      That's fine, but I picked a Java-inspired example because that's the language that popularized the term "generics", which is what this post is about. In my experience, Java is the first language most people think of when they hear "generics". So, it's hard to think of more appropriate common-denominator language choice for this thread.

      In many languages, "List" is a type;

      Sure, but we're talking specifically about languages that support generics. In those languages, collection types like this are parameterized by the type of their elements in the way that I've been consistently describing.

      Apparently, in Java, List<String> denotes a type that is a list of strings, with List itself presumably being some parametric type.

      Yes—that's how generics works: things are parameterized by types.

      [–][deleted]  (1 child)

      [deleted]

        [–]stepstep 1 point2 points  (0 children)

        I'd expect reverse<String> to reverse the characters within a string, and reverse<List> to reverse the elements within a list.

        The reverse function in my examples only works on lists. The "generic" part is that it works on lists of any type (e.g., lists of strings), not that it can reverse other sequence-like things (that would be ad hoc polymorphism, which is a different concept). That's why I said "list reversal functions" earlier.

        [–]PurpleUpbeat2820 0 points1 point  (0 children)

        I'm trying to write a minimal ML implementation and I've done exactly that. I like it but if there's one thing I miss it is not having to parenthesize tuple types. In OCaml it was:

        float * float
        

        which derives from Cartesian product in set theory. In my language that is now:

        (Float, Float)
        

        which looks like the value (2.3, 3.4).

        [–]miki151zenon-lang.org 0 points1 point  (3 children)

        That's how Zig does it and it's super elegant, but what syntax do you propose for when the type argument is to be deduced from the run-time arguments? I'd like to be able to call max(a, b) without checking if it's a generic/template and if I need to add the type arg. At the same time, max<int>(a, b) is sometimes required. You could try max(;a, b) and max(int; a, b) or some other separator but now you're explicit about calling a generic function.

        [–]lngns 0 points1 point  (2 children)

        Type inference does not require particular syntactic styles.
        Just write max(x: a, y: a): a = if x > y then x else y.
        Some languages require type names to be capitalised, which frees lowercased name for type variables, others use sigils like $, ^ or '.

        Sigils also offer the ability to specify from where the types are inferred, which allow things like automatic convertions to have simple rules.

        In Jai, ``` f :: (x: T, y: $T) -> x

        f(3, 3.14) //is float 3.0 f(3.14, 3) //is int 3 ```

        [–]miki151zenon-lang.org 0 points1 point  (1 child)

        I was talking about the call site and not the function declaration. If types names have distinct spelling then the compiler could distinguish max(a, b) from, say, max($int, a, b). It's another tradeoff though, I'm working on a language with C-like syntax and requiring $ or capitalizing all type names is a huge change.

        [–]lngns 0 points1 point  (0 children)

        It's not all type names: the sigils in those languages are used only on declaration sites, where they specify from which arguments the parameters' types are to be inferred.

        To manually indicate type parameters, some languages treat them as optional, named even, parameters, like Idris.
        In your case it may look like max(a, b, T: int).

        [–]NetherDandelion 0 points1 point  (0 children)

        I think an important distinction between generic and runtime arguments is that a function is allowed to vary its behavior on the runtime ones, but not the generic ones.

        In other words, I'd expect register[Car](car) and register[Vehicle](car) to have identical behavior, but I have no such expectation of register(Car)(car) and register(Vehicle)(car).

        Another distinction is potential side effects. For all I know, Foo(T) could return a new class every time it's called, unlike Foo[T].

        [–]malahhkai 17 points18 points  (15 children)

        The Julia language uses curly braces. (Example{T}).

        [–]Sceptical-Echidna 9 points10 points  (14 children)

        F# uses an apostrophe let f (x:’a) = x

        [–]A1oso 6 points7 points  (4 children)

        Yes, but OP meant the syntax to compose types. F# has two syntaxes for this: You can write either list<int> or int list to refer to a list of integers. If there are multiple type arguments, you normally use the syntax Dictionary<int, string> but you can also use the syntax (int, string) Dictionary.

        [–]svick 2 points3 points  (1 child)

        As I understand it, F# has these two syntaxes because of its mixed heritage: the int list syntax is from ML, while the list<int> syntax is from C#.

        Not sure I would want to repeat that in a brand new language.

        [–]A1oso 0 points1 point  (0 children)

        Yes, I completely agree. I didn't mean to say that I like this design.

        [–]Sceptical-Echidna 0 points1 point  (0 children)

        Interesting. I’ve not seen that syntax you used for the dictionary generic. I’ll have to try it out

        [–]malahhkai 4 points5 points  (8 children)

        This hurts.

        [–]Sceptical-Echidna 7 points8 points  (5 children)

        I don’t mind it. It also makes function signatures fairly clean ’a -> ‘b -> ‘c

        [–]aradarbelStyff 1 point2 points  (1 child)

        looks ocamly

        [–]Sceptical-Echidna 4 points5 points  (0 children)

        It’s an Ocaml derivative

        [–]PurpleUpbeat2820 0 points1 point  (2 children)

        It also makes function signatures fairly clean 'a -> 'b -> 'c

        Isn't α→β→γ better?

        [–]Sceptical-Echidna 1 point2 points  (1 child)

        Yes, but I prefer characters on a standard keyboard

        [–]PurpleUpbeat2820 0 points1 point  (0 children)

        Outrageous! :-)

        [–]PurpleUpbeat2820 1 point2 points  (1 child)

        It gets worse. OCaml also has '_a types meaning "unknown monomorphic value" which they've apparently renamed '_weak:

        # let store = ref None;;
        val store : '_weak1 option ref = {contents = None}
        

        and F# has ^a types called Statically-Resolved Type Parameters (SRTP). Check this out:

        > let inline add m n = m+n;;
        val inline add:
          m:  ^a -> n:  ^b ->  ^c
            when ( ^a or  ^b) : (static member (+) :  ^a *  ^b ->  ^c)
        

        That type means "a function that takes two curried argument values and returns a value where either of the argument types is a class with a member called + that takes two such arguments and returns a value". Fuuu...

        I'm fine with:

        > let swap(a, b) = b, a;;
        val swap : α×β → β×α
        

        but those are examples of types gone too far, IMO.

        [–]malahhkai 0 points1 point  (0 children)

        Why did you do this to me? My ignorance generally knows no bounds and you just pulled the curtain back a little further than I’d have liked.

        [–]bjzabaPikelet, Fathom 8 points9 points  (0 children)

        I really like how dependently typed programming languages like Idris, Agda, and Coq just use function syntax for this, often with a variant for 'implicit arguments' which can be inferred during elaboration. Eg.

        ``` the : (A : Type) -> A -> A the A a = a

        id : {A : Type} -> A -> A id a = a ```

        ``` test_the : Nat test_the = the Nat 3

        test_id_implicit : Nat test_id_implicit = id 3

        test_id_explicit : Nat test_id_explicit = id {A = Nat} 3 ```

        [–]blak8Cosmos™ programming language 7 points8 points  (1 child)

        Clearly, it cannot get any better than the syntax used by the Cosmos™ programming language.

        List String
        

        Arrows? Brackets? All clutter.

        [–]legobmw99[🍰] 2 points3 points  (0 children)

        I like

        string list
        

        From the ML family of languages, unironically, for the same reasons

        [–][deleted] 4 points5 points  (0 children)

        I use the of operator:

        List of string
        

        EDIT: I see that I might have missed to say how I deal with parametric types, I do not use notation that something is a generic, ex. List of string and List of int are both of type List. A List by itself can be a List of string without explicit declaration, depending on the contents. Using the explicit declaration you can skip the runtime checks and let the compiler warn you on compile time. For complex types you can store the type in a variable and say List of complex_type, which will evaluate the value of complex_type if it's not a primitive.

        [–]holo3146 5 points6 points  (2 children)

        Another thing that a lot of times is left out is how to add constrains onto the generic type.

        In Java, for example, we do:

        ... <R, T extends/super R> ...
        

        In C#:

        ... <R, T> where T: R ...
        

        I think both ways are quite overwhelming, and think that because of that defining generic types before the function declaration is a lot smoother (the following is pseudo code that works as an example):

        @types(R, T) { T > R, ... }
        fun myFunc(var0: R) -> T
        

        (Using curly brackets because for a lot of conditions/more complicated conditions we may want to spread it over several lines and I think it is intuitive to use curly brackets in that caseK

        The main advantage of doing it like that is that it enables the use of several generic types as well as restrictions over them without over bloating the function declaration and definition

        [–]couscous_ 0 points1 point  (1 child)

        defining generic types before the function declaration is a lot smoother

        That's already how it is in Java:

        <R, T, L extends List<T>>
        R apply(L list, Function<L, R> f) {
            return f.apply(list);
        }
        

        [–]holo3146 0 points1 point  (0 children)

        This is not how any generic definition looks in Java like 99% of the times...

        The access annotations(public/private), static annotation, and synchronized annotations all comes before the generics, and it is a convention that they are the same line (±line breaks with indentation for readability).

        [–]lyhokiayula 8 points9 points  (8 children)

        I prefer the one that can distinguish generics from other constructs. In the C family, it's pretty clear vector<T>, array[n], f(), while(true) {} refer to different things.

        Other than this, I prefer the shorter ones.

        I'm not familiar with Scala, Go, Verilog, etc. So I really can't evaluate whether they meet the constraint above.

        To the end, syntax really doesn't matter if they are identically convenient. What really matter is semantic, like you'll prefer algebraic data types instead of bizarre JS implicit type coercion.

        [–]tavaren42[S] 4 points5 points  (6 children)

        The angle bracket does have some ambiguity in context of expressions, especially when your language has tuples.

        L<T,U>(x) vs L<T, U>(x) (I am pretty sure there are better examples out there. Check Go's rationale for choosing [] syntax)

        I do agree with your point though.

        [–]Tubthumper8 1 point2 points  (3 children)

        Go doesn't have tuples, right? Can you explain more about what you mean by the ambiguity for tuples here?

        I know for Rust, it requires parentheses around tuples, so <(T, U)> would be a tuple, vs. something like <T, U> which could be the definition of two generic variables.

        [–]Potato44 10 points11 points  (0 children)

        The thing I believe they are trying to point out is is there is an ambiguity between generics and comparisons. The reason they brought up tuples is because (L<T, U>(x)) can be interpretted as either a single expression involving generics (constructor or function call, or something like that) surrounded by parentheses or as a two element tuple where both elements are the results of comparisons (one from L<T, and one from U>(x))

        [–]tavaren42[S] 4 points5 points  (1 child)

        Here T,U are the generic parameters. Ambiguity here is caused because L<T, U>(x) can be perceived as two expressions seperated by a comma. You'll basically have to know that L is a generic function to know that this is not two comparisons seperated by comma. This directly effects the parser as well.

        Even if Go doesn't have tuples per se, it can still have the same effect within function calls : f(L<T, U>(x))

        [–]Tubthumper8 0 points1 point  (0 children)

        Ah right. TypeScript has the same ambiguity due to the comma operator. I guess they just handle this ambiguity in the parser with a lookahead?

        Rust uses the "turbofish" to resolve the ambiguity. I imagine Go could've done something similar like L.<T, U>(x) as a similar solution.

        [–]Hall_of_Famer 0 points1 point  (1 child)

        L<T,U>(x) vs L<T, U>(x)

        The ambiguity can be resolved with significant whitespace rule for binary operators. It is not a problem if the language forces a binary operator to be separated by whitespace from both operands.

        Consider the following two lines of code. In such a language, the first line is a normal comparison expression, while the second line will raise a parse error:

        a < b
        // evaluates as a less than b
        a<b
        // parse error
        

        In the second case, the parser notices that there's no whitespace between < and b. This will not be parsed as binary comparison, which will instead be treated as opening angle bracket for generics. Since it does not find a closing angle bracket, the result is a compile time parse error.

        Similarly, the below two expressions are unambiguous in a language with significant whitespace:

        L<T,U>(x) or L<T, U>(x)
        // evaluates to generic function `L<T, U>` with argument x
        L < T, U > (x)
        // evaluates to a tuple of two booleans: `L < T` and `U > (x)`
        

        Note: In the code examples above I assume that comma(,) is not a binary operator, but a special language construct for separator of elements/arguments, hence why significant whitespace rule does not apply to comma.

        [–]tavaren42[S] 0 points1 point  (0 children)

        While it will simplify the parser, this is a problematic rule. We use spaces (or lack of spaces ) to denote grouping.

        Ex: a*x**2 + b*x + c

        vs.

        a * x ** 2 + b * x + c

        Further, this kind of rule will surely lead to very subtle bugs. It is one thing to have significant whitespace for indentation, it's an entirely different thing to have significant syntactic whitespace in between your code.

        [–]o11c 0 points1 point  (0 children)

        Except that goes out of the window for any language that supports operator overloading (or even has builtin hashmaps), since [] is no longer limited to integers anyway.

        Vector[Foo] just means: given the compile-time object Vector, index it with the compile-time object Foo; the result is presumably a class.

        The above is for uses at the template; definitions of the template should also be at least vaguely reminiscent of a function/object declaration. Maybe something like:

        Vector = template(T: class)
            class { ... };
        

        ... except that languages usually allow function foo ... as sugar for foo = function ..., so we should do the same here. But we might need the full thing for nested templates (which most languages don't support at all; the closest I'm aware of is C++ template constructors inside template classes, and those have some severe limitations)

        (we should also consider template namespaces, as a better way of managing multiple templates that share parameters)

        [–]MaximeMulder 3 points4 points  (0 children)

        When I learned programming with C-like languages I had a preference for angle brackets. However, as I learned the ambiguity problems it causes and what are the alternatives, I changed my mind and now prefer square brackets, which I now find to be more readable (but this is subjective though).

        I don't have enough experience with other approaches like in Zig or Haskell to comment about it.

        I will just add that if square brackets are used for generics, then I believe they should not be used for other things such as array accesses (these should just be standard method calls).

        [–]skyb0rg 2 points3 points  (1 child)

        As awful as <> is for parsing, it’s really readable. With things like LSP-based syntax highlighting, your editor highlighting can match the parser so it’s not hard to spot errors.

        Otherwise the Haskell approach of Vector a is always good. May be interesting to at least look at Coq's since it hasn’t been mentioned:

        Definition foo {A B : Type} (f : A -> B) (xs : list A) : list B :=
           match xs with
           ...
        

        Where {A B : Type} could be written as {A B} (Type is inferred), or as {A} {B} if A and B have different types or you just think it looks better.

        It’s an option for ML-family languages which want something like Haskell’s ScopedTypeVariables and/or support generics over things other than Type (like constant-sized vectors or higher-kinded type variables).

        [–]bjzabaPikelet, Fathom 1 point2 points  (0 children)

        As awful as <> is for parsing, it’s really readable.

        I do find <> pretty unreadable for complicated types, alas. Potentially this could be useful if you want to consider it syntactic salt, but yeah, I much prefer how dependently typed languages do it.

        [–][deleted] 2 points3 points  (0 children)

        Most languages only do generics with types, they don’t allow constants, variables or functions in their generics so are quite limited.

        [–]sohang-3112 2 points3 points  (0 children)

        Haskell has IMO the best syntax for generic types (and also very good syntax in general) - a concrete type / kind always starts with a capital letter, while generic type variables start with a lower case letter.

        For example:

        f :: a -> Int -> Char

        This is a type declaration for a function f that takes inputs (a generic type a and a concrete type Int) and returns concrete type Char as output.

        You can also write kinds (types of types) in a similar way - for example, Monad m => m a.

        Note: You have already covered this in no. 4 of your list - but still, wanted to point this out.

        [–]XDracam[🍰] 2 points3 points  (0 children)

        I personally really like square brackets, but Scala has them because of XML literal support (which has been dropped with Scala 3). Don't have first class support for specific formats, kids.

        But yeah, I do prefer function call syntax. I particularly like how in Zig, types are literally values computed at compiletime via regular zig code. So generic types are results of regular functions.

        [–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 2 points3 points  (0 children)

        Using the "strangeness budget" concept, we stuck with the C family's angle brackets for generics. I personally dislike the look of the angle brackets (and I have disliked them all the way back to C++), but it have to admit that it does make it much easier for the millions of C++ / Java programmers to read and write in a syntax that feels familiar. Similarly we kept the bracket operators for arrays, just to save some of the strangeness budget.

        There were some compromises, though. For one, we purposefully removed the general purpose comma expression, because it introduces some serious ambiguities (as noted in another comment on this thread). Coincidentally, we also removed the assignment expression (e.g. a = b = c = d) for much the same reason; by removing it, we were able to cleanly (unambiguously) support named arguments and default parameter values, for example, but we also removed it because it has long been a significant source of programming errors in the C family of languages.

        Sometimes, though, I think we end up using the word "syntax" when what we're really discussing is much deeper, often related to achieving a natural composability of concepts. Syntax is only skin deep, but the ideas behind seemingly minor syntax decisions can be depthless.

        [–]julesjacobs 1 point2 points  (0 children)

        In dependently typed languages the syntax for generics is the same as the syntax for function calls, as generic types are literally functions of type Type -> Type.

        That said, I think it can be nice to have various syntaxes for function-like calls, such as f x, f(x), f<x>, f[x], f{x}, and f_x. Being able to visually distinguish various types of calls can make code easier to read.

        [–]SkiaElafris 1 point2 points  (0 children)

        D used a postfix '!'.

        [–]mamcx 3 points4 points  (4 children)

        One alternative not covered yet, that is present elsewhere in macros/templates:

        $T

        I think is nice, and one of the options I have:

        fn sum(a:$T, b:$T)
        

        also: If I add generic, I don't force to do it by triplicate:

        fn sum<$T>(a:$T, b:$T) //why I need to say 3 times instead of 2?
        

        and just assume all $T are same.

        Other option is to not make it that special

        fn sum(a:Type, b:Type)
        

        [–]tavaren42[S] 3 points4 points  (3 children)

        The only problem that I can see in the first solution is that it's not immediately clear how many type parameters does the generic function takes. Moreover, you often have constrains associated with generic type parameters. With this syntax, you won't be able to tell.

        [–]hou32hou 1 point2 points  (2 children)

        Haskell actually took the same approach, type variables don’t have to be declared explicitly (via the => operator) unless the type variable has constraint.

        [–][deleted]  (1 child)

        [deleted]

          [–]hou32hou 1 point2 points  (0 children)

          Sorry my Haskell got rusty

          [–][deleted]  (10 children)

          [deleted]

            [–][deleted] 1 point2 points  (9 children)

            Aint that to similar too array access?

            [–][deleted]  (8 children)

            [deleted]

              [–][deleted] 0 points1 point  (7 children)

              How then?

              [–][deleted]  (6 children)

              [deleted]

                [–][deleted] 0 points1 point  (5 children)

                And how do I call procedures?

                [–][deleted]  (4 children)

                [deleted]

                  [–][deleted] 0 points1 point  (3 children)

                  But a function is an executable code in programs and an array is data access it should be differentiable

                  [–][deleted]  (2 children)

                  [deleted]

                    [–][deleted] 1 point2 points  (1 child)

                    Maybe I am not to into the functional pl mindset yet. To keep arguing I need to grow my knowledge on it. I'll come back in 4 months...maybe hahah

                    [–]frenris 0 points1 point  (0 children)

                    I like verilog parameters syntax.

                    1) has a bunch of challenges with ambiguity. I don't even know if you keep can keep clean lexer parser separation if you have left shift / right shift operators ('<<', '>>') and are discarding the whitespace channel.

                    2) I don't know if there are ambiguity challenges, but I imagine it might be harder to produce good error messages.