An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Shapeless provides type-safe bidirectional conversions between various Scala data structures including tuples, functions, and HLists. These conversions maintain type safety and enable seamless interoperability between different representations of the same data.
/**
* Converts Products (tuples) to HLists
*/
trait HLister[T <: Product] {
type Out <: HList
def apply(t: T): Out
}/**
* Converts HLists to tuples (when possible)
*/
trait Tupler[L <: HList] {
type Out <: Product
def apply(l: L): Out
}/**
* Provides enhanced operations for tuple conversions
*/
object Tuples {
trait TupleOps[L <: HList] {
def hlisted: L
}
implicit def tupleOps[T <: Product](t: T)(implicit hlister: HLister[T]): TupleOps[hlister.Out]
object hlisted extends Poly1 // Converts Tuples to HLists
object tupled extends Poly1 // Converts HLists to Tuples
}Usage Examples:
import shapeless._
import syntax.std.tuple._
// Convert tuple to HList
val tuple = (42, "hello", true)
val hlist = tuple.hlisted // Int :: String :: Boolean :: HNil
// Convert HList to tuple
val backToTuple = hlist.tupled // (Int, String, Boolean) = (42, "hello", true)
// Polymorphic conversion
import Tuples._
val pairs = ((1, "a"), (2, "b"), (3, "c"))
val hlistPairs = (pairs._1.hlisted, pairs._2.hlisted, pairs._3.hlisted)
// (1 :: "a" :: HNil, 2 :: "b" :: HNil, 3 :: "c" :: HNil)/**
* Witnesses the arity of a Product type
*/
trait ProductArity[P <: Product] {
type N <: Nat // Arity as natural number
}Usage Examples:
import shapeless._
// Get arity information at type level
def showArity[P <: Product, N <: Nat]
(p: P)
(implicit arity: ProductArity.Aux[P, N], toInt: ToInt[N]): Int = toInt()
val pair = (1, "hello")
val triple = (1, "hello", true)
val pairArity = showArity(pair) // 2
val tripleArity = showArity(triple) // 3/**
* Converts ordinary functions to HList functions
*/
trait FnHLister[F] {
type Out
def apply(f: F): Out
}/**
* Converts HList functions to ordinary functions
*/
trait FnUnHLister[F] {
type Out
def apply(f: F): Out
}/**
* Provides enhanced operations for function conversions
*/
object Functions {
trait FnHListOps[HLFn] {
def hlisted: HLFn
}
trait FnUnHListOps[F] {
def unhlisted: F
}
}Usage Examples:
import shapeless._
import syntax.std.function._
// Convert regular function to HList function
val add: (Int, Int) => Int = _ + _
val hlistAdd = add.hlisted // (Int :: Int :: HNil) => Int
// Apply HList function
val args = 5 :: 3 :: HNil
val result = hlistAdd(args) // 8
// Convert back to regular function
val regularAdd = hlistAdd.unhlisted // (Int, Int) => Int
val result2 = regularAdd(7, 2) // 9
// More complex example
val processData: (String, Int, Boolean) => String =
(name, age, active) => s"$name is $age years old and ${if (active) "active" else "inactive"}"
val hlistProcessor = processData.hlisted
val data = "Alice" :: 25 :: true :: HNil
val processed = hlistProcessor(data) // "Alice is 25 years old and active"Shapeless provides function conversion support for functions of arity 0 through 22:
import shapeless._
import syntax.std.function._
// Nullary function
val getValue: () => String = () => "constant"
val hlistGetValue = getValue.hlisted // HNil => String
val value = hlistGetValue(HNil) // "constant"
// Unary function
val double: Int => Int = _ * 2
val hlistDouble = double.hlisted // (Int :: HNil) => Int
val doubled = hlistDouble(5 :: HNil) // 10
// Higher arity functions
val combine5: (Int, String, Boolean, Double, Char) => String =
(i, s, b, d, c) => s"$i-$s-$b-$d-$c"
val hlistCombine5 = combine5.hlisted
val args5 = 42 :: "test" :: true :: 3.14 :: 'x' :: HNil
val combined = hlistCombine5(args5) // "42-test-true-3.14-x"/**
* Type-safe conversion from Traversables to HLists
*/
trait FromTraversable[Out <: HList] {
def apply(l: GenTraversable[_]): Option[Out]
}/**
* Provides enhanced operations for traversable conversions
*/
object Traversables {
trait TraversableOps {
def toHList[L <: HList](implicit fl: FromTraversable[L]): Option[L]
}
}Usage Examples:
import shapeless._
import syntax.std.traversable._
// Convert List to HList with specific type structure
val mixedList: List[Any] = List(42, "hello", true)
val hlistOpt: Option[Int :: String :: Boolean :: HNil] = mixedList.toHList
hlistOpt match {
case Some(hlist) =>
val i: Int = hlist.head // 42
val s: String = hlist.tail.head // "hello"
val b: Boolean = hlist.tail.tail.head // true
case None =>
println("Conversion failed - type mismatch")
}
// Safe conversion - returns None on type mismatch
val wrongTypes: List[Any] = List("not", "an", "int")
val failedConversion: Option[Int :: String :: Boolean :: HNil] = wrongTypes.toHList
// failedConversion: None
// Convert collections of uniform type
val numbers = List(1, 2, 3)
val numberHList: Option[Int :: Int :: Int :: HNil] = numbers.toHListShapeless provides automatic conversions between equivalent representations:
import shapeless._
// Case class to HList (requires Generic)
case class Person(name: String, age: Int, active: Boolean)
val person = Person("Bob", 30, true)
// Conversion to HList happens through Generic (covered in generic.md)
// Automatic tuple/HList interop in operations
val tuple = (1, "hello", true)
val hlist = 42 :: "world" :: false :: HNil
// Can zip tuple with HList (after conversion)
val tupleAsHList = tuple.hlisted
val zipped = tupleAsHList.zip(hlist)
// (1, 42) :: ("hello", "world") :: (true, false) :: HNilimport shapeless._
import syntax.std.tuple._
// Complex conversion pipeline
val data = ((1, "a"), (2, "b"), (3, "c"))
// Convert tuple of tuples to HList of HLists
val step1 = data.hlisted // (1, "a") :: (2, "b") :: (3, "c") :: HNil
val step2 = step1.map(hlisted) // Would require proper Poly1 setup
// Practical example with functions
val processor: ((String, Int)) => String = { case (name, age) => s"$name:$age" }
val hlistProcessor = processor.hlisted // ((String :: Int :: HNil)) => String
val inputs = ("Alice" :: 25 :: HNil) :: ("Bob" :: 30 :: HNil) :: HNil
// Process each input (would need proper mapping setup)import shapeless._
// Convert only when types match exactly
def safeConvert[T <: Product, L <: HList]
(product: T)
(implicit hlister: HLister.Aux[T, L]): L = hlister(product)
val tuple2 = (42, "hello")
val tuple3 = (42, "hello", true)
val hlist2 = safeConvert(tuple2) // Int :: String :: HNil
val hlist3 = safeConvert(tuple3) // Int :: String :: Boolean :: HNilimport shapeless._
import syntax.std.tuple._
import syntax.std.function._
// Round-trip conversion maintains types
def roundTrip[T <: Product, L <: HList]
(t: T)
(implicit
hlister: HLister.Aux[T, L],
tupler: Tupler.Aux[L, T]): T = {
val hlist = t.hlisted
// Could perform HList operations here
hlist.tupled
}
val original = (1, "test", true)
val afterRoundTrip = roundTrip(original)
// afterRoundTrip has same type and value as originalimport shapeless._
// Validate conversions at compile time
def validateConversion[T <: Product, L <: HList, T2 <: Product]
(t: T)
(implicit
hlister: HLister.Aux[T, L],
tupler: Tupler.Aux[L, T2],
ev: T =:= T2): T2 = {
t.hlisted.tupled
}
// This ensures the round-trip conversion preserves exact types
val validated = validateConversion((1, "hello"))
// Type is exactly (Int, String)import shapeless._
import syntax.std.tuple._
import syntax.std.traversable._
// Convert collection of tuples to collection of HLists
val tupleList = List((1, "a"), (2, "b"), (3, "c"))
val hlistList = tupleList.map(_.hlisted)
// List[Int :: String :: HNil]
// Process and convert back
val processedHLists = hlistList.map(_.reverse)
val backToTuples = processedHLists.map(_.tupled)
// List[("a", 1), ("b", 2), ("c", 3)]Type-safe conversions in shapeless enable seamless interoperability between different data representations while maintaining compile-time safety and type inference. This allows you to choose the most appropriate representation for each operation while avoiding runtime type errors.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11