ringabout
Posted on July 6, 2020
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
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"
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"
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
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.}
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
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)
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)
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))
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.
Posted on July 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.