How to create a list of trait

I have a list of operations, e.g.: Add, Sub, etc… I need to apply them to some numbers.

But I can’t find a way to create a List of trait

from collections import List

trait Operation(Copyable & Movable):
	pass

fn main():
	var list = List[Operation]() # error

It fails with error:

error: cannot implicitly convert 'AnyTrait[Operation]' value to 'Copyable & Movable' in type parameter
       var list = List[Operation]() # error
                       ^~~~~~~~~

Any workaround? Thanks.

Hi! We don’t support existentials yet, so you can’t put trait objects in a list.

That said, this sounds like a case where enums really shine:

from utils import Variant

@fieldwise_init  # if you are on nightly
struct Add(Copyable, Movable):
  ...

@value  # if you are on 25.3
struct Mul:
  ...

alias Op = Variant[Add, Mul]

fn main():
  add = Op(Add())
  mul = Op(Mul())
  l = List[Op](add, mul)
1 Like

Might wanna remove @value from example as it will soon be removed. So other can see in future.

1 Like

Thank you very much.

The working code:

from utils import Variant

@fieldwise_init 
struct Add(Copyable, Movable):
	var a: Int
	var b: Int

	fn __copyinit__(out self, other: Self):
		self.a = other.a
		self.b = other.b

	fn __moveinit__(out self, owned other: Self):
		self.a = other.a
		self.b = other.b

	fn calc(self) -> Int:
		return self.a + self.b

@fieldwise_init  
struct Mul(Copyable, Movable):
	var a: Int
	var b: Int

	fn __copyinit__(out self, other: Self):
		self.a = other.a
		self.b = other.b

	fn __moveinit__(out self, owned other: Self):
		self.a = other.a
		self.b = other.b

	fn calc(self) -> Int:
		return self.a * self.b

alias Op = Variant[Add, Mul]

fn main():
	add = Op(Add(1,2))
	mul = Op(Mul(3,4))
	l = List[Op](add, mul)

	for op in l: 
		if op[].isa[Add](): # <---- is it supposed to be used like this
			print(op[][Add].calc())
		if op[].isa[Mul]():
			print(op[][Mul].calc())

Looking forward to the List[trait] support :grinning_face:

1 Like

The code becomes very complex when there’re lots of .isa[](), so I ended up using high order functions:

alias F = fn(Int) escaping -> Int

fn add(x: Int) -> F:
	fn inner(y: Int) -> Int:
		return x + y
	return inner

fn mul(x: Int) -> F:
	fn inner(y: Int) -> Int:
		return x * y
	return inner

fn main(): 
	var result = 1

	var list = List[F](add(7), mul(11))
	for f in list:
		result = f[](result)
	
	print(result) 

I see you already have a solution, but your initial question triggered my curiosity to find an elegant algorithm. Initially I thought you needed a parallel approach, then later I saw your ‘higher order’ solution which uses sequential calculations, so I worked that out too. In short, the sequential solution below performs the same calculations as in your example (I think). The parallel algorithm does not, but I thought it worth sharing FWIW. Cheers!

from algorithm.functional import parallelize

alias fn_sig = fn (a: Int, b: Int) -> Int

struct Op(Copyable & Movable):
    """Contains mathematical operations."""

    alias Add = Self.add
    alias Mul = Self.mul

    var operation: fn_sig
    var a: Int
    var b: Int

    fn __init__(out self, operation: fn_sig, a: Int, b: Int):
        self.operation = operation
        self.a = a
        self.b = b

    @parameter
    fn __call__(self, out value: Int):
        """Parallel - returns values from initial arguments."""
        value = self.operation(self.a, self.b)

    @parameter
    fn __call__(self, mut value: Int):
        """Sequential - mutates the 'value' argument."""
        value = self.operation(value, self.b)

    @staticmethod
    fn bind(func: fn_sig, a: Int, b: Int, out result: Self):
        result = Self(func, a, b)

    @staticmethod
    fn add(a: Int, b: Int, out value: Int):
        value = a + b

    @staticmethod
    fn mul(a: Int, b: Int, out value: Int):
        value = a * b

