Twiddle Lists

Twiddle lists are tuples that are built incrementally, element by element, via the *: operation. Skunk uses the Typelevel Twiddles library for parameter encoders and row decoders, and via this mechanism twiddle list types appear as type arguments for Query and Command.

val q: Query[Short *: String *: String *: Int *: EmptyTuple] =
  sql"""
    SELECT name, age
    FROM   person
    WHERE  age < $int2
    AND    status = $
  """.query(varchar *: int4)

On Scala 3, *: and EmptyTuple come from the standard library and are exactly the same as a n-argument tuple. For example, we could equivalently write the type of q as Query[(Short, String, String, Int)] (though not on Scala 2, more on that in a moment). Similarly, we can construct a 4-tuple using *: and EmptyTuple.

val t1: (Short, String, String, Int) = (42.toShort, "Edgar", "Dijkstra", 100)
val t2: (Short, String, String, Int) = 42.toShort *: "Edgar" *: "Dijkstra" *: 100 *: EmptyTuple
val t3: Short *: String *: String *: Int *: EmptyTuple = 42.toShort *: "Edgar" *: "Dijkstra" *: 100 *: EmptyTuple
val t4: Short *: String *: String *: Int *: EmptyTuple = (42.toShort, "Edgar", "Dijkstra", 100)

On Scala 2, the Twiddles library defines *: and EmptyTuple as aliases for Shapeless HLists. Those polyfills allow the previous examples to compile on Scala 2 (including t1 through t4). Twiddles includes implicit conversions between twiddle lists (i.e., HLists) and regular tuples.

Isomorphism with Case Classes

A twiddle list of type (A, B, C) has the same structure as a case class with fields of type A, B, and C, and we can trivially map back and forth.

case class Person(name: String, age: Int, active: Boolean)

def fromTwiddle(t: String *: Int *: Boolean *: EmptyTuple): Person =
  t match {
    case s *: n *: b *: EmptyTuple => Person(s, n, v)
  }

def toTwiddle(p: Person): String *: Int *: Boolean *: EmptyTuple =
  p.name *: p.age *: p.active *: EmptyTuple

Because this mapping is useful and entirely mechanical, the Twiddles library provides a typeclass that does it for you.

@ val bob = Person("Bob", 42, true)
bob: Person = Person("Bob", 42, true)

@ val iso = Iso.product[Person]
iso: org.typelevel.twiddles.Iso[Person,String *: Int *: Boolean *: org.typelevel.twiddles.EmptyTuple] = org.typelevel.twiddles.Iso$$anon$1@84d9646

@ val twiddle = iso.to(bob)
twiddle: String *: Int *: Boolean *: org.typelevel.twiddles.EmptyTuple = Bob :: 42 :: true :: HNil

@ val bob2 = iso.from(twiddle)
bob2: Person = Person("Bob", 42, true)

Decoders, Encoders, and Codecs use this facility to provide to, which allows quick adaptation of a twiddle-list Codec (for instance) to one that maps to/from a case class.

@ val codec = varchar *: int4 *: bool
codec: Codec[String *: Int *: Boolean *: EmptyTuple] = Codec(varchar, int4, bool)

// Since `Person` has the same structure we can use `to` to create a `Codec[Person]`
@ val personCode = codec.to[Person]
personCodec: Codec[Person] = Codec(varchar, int4, bool)

Legacy Twiddle Lists

Prior to Skunk 0.6, a twiddle list was defined as a left-associated chain of pairs.

val x: (((Int, String), Boolean), Double) =
  (((42, "hi"), true), 1.23)

Twiddle lists were built using the ~ operator.

type ~[+A, +B] = (A, B)

final class IdOps[A](a: A) {
  def ~[B](b: B): A ~ B = (a, b)
}

// ~ is left-associative so this is exactly the same as `x` above.
val x: Int ~ String ~ Boolean ~ Double =
  42 ~ "hi" ~ true ~ 1.23

And thanks to the following definition, we could also pattern-match.

object ~ {
  def unapply[A, B](t: A ~ B): Some[A ~ B] = Some(t)
}

// Deconstruct `x`
x match {
  case n ~ s ~ b ~ d => ...
}

Skunk's Codec, Decoder, and Encoder types provided special methods for converting twiddle lists to case classes (gimap, gmap, gcontramap respectively). The to operation replaces all of these twiddle specific conversions.

Legacy Command Syntax

Prior to Skunk 0.6, parameterized commands used (left-nested) twiddle lists. Upon upgrading to Skunk 0.6, such commands use the new style twiddle lists. To ease migration, you can add an import to restore the old behavior until it's convenient to upgrade to new style twiddles.

import skunk.feature.legacyCommandSyntax

val insertCityLegacy: Command[City] =
  sql"""
      INSERT INTO city
      VALUES ($int4, $varchar, ${bpchar(3)}, $varchar, $int4)
    """.command.contramap {
          c => c.id ~ c.name ~ c.code ~ c.district ~ c.pop
        }

The skunk.feature.legacyCommandSyntax import ensures the command returned by the sql interpolator uses legacy twiddles. Without the import, the type of the command is Command[Int *: String *: String *: String *: Int *: EmptyTuple] and the function passed to contramap would need to be adjusted to account for the new style twiddle.

val insertCity2: Command[City] =
  sql"""
      INSERT INTO city
      VALUES ($int4, $varchar, ${bpchar(3)}, $varchar, $int4)
    """.command.contramap {
          c => (c.id, c.name, c.code, c.district, c.pop)
        }