5
lorentz
3y

[LANGUAGE DESIGN]

What are some interesting / unique approaches you've seen to representing generics in syntax? The only one I know is the Java/C#/Typescript way of using a separate set of parentheses and either specifying all of them or none, and I've already experimented a bit with passing them as regular arguments which works great with autocurry but every syntax option for inference adds visual noise.

Comments
  • 0
    If I remember correctly, the Zig language has a specific set of keywords for compile-time stuff that actually 'includes' generics naturally
  • 0
    This sounds like an interesting question but can you be a bit more specific?
    What do you mean by "either specifying all of them or none"?

    Are you interested in alternative syntax or alternative concepts of how generics work?
  • 1
    @Lensflare Both, the syntax is normally picked to complement the way generics work. I'm looking for examples because my initial idea is having problems so I'd like to consider alternatives.

    "All of them or none" means that when using a Java/C#/Typescript generic you either specify all type arguments or you omit the <> and don't specify any. I'm not sure if I want to change that, but it's a limitation to be noted.

    The language in question looks a bit like Haskell with better syntax magic so operators don't end up looking like arcane symbols and user-defined macros are on par with built-in sugar.
  • 1
    I don‘t know Haskell unfortunately (but I want to learn it some day).
    C++ comes into my mind which has a unique generics system, which, as you probably know, calls it templates and makes meta programming possible to a certain degree.

    Another interesting generics system is that of Swift. The syntax is looking very familiar at the surface but has many differences compared to let‘s say C#.

    On the call side, you never specify the generic arguments explicitly. They are always inferred by the arguments and return types of the called function.
    If it gets ambiguous, the compiler asks you to specify the types explicitly where needed.

    Coupled with extensions, it‘s possible to add functions to arbitrary types for particular generic type constraints. It is called conditional conformance.
    This allows for nice things like a joined(separator: String) function that is only available on any collection type those element type is a kind of string.
  • 1
    Then there is a feature called opaque types. It is somewhat related to generics and some people refer to it as inverse generics.

    An opaque type is resolved to a concrete/specific type at compile time, but not at the call side but on the implementation side, so in the body of the function.
    This allows to have very generic looking interfaces but at the same time keeping the exact type without the need to erase it.
    The type info can be utilized for all kind of static type goodness like optimization and security.
  • 1
    @Lensflare Sorry I somehow missed your answers

    I also thought about using generics to establish relations between the interface types and then having the caller pass the types indirectly by specifying types of parameters and return values. I want this to be an option (as it usually is in languages with type inference), but I don't want it to be the *only* option because I've seen in Typescript that it's easy end up with interfaces whose boundary consists of very complex and unintuitive types. The solution is to derive all of them from a common type parameter that is expressed in an intuitive way.

    The conditionally applicable function thing is a major feature in FP and I have lots of ideas for how it could work, mostly based on Rust's Trait system.
  • 1
    For example, "joined" might be typed with a generic T such that Iterator<T, String> is defined, taking parameters of type T and String. This way the function can be called on anything that can somehow be reinterpreted as a sequence of strings. Iterator<C, T> might then be defined for any Iterator<C, U> such that Converter<U, T> is defined, and bam, you can join sequences of integers.

    Here Iterator<T, U> is some function that can take an instance of T and produce a sequence of U, and Converter<T, U> is some function that can take an instance of T and produce an instance of U. Naturally these are labels the author of the function has to assign.
  • 1
    @Lensflare As for opaque types, is this the same thing as Rust's/C++'s technique for generics/templates where they essentially use the definition to create a separate thing (function, class, whatever) for all parameter values statically?
  • 0
    @lbfalvy No, opaque types are different than templates in C++. I don’t know about Rust.

    Let me show you an example from SwiftUI

    protocol View {
    var body: some View { get }
    }

    struct Example: View {
    var body: some View {
    return (a very complex type)
    }
    }

    The "some" keyword makes it an opaque type. "protocol" is like interface.

    You can return a huge complex Type now, encoding the whole hierarchical structure of the view.

    Small example:
    Tuple<Text, Tuple<Button, Icon>>

    The good thing is you don‘t need to worry about that mega type anywhere in the code but the type is not erased. Technically, the body returns this exact type at compile time.
Add Comment