This is not a question, more like a blog post. I don't have a blog so I do it 
here. Feel free to comment or ask questions.

In my past I have learned the programming language Go, and I really started to 
like the interface type that Go offers, and I wondered, weather it is possible 
to port this go languge feature to Nim with the aid of macros.

First of all, I would like to show you a bit of the advangae of this Go 
interface type, by comparing it the the opetions c++ can give you.

_virtual methods_
    
    
    struct MyInterface {
        virtual int foo() = 0;
        virtual int bar() = 0;
        virtual ~MyInterface(){}
    };
    
    struct MyImplementationA: public MyInterface {
        int foo() override { return 17; }
        int bar() override { return 4; }
    };
    
    struct MyImplementationB: public MyInterface {
        int m_foo;
        int m_bar;
        int foo() override { return m_foo; }
        int bar() override { return m_bar; }
    };
    
    // polymorphic usage
    int foobar(MyInterface* mi) {
        return mi->foo() + mi->bar();
    }
    
    #include <iostream>
    using std::cout;
    using std::endl;
    
    int main() {
        MyImplementationA a;
        MyImplementationB b;
        b.m_foo = 32;
        b.m_bar = 8;
        
        cout << foobar(&a) << endl;
        cout << foobar(&b) << endl;
    }
    
    

In this version the virtual method dispatch in foobar is done with the vtable. 
With the declaration of the first virtual method in MyInterface c++ adds a 
hidden field to the struct, the so called vtable pointer. that points to a 
struct with method pointers to the method implementations. Both 
MyImplementationA and MyImplementationB change the value of this pointer to the 
vtable of teir type. The method invocation foo in foobar opens the vtable, and 
reads from there which method to invoke. Dependent on the type it is either 
MyImplementationA::foo or MyImplementationB::foo. Because of this explicit 
inheritance the interface implementation needs to be newer than the interface 
itself. Also small objects that need to be compact in memory should better not 
have virtual methods because of this added hidden field. C# and Java work 
almost identical, just that in java everything is virtual.

_templates_
    
    
    struct MyImplementationA {
        int foo() { return 17; }
        int bar() { return 4; }
    };
    
    struct MyImplementationB {
        int m_foo;
        int m_bar;
        int foo() { return m_foo; }
        int bar() { return m_bar; }
    };
    
    // polymorphic usage
    template <typename T>
    int foobar(T* mi) {
        return mi->foo() + mi->bar();
    }
    
    #include <iostream>
    using std::cout;
    using std::endl;
    
    int main() {
        MyImplementationA a;
        MyImplementationB b;
        b.m_foo = 32;
        b.m_bar = 8;
        
        cout << foobar(&a) << endl;
        cout << foobar(&b) << endl;
    }
    
    

This version beats all problems of the previous version, but not without 
introducing new problems. There is no hidden field in neither MyImplementationA 
nor MyImplementationB. foobar is compiled for each type it is called 
separately. Meaning that the compiler can also inline the the content from foo 
and bar. Templates in c++ have a huge problem of being recompiled not only for 
each type, but also for each compilation unit. So depending on the complexity 
of foobar and the amount of compilation units where foobar is used, this 
version can really explode compilation time in bigger problems. This is so bad 
that several bigger c++ projects completely abandoned templates. Nim generics 
are very much like c++ templates, so I guess they have this flaw as well. I 
just don't have big enough projects yet to notice any major concerns with Nim's 
generics.

_Go Interface_
    
    
    package main
    
    import "fmt"
    
    type MyImplementationA struct  {}
    
    func (this *MyImplementationA) foo() int {
            return 17;
    }
    
    func (this *MyImplementationA) bar() int {
            return 4;
    }
    
    type MyImplementationB struct  {
        m_foo,m_bar int
    }
    
    type MyInterface interface {
            foo() int
            bar() int
    }
    
    func (this *MyImplementationB) foo() int {
            return this.m_foo;
    }
    
    func (this *MyImplementationB) bar() int {
            return this.m_bar;
    }
    
    // polymorphic usage
    func foobar(mi MyInterface) int {
        return mi.foo() + mi.bar();
    }
    
    func main() {
            a := MyImplementationA{}
            b := MyImplementationB{32, 8}
            
            fmt.Println(foobar(&a))
            fmt.Println(foobar(&b))
    }
    

The Go version does neither put a hidden field in the struct datatype or mark 
anything as virtual. In fact virtual is not a keyword in go nor does it have an 
equivalent. Methods just become virtual, as soon as the type gets casted into 
an interface type.

This works, because the vtable is not part of the struct. An instance of 
MyInterface is actually a tuple of two pointers. The first one is the vtable 
pointer, and the second one is the pointer to the struct that implements the 
interface. With this design decision the Go compiler can just create a vtable 
for any type that has the methods required to implement it. And when the method 
foobar grows over time, and get's used by a lot of different types, it still 
get's compiled only once. If you remember one of Go's main features is it's 
compilation speed.

Now I want the same concept of the Go interfaces in Nim. The idea is to do the 
same thing in two steps:

>   1. build an example case with everything, including vtable and iterface 
> written manually.
>   2. write macros that create the exact same vtable and interface that were 
> manually created in step 1
> 


To not make this post too long, I have joined thise two steps into one step. A 
macro call is in this code followed by a multiline comment that exactly 
represents the generated source code
    
    
    import interfacemacros
    
    type
      MyImplementationA = object
        data : array[0,byte]
      
      MyImplementationB = object
        foo : int
        bar : int
    
    createInterface(MyInterface):
      proc foo(this : MyInterface) : int
      proc bar(this : MyInterface) : int
    
    #[
    type
      MyInterfaceVtable = object
        foo: proc (this: pointer): int
        bar: proc (this: pointer): int
      
      MyInterface = object
        objet: pointer
        vtable: ptr MyInterfaceVtable
    
    proc foo(this: MyInterface): int =
      this.vtable.foo(this.objet)
    
    proc bar(this: MyInterface): int =
      this.vtable.bar(this.objet)
    ]#
    
    proc foo(this : ptr MyImplementationA) : int = 17
    
    proc bar(this : ptr MyImplementationA) : int = 4
    
    implementInterface(MyInterface, MyImplementationB)
    
    #[
    let MyImplementationBVtable = MyInterfaceVtable(
      foo: proc (this: pointer): int = cast[ptr MyImplementationB](this).foo,
      bar: proc (this: pointer): int = cast[ptr MyImplementationB](this).bar
    )
    
    proc MyInterface_InterfaceCast(this: ptr MyImplementationB): MyInterface =
      MyInterface(objet: this, vtable: MyImplementationBVtable.unsafeAddr)
    ]#
    
    implementInterface(MyInterface, MyImplementationA)
    
    #[
    let MyImplementationAVtable = MyInterfaceVtable(
      foo: proc (this: pointer): int = cast[ptr MyImplementationA](this).foo,
      bar: proc (this: pointer): int = cast[ptr MyImplementationA](this).bar
    )
    
    proc MyInterface_InterfaceCast(this: ptr MyImplementationA): MyInterface =
      MyInterface(objet: this, vtable: MyImplementationAVtable.unsafeAddr)
    ]#
    
    proc foobar(mi: MyInterface) : int =
      return mi.foo + mi.bar
    
    var
      a : MyImplementationA
      b : MyImplementationB = MyImplementationB(foo : 32, bar : 8)
    
    echo foobar(MyInterface_InterfaceCast(a.addr))
    echo foobar(MyInterface_InterfaceCast(b.addr))
    
    

The differences two the Go version are:

>   * The interface declaration is in a macro call.
>   * The vtables for the different types are requested explicitly with 
> implementInterface(MyInterface, MyImplementation)
>   * The conversion between pointer type and interface type is explicit with 
> MyInterface_InterfaceCast
>   * error messages when the interface can not be implemented are not really 
> _nice_
> 


