Hi,
I wanted to encode entities with an "intrinsic" (existential) type and a thing that comes close to it is described in :
https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
I had to extend the pattern a bit with a template taking two types, the latter for the intrinsic type and the implementation requires three steps : a constructor pattern, the definition of constructor classes (being templates themselves) relying on the constructor pattern, and finally the instantiations of types (classes) that contain the intrinsic type.
I then can "at will" decide if I want to keep the instantiation type abstract because it can never be seen from the outside ("we don't now"), or I can "pull it out" with type reconstruction, in particular using a template<template> construct. The latter can be regarded as a rank-2 type, because the type is not given by forehand.
At my surprise, it is possible within C++. It shows that C++ retains the abstract part of a template pretty well - at least for the purpose. If C++ allowed for better abstractions, we could do more with it. But C++'s templates were introduced at a time where these extensions could not be seen - they were not understood yet. (Instantiation types can't reflect abstract types in a satisfying way...)
However, the implementation is entirely static. Where in other languages, it would be done with virtual functions with the help of a runtime table.
#include <stdio.h>
#include <iostream>
using namespace std;
#define reif (static_cast<T&>(*this))
//constructor pattern
template<class T, typename V = int>
struct pcset  {
  void show() {     reif.mshow();    }
  void set() {      reif.mset();     }
  void sumop(T& v){ reif.msumop(v);  }
  void notimpl() {  reif.mnotimpl(); }
  void mshow() { printf("dummy: %d\n" , dummy); }
  void msset() { printf("set not implemented yet!\n"); }
  void msumop(T& v) { printf("sumop not implemented yet!\n"); }
  void mnotimpl() { printf("... not implemented yet!\n"); }
  int dummy;  protected:
  V mc;
  static V static_op( V a, V b) { return T::mstatic_op(a,b); }
  static V mstatic_op( V a, V b)  {
       printf("static_op not implemented yet!\n"); return a; }
} ;
//constructors
template<class A, typename U, typename V>
struct dvd_ggen : pcset<A,V> {
  U ma,mb;
  void init(U a, U b) { ma = a; mb = b; this->mc = b; this->dummy = 0;    }
  void mshow() { cout << ma << " " << mb << "  " << this->dummy <<"\n" ; }
  void mset() {  ma = mb; }
  void msumop(A& v) { ma = this->static_op(ma,v.mb) ; }
  static V mstatic_op(V a, V b) { return a + b ; }
} ;
template<typename U>
struct dvd_gen : dvd_ggen<dvd_gen<U>,U,U> {};
struct dvd_int  : dvd_ggen<dvd_int,int,int> {
  void init (int a,int b) { this->dummy = b; }
  static int mstatic_op(int a, int  b) { printf("I'll do nothing for you!\n"); return 0 ; }
  void mnotimpl() { printf("surprise! surprise!\n");}
} ;
//instantiations: bind the intrinsic type
dvd_gen<int> da,db;
dvd_gen<string> dc,dd;
dvd_int de;
//access with template<template>  a rank-2 quantifier
template <template<typename> class  A, typename T>
void binop(A<T>& b, A<T>& c) {
    b.sumop(c) ;   b.show() ;
    T temp = b.mb; b.ma = temp;
}
//some operations
int main(int argc, char *argv[]) {
    da.init(5,7) ; da.show();
    db.init(1,2); db.sumop(da); db.show();
    dc.init("alpha","beta"); dd.init("delta","epsilon");
    dc.sumop(dd); dc.show();
    de.init(0,42) ; de.set() ; printf("...show\n") ; de.show();
    dd.notimpl();
    de.notimpl();
    //binop<dvd_gen<int>>(da);
    binop(dc,dd);
}
My question is: How could this be achieved in Nim? Or, alternate question : How abstract are instantiation types in Nim?
I tried here https://github.com/mratsim/weave/blob/a2e118b/experiments/e01_staccato.nim#L17-L42
Unfortunately it crashes the compiler:
type FibTask = object of Task[FibTask]
  n: int
  sum: ptr int64
## This crashes the compiler
## with `Error: internal error: getTypeDescAux(tyOr)`
##
# var x: FibTask
Are we sure this is still the case, I did fix some stuff with recursive generics which released with 1.6, the follow does compile.
type
  Task[T] = object of RootObj
    data: ref T
    doThingInternal: proc(a: T) {.nimcall.}
  
  FibTask = object of Task[FibTask]
    n: int
    sum: ptr int64
proc doThing[T](a: Task[T]) = a.doThingInternal(a.data[])
var a: FibTask
a.doThingInternal = proc(fib: FibTask) = echo fib.n
a.data = new FibTask
a.data.n = 300
a.doThing()