Search code examples
c#rustcovariance

What to use for covariance over fn parameter in Rust?


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


Solution

  • 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.