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

RFC: Generic generic parameters #5

Closed
bpr opened this issue Feb 11, 2016 · 14 comments
Closed

RFC: Generic generic parameters #5

bpr opened this issue Feb 11, 2016 · 14 comments
Assignees
Labels

Comments

@bpr
Copy link

bpr commented Feb 11, 2016

Nim doesn't allow parameters of generic to be generic themselves. The lack of this feature has come up a few times, sometimes in the context of implementing something from functional programming like a
flatMap or monadic API, and also as with C++ in parameterizing containers. The usual workaround in the
absence of this feature is to pass in only instantiated types to generics.

This feature exists in C++, D, and Scala, and it is one of the highly desired missing features from Rust, see 'higher kinded types', or HKT, where it's perhaps more necessary than in Nim since Rust uses algebraic data types for errors rather than exceptions. Having this, variadic generics, and working static[T], and concepts will close the gap between Nim and C++ 17.

@vegansk
Copy link

vegansk commented Feb 11, 2016

👍 for HKT!

@Araq
Copy link
Member

Araq commented Feb 29, 2016

Note that higher kinded types are not as required since you can simply underspecify generic constraints in Nim (Nim's generics are close to C++ templates in this regard).

@bpr
Copy link
Author

bpr commented Mar 1, 2016

The feature, while not required, is consistent with (my view of) the Nim way, works especially well with concepts, and increases the expresiveness of the type system. That you can get around their omission by specifying less strikes me as making a virtue of a necessity. Let's take the simplest example in pseudo-Nim

type
  Array[T] = concept a
    a.len is Ordinal
    get(a, Ordinal) is T
    for value in a:
      type(value) is T

  Map[K, V, A: Array[T]] = object
    keys: A[K]
    values: A[V]

The meaning is clear, and the variations on this (allowing another generic array param to allow different arrays to back keys and values) are straightforward.

@Xe
Copy link

Xe commented Mar 1, 2016

Isn't this already the case?

@vegansk
Copy link

vegansk commented Mar 1, 2016

@Xe, no. The previous example is written in pseudo-Nim. You can't implement Map like this in real Nim. How I wish to be wrong, but I can't find any solution.

@bpr
Copy link
Author

bpr commented Mar 1, 2016

@Xe, the issue is not concepts, but "generic generic parameters", that is, using uninstantiated generics as the parameters of a generic, as A is in Map in the example. If you fix the syntax, the compiler will tell you: 'Error: no generic parameters allowed for A'.

Concepts make this feature much nicer.

@zah zah self-assigned this May 13, 2017
@mratsim
Copy link
Collaborator

mratsim commented Sep 29, 2017

I would love that.

I have a base method declared with a generic type TT (TT is expected to be Container[T])

But in some implementations I need to get access to the T subtype of TT for proc like newSeq[T](foo) to return a new container.

@metagn
Copy link
Contributor

metagn commented Apr 5, 2018

This works:

type
  Array[T] = concept a
    a.len is Ordinal
    #get(a, Ordinal) is T
    for value in a:
      value is T

  Map[K, V] = concept m
    type A = Array
    m.keys is A[K]
    m.values is A[V]

template mapImpl(A: typedesc): auto =
  type MapImpl[Key, Value] = object
    keys: A[Key]
    values: A[Value]
  
  assert MapImpl is Map
  
  MapImpl

proc get[T](arr: openarray[T], idx: Ordinal): T = arr[idx]

type
  SeqMap = mapImpl(seq)

iterator pairs(m: Map): (m.K, m.V) =
  let ks = iterator(keys: type(m.keys)): m.K =
    for k in keys:
      yield k
  let vs = iterator(values: type(m.values)): m.V =
    for v in values:
      yield v
  while true:
    let p = (ks(m.keys), vs(m.values))
    if ks.finished or vs.finished: break
    yield p

proc toSeqMap[K, V](ps: openarray[(K, V)]): SeqMap[K, V] =
  result.keys = newSeq[K](ps.len)
  result.values = newSeq[V](ps.len)
  for i, p in ps.pairs:
    (result.keys[i], result.values[i]) = p

var sm = {"a": 1, "b": 2}.toSeqMap

for k, v in sm:
  echo "key: ", k, ", value: ", v

Comment out the get(a, Ordinal) is T line and it breaks. Change it with a[Ordinal] is T and it breaks again because the system module says T[I] is T for whatever reason

@narimiran narimiran transferred this issue from nim-lang/Nim Jan 2, 2019
@Kazark
Copy link

Kazark commented Jun 25, 2020

I believe that as people scorned generic types in the past (Java, Go) and have always ended up conceding in the end (and being rescued by Wadler) so one day languages that have said "we do not need higher-kinded generics" will realize that they ought to be a standard part of modern languages. Nim would do well to add itself to the (currently relatively short) list of (non-toy) languages that have this feature today. It would be a way of giving itself a boost (particularly with the functional community).

@mratsim
Copy link
Collaborator

mratsim commented Jun 25, 2020

I believe that as people scorned generic types in the past (Java, Go) and have always ended up conceding in the end (and being rescued by Wadler) so one day languages that have said "we do not need higher-kinded generics" will realize that they ought to be a standard part of modern languages. Nim would do well to add itself to the (currently relatively short) list of (non-toy) languages that have this feature today. It would be a way of giving itself a boost (particularly with the functional community).

Since 2016, Nim support for higher-kinded type is actually quite usable. The only limitation being generic generics in the proc signature but you can do

proc foo[T](a: T) =
  type U = T.U # Access the nested generics

instead of having to do

proc foo[T, U](a: T[U]) =
  discard

which is a ergonomic improvement.

I.e. functionality is there, and this RFC improves ergonomics.

@Kazark
Copy link

Kazark commented Jun 25, 2020

@mratsim thanks for the clarification. Maybe just an ignorance of the syntax here, but type U = T.U looks like an existential type to me, whereas the second one looks like a universal (forall). What I mean is, where is the U bound in the first case? It looks to me like it is only bound within the body, not within the signature.

This is probably just my ignorance. Where's the best place to read up on this? (Searching led me here.)

@mratsim
Copy link
Collaborator

mratsim commented Jun 25, 2020

@mratsim thanks for the clarification. Maybe just an ignorance of the syntax here, but type U = T.U looks like an existential type to me, whereas the second one looks like a universal (forall). What I mean is, where is the U bound in the first case? It looks to me like it is only bound within the body, not within the signature.

This is probably just my ignorance. Where's the best place to read up on this? (Searching led me here.)

It would come from the type declaration, assuming:

type Foo[U] = object
  field: U

proc foo[T](a: T) =
  type U = T.U # Access the nested generics

The best places to show examples of Nim higher-kinded types in use are probably:

In short Nim is a practical language to implement math concepts in Number Theory in a generic way combining higher-kinded types and dependent-types like capabilities.
It's also as fast as specialized C libraries (as fast as assembly libraries coming).
And you have very few restrictions to precompute at compile-time. I do BigInt computation at compile-time in Constantine. The compilation time stays fast as well (contrary to C++).

Bonus, you can use unicode if it makes sense: https://github.com/mratsim/constantine/blob/ec76ac5e/constantine/towers.nim#L29

type
  Fp2*[C: static Curve] = object
    c0*, c1*: Fp[C]

  β = NonResidue

type
  Fp6*[C: static Curve] = object
    c0*, c1*, c2*: Fp2[C]

  ξ = NonResidue
    # We call the non-residue ξ on 𝔽p6 to avoid confusion between non-residue
    # of different tower level

type
  Fp12*[C: static Curve] = object
    c0*, c1*: Fp6[C]

  γ = NonResidue
    # We call the non-residue γ (Gamma) on 𝔽p6 to avoid confusion between non-residue
    # of different tower level

@Kazark
Copy link

Kazark commented Jun 25, 2020

@mratsim dependent types too! What!! I definitely need to dig more deeply into Nim than I have. Thanks so much for the explanation.

@github-actions
Copy link

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.

@github-actions github-actions bot added the Stale label Jul 20, 2023
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Aug 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants