Search code examples
scalatypesshapeless

Type safe dominoes in scala


So I'm trying to write a dominoes game server, and I'm writing my core types, tiles and sets of dominoes and it occurs to me that including type information for the tile pips would allow me to write a much simpler function that creates chains of dominoes, I've started and left this project incomplete several times due to my being unable to figure this out, hoping someone has a simple type safe tile representation that leads to a simple domino chain function. the reason being is my current mental model is a board in a dominoes games is just an initial tile and 1-3 chains of dominoes each beginning and matching the pips on the initial tile.

Thanks so much in advance, and apologies for any imperfections in my question.

sealed case class DoubleSix[L >: Nat, R <: Nat](lPips: Int, rPips: Int) extends Tile[L, R]

object DoubleSixSet {
  val zeroZero: DoubleSix[_0, _0] = DoubleSix(0, 0)

}

an older attempt at the type safe chaining function.

trait DominoChain[End] {
    // val hasSpinner: Boolean
}
case object End extends DominoChain[Nothing] {
    // val hasSpinner = false
}
case class  Chain[D0, D1, X](head: Domino[D0, D1], tail: DominoChain[Domino[D1, X]]) extends DominoChain[Domino[D0, D1]] {
     def hasSpinner =
         if (head.isDouble || rest.hasSpinner) true
         else false
}

Solution

  • As you probably noticed expressing dominoes in types is easy:

    sealed trait One
    sealed trait Two
    sealed trait Three
    sealed trait Four
    sealed trait Five
    sealed trait Six
    
    sealed trait Domino[A, B] extends Product with Serializable
    object Domino {
        case object OneOne[One, One]
        case object OneTwo[One, Two]
        ... // the other types of dominoes
    }
    

    If you want to have a linear chain it is also easy:

    sealed trait Chain[A, B] extends Product with Serializable
    object Chain {
      case class One[A, B](domino: Domino[A, B]) extends Chain[A, B]
      case class Prepend[A, B, C](head: Domino[A, B], tail: Chain[B, C]) extends Chain[A, C]
    }
    

    Things gets tricky if this is not linear though. You might want to make a turn. There is more than one way of doing this:

    xxyy
    
     yy
    xx
     
    xx
     yy
    
    xx
     y
     y
    
     y
     y
    xx
    

    and each of them would have to be expressed as a separate case. If you would like to avoid things like:

      f <- f tile would have to be over or under bb tile
    aabbc  f
      e c
      edd
    

    you would have to somehow detect such case and prevent it. You have 2 options here:

    • don't express it in type, express it as value and use some smart constructor which would calculate if your move is valid and return chain with added tile or error
    • express each turn as a different type, express it in type level and require some evidence in order to create a tile. This should be possible but much much harder, and it would require you to know the exact type in compile time (so adding tile dynamically on demand could be harder as you would have to have that evidence prepared upfront for each move)

    But in domino besides turns we can also have branches:

    aab
      bdd
     cc
    

    If you wanted to express it in type, now you have a two heads you can prepend to (and one tail you can append to). And during the game you could have more of them, so you would have to somehow express both: how many branches you have but also to which one do you want to add a new tile. Still possible, but but complicates your code even further.

    You could e.g. express heads with some sort of HList (if you are using shapeless) and use that representation to provide implicit telling you which element of HList you want to modify.

    However at this point you have very little benefits of type-level programming: you have to know you types ahead of time, you would have difficulties adding new tiles dynamically, you would have to persist state in such a way you would be able to retrieve the exact type so that type-level evidence would work...

    Because of that I suggest an approach which is still type-safe while much easier to approach: just use smart constructors:

    type Position = UUID
    
    sealed trait Chain extends Product with Serializable
    object Chain {
      // prevent user from accessing constructors and copy directly
      sealed abstract case class One private (
        domino: Domino,
        position: Position
      ) extends Chain
      sealed abstract case class PrependLine private (
        domino: Domino,
        position: Position,
        chain:  Chain
      )
      sealed abstract case class Branch private (
        chain1: Chain,
        chain2: Chain
      )
    
      def start(domino: Domino): Chain
    
      // check if you can add domino at this position, recursively rewrite tree
      // if needed to add it at the right branch or maybe even create a new branch
      def prepend(domino: Domino, to: Chain, at: Position): Either[Error, Chain]
    }
    

    This will still make it impossible to create an "invalid" domino chain. At the same time it will be much easier to add new rules, expand functionalities and persist state between requests (you mentioned that you want to build a server).