beef331/traitor

Ideas for improvement

mildred opened this issue · 3 comments

Hi, I recently was redirected here from my question on nim-forum because I want to see how it is possible to implement golang-like streams in Nim. I want to see how I can declare an interface type / trait that represents a stream, and how it can be implemented by multiple other types independencly. I don't really want duck-typing and manual declaration is better I think, but I need a wite set of features:

  • Generic traits, because why a stream should always be a stream of characters
  • A type should be able to implement multiple traits sharing the same procedure signature at the same time
  • once a type is converted to a trait type, a run-time conversion should be possible to convert it to another trait the underlying type implements (if possible, and if impossible it should be detectable by the caller). This is in order to convert an input stream to use another proc signature to implement fewer copies if possible, or even zero-copy
  • traits and their implementation should be exportable to other packages, traits and implementations sould be able to be declared in different packages

Here is what I'd like to write andcompile:

import traitor
import std/options

type
  Reader*[T] = concept
    proc read(self: Self, buffer: openArray[T]): Option[int]
  Writer*[T] = concept
    proc write(self: Self, buffer: openArray[T], num: ref int)
  ReadWriter*[T] = concept
    proc read(self: Self, buffer: openArray[T]): Option[int]
    proc write(self: Self, buffer: openArray[T], num: ref int)
  WriterTo*[T] = concept
    proc write_to(self: Self, r: Reader[T]): Option[int]
  ReaderFrom*[T] = concept
    proc read_from(self: Self, w: Writer[T], num: ref int)
  Closer*[T] = concept
    proc close(self: Self)

type Null[T] = ref object

implTraits Reader, ReadWriter:
  proc read[T](self: Null[T], buffer: openArray[T]): Option[int] =
    echo "read"
    echo $buffer.len
    echo $T
    some(buffer.len)

implTraits Writer, ReadWriter:
  proc write[T](self: Null[T], buffer: openArray[T], num: ref int) =
    echo "write"
    echo $buffer.len
    echo $T
    if num != nil: num[] = buffer.len

let null = new(Null[char])
let r: Reader[char] = null.to(Reader[char])
r.read(@['a'])
let w: Writer[char] = r.to(Writer[char])
assert w != nil
w.write(@['a'], nil)

The use of a generic makes this relative complicated for a macro as we do not get when types are instantiated so you have to manually instantiate the procedures to store them to the vtable(this actually might be wrong).
You'd also have to do something more like:

let null = new(Null[char]).toImpl ReadWriter
let r: Reader[char] = null.to(Reader[char]) # Presently not a thing that's done but could be in theory
r.read(@['a'])
let w: Writer[char] = r.to(Writer[char]) # same as priror
assert w != nil
w.write(@['a'], nil)

To get the API you want you'll certainly have to get deep into how Go/Swift does objects and replicate it in Nim using macros.

Edit: This does give me an idea that can make this a much simpler api and solve most issues, but will need to investigate further.

Well, I no not wish to replicate how Go does its interfaces, I just need some system in Nim that it powerful enough. I believe I could make it work in Nim without any sugar for the moment:

import std/options

type
  Iface*[T] = ref object
    original*: ref RootObj
    vtables*: seq[ref RootObj]
    vtable*: T
  None = ref object of RootObj

let noneVtable: None = new(None)

proc to*[S, T](self: S, t: typedesc[T]): Option[T] =
  var res: T
  var vt: seq[ref RootObj]

  when compiles(vt = self.vtables()):
    vt = self.vtables()
  else:
    vt = self.vtables

  for i, v in vt.pairs():
    if v of typeof(res.vtable):
      return some(Iface[typeof(res.vtable)](
        original: cast[ref RootObj](self),
        vtables: vt,
        vtable: cast[typeof(res.vtable)](v)))
  result = none T

########################

type
  ReaderVtable*[S, T] = ref object of RootObj
    read*: proc(self: S, buffer: openArray[T]): Option[int]

  WriterVtable*[S, T] = ref object of RootObj
    write*: proc(self: S, buffer: openArray[T], num: ref int)

  Reader*[T] = Iface[ReaderVtable[ref RootObj, T]]
  Writer*[T] = Iface[WriterVtable[ref RootObj, T]]

# Should be auto generated from vtable type if possible
proc read*[T](self: Reader[T], buffer: openArray[T]): Option[int] =
  self.vtable.read(self.original, buffer)

# Should be auto generated from vtable type if possible
proc write*[T](self: Writer[T], buffer: openArray[T], num: ref int = nil) =
  self.vtable.write(self.original, buffer, num)

########################

type Null[T] = ref object of RootObj

proc read*[T](self: Null[T], buffer: openArray[T]): Option[int] =
  result = some(buffer.len)

# Should be auto generated as this is just a type cast for self
proc readImplem[T](self: ref RootObj, buffer: openArray[T]): Option[int] =
  cast[Null[T]](self).read(buffer)

proc write*[T](self: Null[T], buffer: openArray[T], num: ref int) =
  echo "write"
  discard

# Should be auto generated as this is just a type cast for self
proc writeImplem[T](self: ref RootObj, buffer: openArray[T], num: ref int) =
  cast[Null[T]](self).write(buffer, num)

# Should be auto generated if possible although manual declaration makes it explicit
proc vtables*[T](self: Null[T]): seq[ref RootObj] =
  let reader = ReaderVtable[ref RootObj, T](
    read: readImplem
  )
  let writer = WriterVtable[ref RootObj, T](
    write: writeImplem
  )
  return @[
    cast[ref RootObj](reader),
    cast[ref RootObj](writer)
  ]

var foo = new(Null[char])
var reader = foo.to(Reader[char]).get
echo reader.read(@['q'])
var writer = reader.to(Writer[char]).get
writer.write(@['q'], nil)

I personally dont like playing compiler so avoid manual stuff like the above, but I did start on the implementation I described earlier. Do not know if I am going to continue as I do not really have a need for it or traitor.

As an aside you can just do Null[T](self) instead of the cast