And for completion, here are the macros that do the magic:
    
    
    import macros
    
    macro createInterface*(name : untyped, methods : untyped) : untyped =
      name.expectKind nnkIdent
      
      let
        vtableRecordList = nnkRecList.newTree
        vtableIdent = newIdentNode($name.ident & "Vtable")
        vtableTypeDef = nnkTypeSection.newTree(
          nnkTypeDef.newTree(
            vtableIdent,
            newEmptyNode(),
            nnkObjectTy.newTree(
              newEmptyNode(),
              newEmptyNode(),
              vtableRecordList
            )
          )
        )
      
      var newMethods = newSeq[NimNode]()
      
      for meth in methods:
        meth.expectKind(nnkProcDef)
        let
          methodIdent = meth[0]
          params = meth[3]
          thisParam = params[1]
          thisIdent = thisParam[0]
          thisType  = thisParam[1]
        
        if thisType != name:
          error thisType.repr & " != " & name.repr
        
        let vtableEntryParams = params.copy
        vtableEntryParams[1][1] = newIdentNode("pointer")
        
        vtableRecordList.add(
          nnkIdentDefs.newTree(
            methodIdent,
            nnkProcTy.newTree(
              vtableEntryParams,
              newEmptyNode(),
            ),
            newEmptyNode()
          )
        )
        
        let call = nnkCall.newTree(
          nnkDotExpr.newTree( nnkDotExpr.newTree(thisIdent, 
newIdentNode("vtable")), methodIdent  ),
          nnkDotExpr.newTree( thisIdent, newIdentNode("objet") ),
        )
        
        for i in 2 ..< len(params):
          let param = params[i]
          param.expectKind(nnkIdentDefs)
          for j in 0 .. len(param) - 3:
            call.add param[j]
        
        meth[6] = nnkStmtList.newTree(call)
        
        newMethods.add(meth)
      
      result = newStmtList()
      result.add(vtableTypeDef)
      result.add quote do:
        type `name` = object
          objet : pointer
          vtable: ptr `vtableIdent`
      
      for meth in newMethods:
        result.add meth
      
      #echo result.repr
    
    macro implementInterface*(interfaceName, implementationCandidateName: 
typed) : untyped =
      let
        vtableSymbol = interfaceName.symbol.getImpl[2][2][1][1][0]
        vtableRecordList = vtableSymbol.symbol.getImpl[2][2]
      
      let vtableValueSymbol = newIdentNode($implementationCandidateName.symbol 
& "Vtable")
      
      let
        objectConstructor = nnkObjConstr.newTree(vtableSymbol)
        
        vtableValueDecalaration =
          nnkLetSection.newTree(
            nnkIdentDefs.newTree(
              vtableValueSymbol,
              newEmptyNode(),
              objectConstructor
            )
          )
      
      for identDefs in vtableRecordList:
        let
          methodName = identDefs[0]
          params = identDefs[1][0]
          lambdaBody = quote do:
            cast[ptr `implementationCandidateName`](this).`methodName`()
          call = lambdaBody[0]
        
        for i in 2 ..< len(params):
          let param = params[i]
          param.expectKind(nnkIdentDefs)
          for j in 0 .. len(param) - 3:
            call.add param[j]
        
        # leave out () when not needed
        if call.len == 1:
          lambdaBody[0] = call[0]
        
        methodName.expectKind nnkIdent
        
        objectConstructor.add nnkExprColonExpr.newTree(
          methodName,
          nnkLambda.newTree(
            newEmptyNode(),newEmptyNode(),newEmptyNode(),
            params.copy,
            newEmptyNode(),newEmptyNode(),
            lambdaBody
          )
        )
      
      result = newStmtList()
      result.add vtableValueDecalaration
      
      let castIdent = newIdentNode($interfaceName.symbol & "_InterfaceCast")
      
      result.add quote do:
        proc `castIdent`(this: ptr `implementationCandidateName`) : 
`interfaceName` = `interfaceName`(
          objet : this,
          vtable : `vtableValueSymbol`.unsafeAddr
        )
      
      # echo result.repr
    
    

conclusion: Nim turns out to be a very powerful programming language.

Reply via email to