nim-lang / RFCs

A repository for your Nim proposals.
136 stars 26 forks source link

Support generic wildcard for ref and ptr types #246

Closed tulayang closed 3 years ago

tulayang commented 3 years ago

Motivation

When creating a container or collection, the specific type of a ref or a ptr element does not need to be specified.

Description

In java, the question mark (?) is a wildcard to represent an unknown type. You can use a wildcard to relax the restrictions on a variable.

class Foo<T> {
  T value;
}

class Bar {
  List<Foo<?>> foos = new LinkedList<Foo<?>>();
}

This can be very useful especially when creating a container or collection.

Examples

For example, define a queue for dispatching tasks:

type
  Task[T] = object
    data: T
    run: proc (task: ref Task[?])

  Queue = object
    data: seq[ref Task[?]]

proc add(queue: Queue, task: ref Task[?]) =
  queue.data.add(task)

proc exec(queue: Queue) =
  for d in queue.data:
    d.run(d)
type
  FooData = object
    value: int

  FooTask = Task[FooData]

proc runFooTask(task: ref Task[?]) = 
  echo (ref FooTask)(task).data.value

proc newFooTask(value: int): ref FooTask =
  new(result)
  result.value = value
  result.run = runFooTask

type
  BarData = object
    name: string
    value: int

  BarTask = Task[BarData]

proc runBarTask(task: ref Task[?]) = 
  echo (ref BarTask)(task).data.name, ":", (ref BarTask)(task).data.value

proc newBarTask(name: string, value: int): ref BarTask =
  new(result)
  result.name = name
  result.value = value
  result.run = runBarTask
var queue = new(Queue)

queue.add(newFooTask(1))
queue.add(newFooTask(2))
queue.add(newFooTask(3))

queue.add(newBarTask("bar1", 1))
queue.add(newBarTask("bar2", 2))
queue.add(newBarTask("bar3", 3))

queue.exec()
ghost commented 3 years ago

The code you showed can be easily achieved by generics, so I guess all you want is some syntax sugar to make it a little less verbose?

ghost commented 3 years ago

And for generic ptr we already have pointer

Araq commented 3 years ago

The "Motivation" is lacking and unfortunately I don't understand the purpose of this RFC.

For example:


type
  TaskBase = object of RootObj
  Task[T] = object
    data: T
    run: proc (task: ref TaskBase)

  Queue = object
    data: seq[ref TaskBase]

proc add(queue: Queue, task: ref TaskBase) =
  queue.data.add(task)

proc exec(queue: Queue) =
  for d in queue.data:
    d.run(d)

Works in today's Nim and is common too. Type erasure is easy enough to do in Nim.

mratsim commented 3 years ago
type
  Task[T] = object
    data: T
    run: proc (task: ref Task[?])

  Queue = object
    data: seq[ref Task[?]]

proc add(queue: Queue, task: ref Task[?]) =
  queue.data.add(task)

proc exec(queue: Queue) =
  for d in queue.data:
    d.run(d)

can be written

type
  Task[T, U] = object
    data: T
    run: proc (task: ref Task[U])

  Queue[U] = object
    data: seq[ref Task[U]]

proc add(queue: Queue, task: ref Task) =
  queue.data.add(task)

proc exec(queue: Queue) =
  for d in queue.data:
    d.run(d)
tulayang commented 3 years ago

@Araq

Yeah. I know this solution. And in my code, this compromise is now adopted. I hope to achieve this goal without object of. I think object of has two limitations:

The solution provided by @mratsim looks nice, but it cannot be compiled:

type
  Task[T] = object
    data: T
    run: proc (task: ref Task[T])

  Queue[T] = object
    data: seq[ref Task[T]]

proc initQueue(): Queue =
  # Error: cannot instantiate: 'Queue[T]'; Maybe generic arguments are missing?
  result.data = @[]

var queue = initQueue()

Motivation: if the elements in seq is ref T type, then T can be ignored when seq[ref T] is instantiated. Like this:

type
  Task[T] = object
    data: T
    run: proc (task: ref Task[T])

  Queue = object
    data: seq[ref Task[?]]

proc initQueue(): Queue =
  result.data = @[]

var queue = initQueue()
var a: seq[ref ?] = @[] 
var a: seq[ptr ?] = @[] 
var a: seq[ref Task[?]] = @[] 
var a: seq[ptr Task[?]] = @[] 

# similar to pointer
var a: seq[pointer] = @[] 
mratsim commented 3 years ago

The error is pretty clear though, you're missing a generic argument.

type
  Task[T] = object
    data: T
    run: proc (task: ref Task[T])

  Queue[T] = object
    data: seq[ref Task[T]]

proc initQueue(T: typedesc): Queue[T] =
  discard

var queue = initQueue(int)
echo queue.data.len
tulayang commented 3 years ago

@mratsim

This (initQueue(int)) only allows integers (ref Task[int]). However, I assume that ref Task[int], ref Task[string], ref Task[A] and ref Task[B] are allowed to be stored in a same queue.

Task.data is a user data, the Queue has nothing to do with it and does'nt care its specific value.

Araq commented 3 years ago

If we extend the example slightly we can see some problems:


type
  Task[T] = object
    data: T
    run: proc (task: ref Task[?])

  Queue = object
    data: seq[ref Task[?]]

proc add(queue: Queue, task: ref Task[?]) =
  queue.data.add(task)

proc pop(q: Queue): ref Task = discard "..."

let x = Task[string](q.pop()) # how to check if this conversion is valid? 
# it requires a runtime check. How we can perform this check? Well with 
# runtime type information. Now if only we inherited from some base that
# would be available...
# Or if queue wouldn't have lost type information we could statically check that...
Araq commented 3 years ago

doesn't allow multiple inheritance

So what, type erase remains possible, it's good enough.

adds redundant abstraction: TaskBase and Task

"Redundant" abstractions are better than adhoc rules that break the type system in unforeseen ways. There is nothing "redundant" here anyway, what you really mean is inconvenient.

mratsim commented 3 years ago

Task.data is a user data, the Queue has nothing to do with it and doesn't care its specific value.

The type system cares. Nim is not Java where every single type is Boxed and derives from a Root object. Ideally this would be possible with the introduction of VTable for concept and creating a concept called Any. Or use what Araq's suggested.

If it's too cumbersome you can write a template to automate that in a one-liner.

Araq commented 3 years ago

Sorry but this has a chance of 0% of being adopted.