fn main():
    alias RESIZE_DEFAULT = -1  # Value not important.
    operations = List[Op]()

    # 1) Sequential calculations.
    value = 1
    operations.append(Op.bind(Op.Add, value, 7))
    operations.append(Op.bind(Op.Mul, value, 11))

    # Perform Sequential calculations.
    for op in operations:
        op[](value)
    print("Sequential value =", String(value))
    print()

    # 2) Parallel calculations.
    print("Parallel results:")
    operations.clear()
    operations.append(Op.bind(Op.Add, 1, 2))
    operations.append(Op.bind(Op.Mul, 3, 4))
    operations.append(Op.bind(Op.Add, 5, 6))
    operations.append(Op.bind(Op.Mul, 7, 8))
    operations.append(Op.bind(Op.Add, 9, 10))

    results = List[Int]()
    results.resize(len(operations), RESIZE_DEFAULT)

    # Nested function for parallel calculations.
    @parameter
    fn calc(index: Int):
        results.insert(index, operations[index]())
        print("results[" + String(index) + "] =", results[index])

    # Perform parallel calculations.
    num_work_items = len(operations)
    num_workers = num_work_items
    parallelize[calc](num_work_items, num_workers)

Output:

Sequential value = 88

Parallel results:
results[0] = 3
results[4] = 19
results[1] = 12
results[3] = 56
results[2] = 11

@EzRyder Thank you/ sir. I see the point is to wrap the function in a struct, this makes it possible to add some debug information of the function. But I prefer implementng them separately, it’s easier to maintain.
The updated code:

alias F = fn(y: Int) escaping -> Int
alias Debug = fn() escaping -> String


@fieldwise_init
struct Wrapper(Movable & Copyable):
	var f: F
	var debug: Debug

	fn __call__(self, b: Int) -> Int:
		return self.f(b)

fn add(x: Int) -> Wrapper:
	fn f(y: Int) -> Int:
		return x + y

	fn debug() -> String:
		return String("+") + x.__str__()

	return Wrapper(f, debug)

fn mul(x: Int) -> Wrapper:
	fn f(y: Int) -> Int:
		return x * y

	fn debug() -> String:
		return String("*") + x.__str__()

	return Wrapper(f, debug)

fn main(): 
	var result = 1

	var list = List(add(7), mul(11))
	for f in list:
		print("operation: " + f[].debug())
		result = f[](result)
	
	print(result)

Output:

operation: +7
operation: *11
88

2 Likes

This isn’t on my radar. What’s the background/timeline for this change?

1 Like

Here is an approach using a trait object and dynamic dispatch.

from memory import ArcPointer

trait Op(Copyable, Movable):
	fn calc(self) -> Int:
		pass

fn calc_trampoline[T: Op](ptr: ArcPointer[NoneType]) -> Int:
    var data = rebind[ArcPointer[T]](ptr)
    return data[].calc()

fn clear_trampoline[T: Op](var ptr: ArcPointer[NoneType]):
    var data = rebind[ArcPointer[T]](ptr^)
    _ = data^

struct DynOp(Op):
	var data: ArcPointer[NoneType]
	var calc_func: fn(ArcPointer[NoneType]) -> Int
	var clear_func: fn(var ArcPointer[NoneType])
	
	fn __init__[T: Op](out self, var ptr: ArcPointer[T]):
		self.data = rebind[ArcPointer[NoneType]](ptr^)
		self.calc_func = calc_trampoline[T]
		self.clear_func = clear_trampoline[T]
	
	fn calc(self) -> Int:
		return self.calc_func(self.data)
	
	fn __del__(var self):
		self.clear_func(self.data^)



@fieldwise_init 
struct Add(Op):
	var a: Int
	var b: Int

	fn calc(self) -> Int:
		return self.a + self.b

@fieldwise_init  
struct Mul(Op):
	var a: Int
	var b: Int

	fn calc(self) -> Int:
		return self.a * self.b


def main():
	var add = ArcPointer(Add(1,2))
	var mul = ArcPointer(Mul(3,4))
	var l: List[DynOp] = [DynOp(add), DynOp(mul)]

	for op in l:
		print(op.calc())

Notice that this implementation is open compared to the one using variant in the sense that you can add new types that conform to the Op trait without changing existing code.

4 Likes