DEV Community

ringabout
ringabout

Posted on • Updated on

Zero-overhead interface exploration in Nim language

Nim is a statically typed compiled systems programming language. It is efficient, expressive and elegant. I really enjoy coding Nim.

However, when it comes to interface, Nim lacks a built-in interface making it hard to implement dependency injection pattern.

In spite of lack of interface at the syntactic level, we still have some ways to simulate interface . It may be not suitable for general purposes. But it works well in terms of implementing dependency injection pattern.

Before starting to read, you should install shene .

Function pointers

A function pointer points to a function declaration.

First we create a new class declaration with all function declarations that we need.

type
  Animal* = object
    id: int
    sleepImpl: proc (a: Animal) {.nimcall, gcsafe.}
    barkImpl: proc (a: Animal, b: int, c: int): string {.nimcall, gcsafe.}
    danceImpl: proc (a: Animal, b: string): string {.nimcall, gcsafe.}

  People* = object
    pet: Animal
Enter fullscreen mode Exit fullscreen mode

Second we write function definitions for Animal class. Then we assign function pointers to corresponding attributes. Next, we construct People object and pass an Animal instance to it. Finally, we call barkImpl and pass parameters to it. Now, we get the right result: “1340”.

proc sleep(a: Animal) =
  discard

proc bark(a: Animal, b: int, c: int): string =
  result = $(a.id + b + c)

proc dance(a: Animal, b: string): string = 
  result = b

proc newAnimal*(id: int): Animal =
  result.id = 1314
  result.sleepImpl = sleep
  result.barkImpl = bark
  result.danceImpl = dance

let people = People(pet: newAnimal(12))
doAssert people.pet.barkImpl(people.pet, 12, 14) == "1340"
Enter fullscreen mode Exit fullscreen mode

This is pretty neat except that we specify people.pet twice. Let’s deduplicate it.

import shene/ucall


let people = People(pet: newAnimal(12))
doAssert people.pet.ucall(barkImpl, 12, 14) == "1340"
Enter fullscreen mode Exit fullscreen mode

Well, better now. Let’s look at the advantages. It does implement late binding and makes codes extensible. We only need to declare Animal class and expose it to users. Users can implement their own function definitions for Animal class and assign function pointers to the attributes of Animal’s instance.

However, users can’t extend the data of the Animal instance. Users’ function definitions are only allowed to use the id attribute which is fixed at the declaration time of Animal class. No more attributes can be added.

Let’s look at another way. This way decouples impl and data and makes data extension easier. You can extend data object at your will.

Decouple impl and data

Impl represents Impl Class and data stands for Data Class. Impl Class supplies all interfaces which should be satisfied. Data Class supplies all attributes that users need and it can be extended by users. They both support inheritance.

# shene.nim
type
  Must*[U: object; T: object | ref object] = object 
    impl*: U
    data*: T
Enter fullscreen mode Exit fullscreen mode

Impl Class

We define function pointers just like before, but now the first parameter of function declarations should be generics type. We should only define function pointers without any other attributes.

import shene/mcall


type
  Animal*[T] = object of RootObj
    sleepImpl: proc (a: T) {.nimcall, gcsafe.}
    barkImpl: proc (a: var T, b: int, c: int): string {.nimcall, gcsafe.}
    danceImpl: proc (a: T, b: string): string {.nimcall, gcsafe.}
Enter fullscreen mode Exit fullscreen mode

Data Class

Data Class contains all user-defined attributes. You can add data attributes by means of inheritance. But be careful, now we need Must[Animal[Cat], Cat] type as the type of our object which is passed to People object. shene supplies must(Animal, Dog) which is a helper templates to simplify type declaration. It also overloads dot operator and makes assignment easier.

# If class is ref object, user must `new Must.data` or `init Must`.
type
  Dog = object
    id: string
    did: int
    name: string


proc bark(d: var Dog, b: int, c: int): string =
  doAssert d.name == "OK"
  d.did = 777
  doAssert d.did == 777
  d.id = "First"
  doAssert d.id == "First"

# must(Impl class, Data class)
proc newDog(): must(Animal, Dog) =
  result.name = "OK"
  result.did = 12
  result.barkImpl = bark
Enter fullscreen mode Exit fullscreen mode

Oriented-User Class

Regarding People class, we need additional generics type. must(Animal, T) is the helper templates for Must[Animal[T], T].

type
  People*[T] = object
    pet: must(Animal, T)
Enter fullscreen mode Exit fullscreen mode

Usage

Its usage is simple. We only need to add additional generics type. There is little difference compared to before.

var d = newDog()
var p1 = People[Dog](pet: d)
discard p1.pet.call(barkImpl, 13, 14)
Enter fullscreen mode Exit fullscreen mode

Other tips

If you need dynamic dispatch, you can also use multi-methods.

A better solution

Thanks jyapayne, we have a better solution:


type
  Animal* = concept a
    a.id is int
    a.sleep()
    a.bark(int, int) is string
    a.dance(string) is string

  People*[T: Animal] = object
    pet: T

  # User defined type
  Dog* = object
    id: int

# User defined procs

proc sleep*(d: Dog) =
  discard

proc bark*(d: Dog, b: int, c: int): string =
  result = $(d.id + b + c)

proc dance*(d: Dog, b: string): string =
  result = b

proc newDog*(id: int): Dog =
  result.id = 1314


proc pay[T](p: People[T]) =
  doAssert p.pet.bark(12, 14) == "1340"
  doAssert p.pet.dance("dancing") == "dancing"

let d = newDog(12)
pay[Dog](People[Dog](pet: d))
Enter fullscreen mode Exit fullscreen mode

from https://forum.nim-lang.org/t/6523

Conclusion

Using function pointers, we can simulate interface . We improve the extensibility and maintainability of codebase. Although we still need a better way to represents interface, let’s take this as our first step.

Last but not least. Nim is a magic programming language. It is still young and needs more attentions. Explore it and you may fall in love with Nim.

Top comments (5)

Collapse
 
shirleyquirk profile image
shirleyquirk • Edited

hey, great post
the thing about Nim is that since it isn't strictly object oriented, interfaces aren't really necessary, as it separates functions and data inherently
So your 'Animal' class doesn't need to define barkimpl etc.
You just define

type
 Animal = ref object of RootObj
  id:int
 People = object
  pet:Animal
proc bark(a:Animal,b:int,c:int):string = $(a.id+b+c)

that's enough for

var people = People(pet:Animal(12))
assert people.pet.bark(13,14) == $(12+13+14)

to work without any messing about with helper macros.
extending:

type
 Dog =ref object of Animal
   did:string
   name:string
let fido = Dog(did:"First",id:777,name:"OK")
people.pet = fido #works
doAssert people.pet.id==777
doAssert people.pet.Dog.name=="OK"     #cast to Dog to access .name
doAssert people.pet is Animal                     #still Animal
proc bark(d:Dog,b:int,c:int) = $(d.id+2*b+3*c)        
doAssert people.pet.bark(13,14) == $(777+13+14)      #Animal.bark
doAssert people.pet.Dog.bark(13,14) == $(777+2*13+3*14) #Dog.bark
Collapse
 
ringabout profile image
ringabout

Thanks for your comments!

Collapse
 
shirleyquirk profile image
shirleyquirk • Edited

Just seen this:
nimble.directory/pkg/interfaced
I think it provides the semantics you are looking for, really cleanly.
Works by creating a vtable under the hood, just like other oo languages do.

Pretty crazy that Nims macro system makes it possible to add such a feature to the language.

Thread Thread
 
ringabout profile image
ringabout

Thanks!

Thread Thread
 
shirleyquirk profile image
shirleyquirk

Oh man, they just keep coming:
This hidden gem is undocumented, but provides the same functionality without the overhead of a vtable.

github.com/mratsim/trace-of-radian...