3

today I have learned that rust does not support covariance over an fn parameter only its return type is covariant. (see rust doc)

Why did I learn about that fact in rust? Because I tried to implement a very simple game where I have separated the logic, the event handling, and the drawing in three distinct functions but all operating on the same vector of players.

If this is not possible what would be the equivalent in rust compared to the c# version be?

In C# this is quite simple Fiddle You can define an interface Y that a class X has to implement and define a corresponding delegate which requires as a parameter an IEnumerable of that Interface Y. Now you can share a List of X between different methods that require only an interface Y.

using System;
using System.Collections.Generic;


public interface Actionable{
    void Do();
}

public interface Drawable{
    void Draw();
}

public class Player: Drawable, Actionable{

    public void Do(){
        Console.WriteLine("Action");
    }

    public void Draw(){
        Console.WriteLine("Draw");
    }
}

public class Program
{
    public delegate void DrawHandler(IEnumerable<Drawable> obj);
    public delegate void LogicHandler(IEnumerable<Actionable> obj);

    public static void gameloop(DrawHandler draw,LogicHandler action){

        List<Player> list = new List<Player>(){
            new Player()
        };

        for(int rounds = 0; rounds < 500; rounds++){
            draw(list);
            action(list);
        }

    }
    public static void Main()
    {
        gameloop(
             list =>{
                foreach(var item in list){
                    item.Draw();
                }
            },
            list =>{
                foreach(var item in list){
                    item.Do();
                }
            }
        );
    }
}

Naive as I am, I tried to do something equivalent to that in rust!

trait Drawable {
    fn draw(&self) {
        println!("draw object");
    }
}

trait Actionable {
    fn do_action(&self, action: &String) {
        println!("Do {}", action);
    }
}

#[derive(Debug)]
struct Position {
    x: u32,
    y: u32,
}
impl Position {
    fn new(x: u32, y: u32) -> Position {
        Position { x, y }
    }
}
#[derive(Debug)]
struct Player {
    pos: Position,
    name: String,
}

impl Player {
    fn new(name: String) -> Player {
        Player {
            name,
            pos: Position::new(0, 0),
        }
    }
}

impl Drawable for Player {
    fn draw(&self) {
        println!("{:?}", self);
    }
}

impl Actionable for Player {
    fn do_action(&self, action: &String) {
        println!("Do {} {}!", action, self.name);
    }
}

type DrawHandler = fn(drawables: &Vec<&dyn Drawable>) -> Result<(), String>;
type LogicHandler = fn(actions: &Vec<&dyn Actionable>) -> Result<(), String>;
type EventHandler = fn(events: &mut sdl2::EventPump) -> Result<bool, String>;

fn game_loop(
    window: &mut windowContext,
    draw_handler: DrawHandler,
    event_handler: EventHandler,
    logic_handler: LogicHandler,
) -> Result<(), String> {
    let mut objects: Vec<&Player> = Vec::new();

    objects.push(&Player::new("b".to_string()));

    while event_handler(&mut window.events)? {
        logic_handler(&objects)?; // Does Not work

        window.canvas.clear();

        draw_handler(&objects)?; // Does Not Work
        window.canvas.present();
        ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
    }

    Ok(())
}

If this is not possible what would be the equivalent in rust compared to the c# version be?

I accept that this is not possible in rust. I would like to know what instead is used in rust

ExOfDe
  • 190
  • 1
  • 12
  • You could look at https://github.com/amethyst/amethyst, but this question is far too broad for a SO question, and I will not call your code "simple", on the contrary, I would advice you to keep it simple, don't try to do so much abstraction before you need it. – Stargateur Jun 11 '20 at 22:39
  • 1
    @Stargateur thanks for the link. Are you sure its too broad for SO, because if i see questions like https://stackoverflow.com/questions/141088/what-is-the-best-way-to-iterate-over-a-dictionary?rq=1 I assumed this one would fit. – ExOfDe Jun 11 '20 at 22:56
  • I don't see the link between a basic question such at iterating on a collection, and your question that would require a book to answer. You just pick a random C# question. – Stargateur Jun 11 '20 at 23:05
  • Indeed I did. I think I lack the skill to see the ramification of my question. – ExOfDe Jun 11 '20 at 23:10

2 Answers2

5

In Rust, very few things are done implicitly, which includes casting as you discovered.

In this case, casting a Vec<&T> into a Vec<&dyn Trait> would be impossible (Given that T != dyn Trait), because of how trait objects are stored; they're two pointer-widths wide while normal references are one pointer-width wide. This means the length of the Vec, in bytes would need to be doubled.

I accept that this is not possible in rust. I would like to know what instead is used in rust

If you're only using one kind of object, you can just restrict the type:

type DrawHandler = fn(drawables: &Vec<Player>) -> Result<(), String>;
type LogicHandler = fn(actions: &Vec<Player>) -> Result<(), String>;

However, there most likely won't only be players in your game, and instead you'd like to include other aspects.

This can be done a few ways:

  • Using enums to represent each type of object. Then your function inputs can take values of the type of the enum:
enum GamePiece {
    Player(Player),
    Enemy(Enemy),
    Item(Item),
    //etc.
}
  • Using an ECS which can manage arbitrary objects based on what properties they have. Some ECSs that exist in rust are:

    In general, their usage will look something like this:

struct DrawingComponent {
    buffers: Buffer
}
struct DirectionAI {
    direction: Vector
}
struct Position {
    position: Point
}

let mut world = World::new();
world.insert((DrawingComponent::new(), DirectionAI::new(), Position::new()));

for (pos, direction) in world.iter_over(<(&mut Position, &DirectionAI)>::query()) {
    pos.position += direction.direction;
}
for (pos, drawable) in world.iter_over(<&Position, &mut DrawingComponent>::query()) {
    drawable.buffers.set_position(*pos);
    draw(drawable);
}

In this system, you work generically over components, and not over types. This way, the ECS can store and access items very quickly, and efficiently.


Covariance in Rust does exist. It just isn't OOP covariance, it's covariance over lifetimes. The Rust Nomicon covers it, since it's a bit of a niche thing for day-to-day users.

Note that the table in that section covers variance of 'a, T, and in some cases, U. In the case of T, and U, the variance there, is in any lifetime parameters they may have, and not of the type itself. IE, it describes how 'b is variant (or invariant) in Struct<'b>, and not how Struct<'b> can be cast to dyn Trait + 'b.

Optimistic Peach
  • 3,862
  • 2
  • 18
  • 29
1

Treating a &Player as &dyn Drawable looks like using a subtype in a super type but actually it is a type conversion (both look completely different in memory @Optimistic Peach explained it in more detail).

With this in mind a Vec<Player> cannot be casted into a Vec<&dyn Drawable>, it has to be converted. Your code with an explicit conversion would look like this:

fn game_loop(
    draw_handler: DrawHandler,
    logic_handler: LogicHandler,
) -> Result<(), String> {
    let mut objects: Vec<Player> = Vec::new();

    objects.push(Player::new("b".to_string()));

    for i in 0..1 {
        let actionable = objects.iter().map(|v| v as &dyn Actionable).collect();
        logic_handler(&actionable)?; // Does work!

        let drawables = objects.iter().map(|v| v as &dyn Drawable).collect();
        draw_handler(&drawables)?; // Does work!
    }

    Ok(())
}

This should only demonstrate the consequences of converting &Player to &dyn Drawable - it is not the best way how to solve your problem.

CoronA
  • 7,717
  • 2
  • 26
  • 53