#unsure how to represent this data (coming from typescript)

43 messages · Page 1 of 1 (latest)

slender belfry
#

i'm trying to learn rust by porting an old typescript project of mine, and i'm struggling to figure out how i want to structure my data. i hope this isn't too long/specific/verbose...

background: i'm building 4 library crates. 3 implement different glicko rating systems (glicko, glicko-2, glicko-boost), and the 4th is a dependency containing shared logic and other common stuff. you can choose to install any of the 3 systems, and you always need the shared lib.

in my ts version, all players had a rating and rating deviation, but glicko-2 players also required a 3rd value called sigma, like so:

export interface Player {
    r: number;
    RD: number;
    sigma?: number;
}
export interface FullPlayer extends Player {
    sigma: number;
}

this let common functions accept Player (you can still pass FullPlayer because duck typing), while functions that required sigma could ask for FullPlayer.

unfortunately it does not end there. ratings can exist on 2 different scales, like Kelvin vs °C. the internal glicko-2 scale is better for computation, but less human readable. all 3 algorithms operated on this internal scale.
-# (if you've ever looked into the glicko papers before, that last statement might seem wrong, but dw about it)

when processing many millions of matches (which can absolutely happen since glicko is batched), there's a certain cost to converting every user in every match between the scales, at least twice. so allowing users to just store them in the internal scale and only convert when displaying data would allow skipping unnecessary conversions.

because of this, my ts implementation also defined:

export interface ScaledPlayer {
    mu: number;
    phi: number;
    sigma?: number;
}
export interface ScaledFullPlayer extends ScaledPlayer {
    sigma: number;
}

most functions accepted Player | ScaledPlayer and/or FullPlayer | ScaledFullPlayer and returned the same type they were passed, using funky overloads to preserve the scale.


this is obviously not something i can translate directly into rust, but i'm not sure what the right approach is...

  • sigma being required by some functions could be modeled with Option<f64>, but that seems to trade compile-time safety for runtime checks
  • allowing players to exist in either scale (and skipping conversions when possible) seems hard, and i don't think rust has union types like ts. there might be some way using generics and phantomdata, but i haven't really figured that out

what would be the idiomatic rust way to handle this? i expect i'll have to do things very differently

fading roost
#

Have you considered making the functions which need sigma as a trait? I have no idea what sigma/glicko is, but maybe you can share some of the functions you use which use sigma that accept Player | ScaledPlayer and/or FullPlayer | ScaledFullPlayer?

An example might be something like:

trait SigmaStuff {
    fn calculate_some_sigma_thing(&self) -> SigmaResult;
}

impl FullPlayer for ScaledFullPlayer { ... }
impl SigmaStuff for ScaledFullPlayer { ... }

But honestly, it seems like Option<f64> may very well be valid and simplest too. But I think it could be useful if you could share some example uses of the sigma related stuff

slender belfry
#

sigma is just a number, roughly representing the "consistency" of a player's performance. it grows if a player experiences a sudden streak of wins or losses, and decreases if the player is performing as expected. just like with rating and rating deviation, it is a parameter for the rating algorithm, and a new value is computed after each rating period

for example this is the signature of the function that computes glicko-1 ratings (simplified, there's some additional params that aren't important here. we don't need to worry about Match either, it's comparatively easy to model)
function rate_g1 (player: ScaledPlayer | Player, matches?: Match[]): ScaledPlayer | Player {...}
and this is the sig of the equivalent function in glicko-2. note that this one requires a volatility property to exist on players
function rate_g2 (player: ScaledPlayerWithVol | PlayerWithVol, matches?: Match[]): ScaledPlayerWithVol | PlayerWithVol {...}

#

as far as i understand, traits are specific to methods, right? i guess i could implement something like get_sigma(&self) -> f64, a getter, and make a trait for that?

stuck yew
#

if you only require sigma why don't you just pass sigma directly to a function

#

for example for the type: FullPlayer | ScaledFullPlayer the only thing both type have in common is just the sigma variable

slender belfry
#

no that's just slightly weird ts syntax

export interface Player {
    r: number;
    RD: number;
    sigma?: number;
}
export interface FullPlayer extends Player {
    sigma: number;
}

is exactly equivalent to

export interface Player {
    r: number;
    RD: number;
    sigma?: number;
}
export interface FullPlayer {
    r: number;
    RD: number;
    sigma: number;
}
#

basically it overrides the optional value with a mandatory one, retaining everything else. i think it's similar to @override in java?

stuck yew
#

yeah I know

#

but r and RD aren't in ScaleFullPlayer no?

slender belfry
#

they have different names so we can figure out what type a player is. since ts does duck typing and doesn't care about the "name" of your type, you need to discern it by its properties

// did we get a player that's already in glicko-2 scale?
const isScaled = 'mu' in player;
// if not, fix it
// aka. step 2
const { mu, phi, sigma } = isScaled ? player : scalePlayer(player);
#

like r and mu are both rating, just in glicko-1 and glicko-2 scale respectively

#

(yea this is all very spaghetti but it works!)

stuck yew
#

I don't understand why don't you just scale the player before passing it to the function

slender belfry
#

since this is a public function in my library, it's mostly for the convenience of my (hypothetical) users

#

the thing is that most people who only have a superficial understanding of the system won't really know what the meaning of the scales is, so i wanted to make it such that they can just pass in a player that has the human-readable values they're used to

stuck yew
#

oh so use an enum no

#

and before calling the function you just scale it

slender belfry
#

hm, how does that work?

stuck yew
#

something like:

struct Player {
  r: f32,
  RD: f32
}
struct FullPlayer {
  r: f32,
  RD: f32,
  sigma: f32,
}
struct ScaledPlayer {
  mu: f32,
  phi: f32,
 }
struct ScaledFullPlayer {
  mu: f32,
  phi: f32,
  sigma: f32,
}
enum PlayerKind {
  Normal(Player),
  Full(FullPlayer),
  Scaled(ScaledPlayer),
  ScaledFull(ScaledFullPlayer),
}
#

then you can use a match branch with PlayerKind

#

but it depends on how many player you have

#

how many player do you have?

slender belfry
#

well... like a few million xD

stuck yew
#

how many different type of player do you have

#

don't tell me you wrote a few million type by hand

slender belfry
#

oh no ok

#

yea i think those 4 would do

stuck yew
#

ok then just use PlayerKind

slender belfry
#

how would a function signature look with that? is PlayerKind used as a parameter or?

stuck yew
#

so basically playerkind represent all the different type of players at the same time

#

and you need to check which type you currently pass to the function

slender belfry
#

hmm

stuck yew
#

using an match arm it's easy however if you want to make sure the player is only certain type it won't be possible at compile time

#

that's the caveat

slender belfry
#

ok i think i understand, let's see

#

so assuming we have a function rate(player: &Player) -> () (figuring out returns later)
we could do something like

fn rate_any(player_kind: &PlayerKind) {
    match player_kind {
        PlayerKind::Normal(p) => rate(p),
        PlayerKind::Full(p) => rate(&Player { r: p.r, RD: p.RD }),
        PlayerKind::Scaled(p) => rate(&unscale_player(p)),
        PlayerKind::ScaledFull(p) => {
            let full = unscale_full_player(p); // could probably do something with into() here instead idk
            rate(&Player { r: full.r, RD: full.RD })
        }
    }
}
#

and if we needed something like FullScaledPlayer or whatever we could like throw an error in the arms where that's not the case

stuck yew
slender belfry
#

mh, either way this is definitely progress, i should be able to get it working like this!

#

thank you for your help!

stuck yew
#

yw