Using generics in switch statements?

Hi all,

This is my first time posting so please let me know if I’m framing this question correctly! This may have been answered somewhere else, but I couldn’t find any documentation on this or tests in Github around this. I was wondering if there was an easy way to use “generics” in switch statements. For example, I’m trying to get better at rescript by creating a Texas Holdem game and this is what I have for comparing scores:

let compare = (score1, score2) => switch(score1, score2) {
  | (#StraitFlush(x), #StraitFlush(y)) => compareFaces(x, y)
  | (#StraitFlush(_), _) => -1
  | (#FourOfAKind(_, _), #StraitFlush(_)) => 1
  | (#FourOfAKind('a, x), #FourOfAKind('a, y)) => compareFaces(x, y)
  | (#FourOfAKind(x, _), #FourOfAKind(y, _)) => compareFaces(x, y)
  | (#FourOfAKind(_, _), _) => -1
  | (#FullHouse(_, _), #StraitFlush(_) | #FourOfAKind(_, _)) => 1
  | (#FullHouse('a, x), #FullHouse('a, y)) => compareFaces(x, y)
  | (#FullHouse(x, _), #FullHouse(y, _)) => compareFaces(x, y)
  | (#FullHouse(_, _), _) => -1
  | (#Flush(_, _, _, _, _), #StraitFlush(_) | #FourOfAKind(_, _) | #FullHouse(_, _)) => 1
  | (#Flush('a, 'b, 'c, 'd, x), #Flush('a, 'b, 'c, 'd, x)) => compareFaces(x, y)
  | (#Flush('a, 'b, 'c, x, _), #Flush('a, 'b, 'c, y, _)) => compareFaces(x, y)
  | (#Flush('a, 'b, x, _, _), #Flush('a, 'b, y, _, _)) => compareFaces(x, y)
  | (#Flush('a, x, _,  _, _), #Flush('a, y, _,  _, _)) => compareFaces(x, y)
  | (#Flush(x, _, _,  _, _), #Flush(y, _, _,  _, _)) => compareFaces(x, y)
  | (#Flush(_, _, _,  _, _), _) => -1
  | (#Strait(_), #StraitFlush(_) | #FourOfAKind(_, _) | #FullHouse(_, _) | #Flush(_, _, _, _, _)) => 1
  | (#Strait(x), #Strait(y)) => compareFaces(x, y)
  | (#Strait(x), _) => -1
  | (#ThreeOfAKind('a, 'b, x), #ThreeOfAKind('a, 'b, y)) => compareFaces(x, y)
  | (#ThreeOfAKind('a, x, _), #ThreeOfAKind('a, y, _)) => compareFaces(x, y)
  | (#ThreeOfAKind(x, _, _), #ThreeOfAKind(y, _, _)) => compareFaces(x, y)
  | (#ThreeOfAKind(_, _, _), #TwoPair(_, _, _) | #Pair(_, _, _, _), #HighCard(_, _, _, _, _)) => -1
  | (#ThreeOfAKind(_, _, _), _) => 1
  | (#TwoPair('a, 'b, x), #TwoPair('a, 'b, y)) => compareFaces(x, y)
  | (#TwoPair('a, x, _), #TwoPair('a, y, _)) => compareFaces(x, y)
  | (#TwoPair(x, _, _), #TwoPair(y, _, _)) => compareFaces(x, y)
  | (#TwoPair(_, _, _), #Pair(_, _, _, _) | #HighCard(_, _, _, _, _)) => -1
  | (#TwoPair(_, _, _), _) => 1
  | (#Pair('a, 'b, 'c, x), #Pair('a, 'b, 'c, y)) => compareFaces(x, y)
  | (#Pair('a, 'b, x, _), #Pair('a, 'b, y, _)) => compareFaces(x, y)
  | (#Pair('a, x, _, _), #Pair('a, y, _, _)) => compareFaces(x, y)
  | (#Pair(x, _, _, _), #Pair(y, _,  _, _)) => compareFaces(x, y)
  | (#Pair(_, _, _, _), #HighCard(_, _, _, _, _)) => -1
  | (#Pair(_, _, _, _), _) => 1
  | (#HighCard('a, 'b, 'c, 'd, x), #HighCard('a, 'b, 'c, 'd, y)) => compareFaces(x, y)
  | (#HighCard('a, 'b, 'c, x, _), #HighCard('a, 'b, 'c, y, _)) => compareFaces(x, y)
  | (#HighCard('a, 'b, x, _, _), #HighCard('a, 'b, y, _, _)) => compareFaces(x, y)
  | (#HighCard('a, x, _, _, _), #HighCard('a, y, _, _, _)) => compareFaces(x, y)
  | (#HighCard(x, _, _, _, _), #HighCard(y, _, _, _, _)) => compareFaces(x, y)
}

As you can see I’m trying to use the generics notation “`a” when I’m trying to tell rescript, “I don’t care what the value is as long as the values are the same”. But this causes a compiler error. Is the only way to do something like this using guard statements? If so, do you know if there is a less verbose of writing this block without a ton of redundant kickerA1 == kickerB1 && kickerA2 == kickerB2 && ...

Thank you so much!

Hi.

Do you think this would be less verbose?

Playground link

let compareFaces = (_, _) => 1
let getFirstUnequal = arr => Js.Array.find(((a, b)) => a != b, arr)
let compare = (score1, score2) =>
  switch (score1, score2) {
  | (#Flush(a, b, c, d, e), #Flush(a1, b1, c1, d1, e1)) =>
    switch getFirstUnequal([(a, a1), (b, b1), (c, c1), (d, d1), (e, e1)]) {
    | Some(a, b) => compareFaces(a, b)
    | None => 1
    }
  }

You are right that there is no way to write this kind of generic.

And it should be written this way:

| (#Flush(a1, b1, c1, d1, x), #Flush(a2, b2, c2, d2, x)) if a1 == a2 && b1 == b2 && c1==c2 && d1 == d2 => compareFaces(x, y)

But note that conditions can be simplified if you order comparison from left to right

  | (#Flush(x, _, _, _, _), #Flush(y, _, _, _, _)) if x != y => compareFaces(x, y)
  | (#Flush(_, x, _, _, _), #Flush(_, y, _, _, _)) if x != y => compareFaces(x, y)
  | (#Flush(_, _, x, _, _), #Flush(_, _, y, _, _)) if x != y => compareFaces(x, y)
  | (#Flush(_, _, _, x, _), #Flush(_, _, _, y, _)) if x != y => compareFaces(x, y)
  | (#Flush(_, _, _, _, x), #Flush(_, _, _, _, y)) if x != y => compareFaces(x, y)
  | (#Flush(_, _, _,  _, _), _) => -1

Hello! And welcome. A few things:

  1. For holdem, the state space is fixed in advance. You should use a regular variant instead of a polymorphic one.
    • Much better error messages since the compiler knows which variant definition you’re referring to, as opposed to thinking that you’re making up a new one at each usage.
    • Much cleaner intent for the programmer.
    • Much cleaner output.
    • The cost is one upfront declaration, but in most cases, like in this one, it serves as good documentation.
  2. You’re confusing a type with a value. 'a is a type parameter, for type declarations. Can’t to use it as a value: it doesn’t exist at runtime. So if you wanted to express that, it should at least have been e.g. (#Flush(a), (#Flush(a)) => .... However…
  3. What you want here is called a nonlinear pattern, which ReScript doesn’t have. It could be convenient in certain cases but generally speaking, equally is a tricky topic (e.g. deep or shallow, customizable or not, what about floats and others, what about perf guarantees).
  4. Like you suspected, our equivalent is:
    | (#Flush(a1), #Flush(a2)) if a1 == a2 => // equal case
    | (#Flush(a1), #Flush(a2)) => // else case
    
  5. Funnily, a language I’ve followed for the longest time has a similar example here (demo broken). it’s a research language though.

That being said, I had a hunch that your compare code didn’t need to pattern match on a tuple. In reality you should just compare the score for equality then pattern match on a single hand score, like so:

let compare = (hand1, hand2) => {
  let hand1Score = RoyalFlush(...)
  let hand2Score = FourOfAKind(...)

  if hand1Score == hand2Score {
    switch hand1Score {
    | RoyalFlush => ...
    | StraightFlush => ...
    }
  } else {
    scoreToIndex(hand1Score) - scoreToIndex(hand2Score)
  }
}

I also suspect that your compareFaces generalization is inadequate. Inside that score pattern match should be inlined, specialized logic; it doesn’t make sense in those branches to use a generic helper with no info on the score.

I’ve rewritten your example as a working playground snippet. Where your compare starts at line 59 (the rest is just to make this whole scenario realistic). I believe it ended up being much more readable and intuitively correct than (ab)using pattern matching on tuple here.

Closing thoughts:

  • Focus on the right data representation and the rest will follow. Find a solution to your problem; don’t try to find a problem (Hold’em scenario) to a solution (pattern matching on tuple). We especially need to prioritize this in our community since folks often like to start with a shiny concept and work backward to a problem.
  • I’ve very grateful that you’ve asked a question with an accompanying concrete scenario. This enabled me to demonstrate a concrete solution. Abstract questions only begets abstract solutions.
  • One obscure benefit of us having designed a language with a clean JS output is that you get a small signal on your source code quality from checking the output quality. If the output is clean then there’s a higher chance that your input is good. This makes sense if you think of the input state space vs output state space, since clean JS output almost always means clean ReScript input. This will serve as a guardrail for your learning as you fumble around various ReScript concepts and need to know whether you’ve made a mess.

Welcome to the community!

2 Likes

Thank you all for the insight and help! This all makes a lot of sense. I’ve created a github gist of the full file of what I have currently (here) and I’ll add a revision once I incorporate this feedback to show the difference for future people.

@chenglou I used a polymorphic variant as I was having trouble created functions that would return a subset of variants (seen in the gist). Is there a way of doing that using normal variants? Or am I thinking about the modeling incorrectly?

Thank you all once again!

Okay, I’ve updated the gist with something that’s working. Would still love feedback if this feels overly complicated or I’m not following proper rescript conventions.

But otherwise it seems to be all working. Thank you all for the help!

Check the playground snippet I’ve sent. Taking the hand out of your variant payload makes everything simpler

From a data structure perspective, adding payload into a variant makes more sense to me, it allows the data type to better represent data coupling.

Otherwise, when separating variant and payload, you end up decoupling your data, which is less expressive, expects the code to be aware of it and makes it less robust.

A typical example here with pattern matching, is that this ends up requiring the use of conditional in matching branches, which is prone to errors in the code logic and also prevents the compiler from being able to check if all the possible matching cases have been covered.

Regarding the original question, types are removed at runtime so you cannot use them for any run-time related check. However, one can think of variants as runtime type annotation so, in fact, you could push your data types even further. I’m thinking something like this:

let compare = (score1, score2) => switch(score1, score2) {
  | (#StraitFlush(x), #StraitFlush(y)) => compareFaces(x, y)
  | (#StraitFlush(_), _) => -1
  | (#FourOfAKind(_, _), #StraitFlush(_)) => 1
  | (#FourOfAKind(#TypeA _, x), #FourOfAKind(#TypeA _, y)) => compareFaces(x, y)
  | (#FourOfAKind(x, _), #FourOfAKind(y, _)) => compareFaces(x, y)

I’m aware of variants and their usage.

Maybe you haven’t seen the playground snippet. There’s a shared payload to be extracted. You shouldn’t carry shared payloads as variant payloads because that does the opposite: signal that said payload is conditional when it’s not.

1 Like