Nim: make interface based on function pointer easier to use

Created on 3 Jul 2020  路  6Comments  路  Source: nim-lang/Nim

Summary

Interface makes libraries extensible. Now we can use function pointers to simulate interface
in Nim such as StreamObj.

However, when we want to use its function calls, we must duplicate object's name such as s.closeImpl(s).

If we heavily use interface(and even nested) to create reusable components, it may be a little tedious. Especially we have a nested object structure, we may not want to write D.c.b.a twice.

type
  A = object
    p: function pointer
  B = object
    a: A
  C = object
    b: B
  D = object
    c: C

Proposal

I suggest making a helper macros(s.ucall(closeImpl)) to deduplicate object's name(Maybe put it in sugar.nim).

Description

Make a ucall macros and automatically call object's name.

macro ucall(obj: typed, call: untyped, params: varargs[untyped]): untyped =
  result = newStmtList()
  var tmp = newNimNode(nnkCall)
  tmp.add(newDotExpr(obj, call))
  tmp.add(obj)
  for param in params:
    tmp.add(param)
  result.add tmp

Effect

# before
echo a.barkImpl(a, 27, c = 14)
# after
echo a.ucall(barkImpl, 27, c = 14)

# before
echo people.longAndUgly.pet.barkImpl(people.longAndUgly.pet, 27, c = 14)
# after
echo people.longAndUgly.pet.ucall(barkImpl, 27, c = 14)

# before
people.longAndUgly.pet.sleepImpl(people.longAndUgly.pet)
# after
people.longAndUgly.pet.ucall(sleepImpl)

# before
echo people.longAndUgly.pet.danceImpl(people.longAndUgly.pet, "Nim")
# after
echo people.longAndUgly.pet.ucall(danceImpl, "Nim")


# echo Animal(id: 12, barkImpl: bark, danceImpl: dance).
#           barkImpl(Animal(id: 12, barkImpl: bark, danceImpl: dance), 27, c = 16)
# echo Animal(id: 12, barkImpl: bark, danceImpl: dance).ucall(barkImpl, 27, c = 16)

# echo newAnimal(13).barkImpl(newAnimal(13), 66, 99)
# echo newAnimal(13).ucall(barkImpl, 66, 99)

Full examples in https://play.nim-lang.org/#ix=2qKq

All 6 comments

Another idea would be to create a macro that generates templates to allow using this syntax s.closeImpl() directly.

Experimental codes: https://github.com/xflywind/shene

Make interface based on function pointers and generics support better polymorphism.

import shene
import strformat


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

  Cat* = object of Animal[Cat]
    cid: int

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


proc sleep*(a: Cat) =
  discard

proc bark*(a: Cat, b: int, c: int): string =
  result = fmt"{a.cid + b + c = }"

proc dance*(a: Cat, b: string): string =
  result = fmt"{a.id = } |-| {b = }"

proc newCat*(id, cid: int): Must[Animal[Cat], Cat] =
  result.id = id
  result.cid = cid
  result.sleepImpl = sleep
  result.barkImpl = bark
  result.danceImpl = dance

let p = People[Cat](pet: newCat(id = 12, 13))
echo p.pet.call(barkImpl, 13, 14)
p.pet.call(sleepImpl)
echo p.pet.id
echo p.pet.cid

I have done this before: https://github.com/b3liever/protocoled however it needs to be split, so protocol and impl are separate blocks, don't have the time, so feel free to take over.

Ideas

Decouple impl and data

Impl represents Impl Class and data stands for Data Class. Impl Class supplies all interfaces which should be satisfied. It shouldn鈥檛 be inherited. Data Class supplies all attributes that can be extended by users. It supports inheritance.

type
  Must*[U; T: object | ref object] = object 
    impl: U
    data: T

Impl Class

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

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

type
  People*[T] = object
    pet: must(Animal, T)

Usage

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

Find vTable in docs and close this issue.

Find vTable in docs and close this issue.

Vtables are not really implemented yet :)

Was this page helpful?
0 / 5 - 0 ratings