An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Shapeless provides an implementation of "Scrap Your Boilerplate with Class" that enables generic queries and transformations over arbitrarily nested data structures. This allows you to write functions that work uniformly across different data types without explicit recursion.
/**
* Type class for generic queries over data structures
* F - query function type, T - data type, R - result type
*/
trait Data[F, T, R] {
def gmapQ(f: F, t: T): R
}/**
* Type class for generic transformations over data structures
* F - transformation function type, T - data type
*/
trait DataT[F, T] {
def gmapT(f: F, t: T): T
}The everything combinator applies a query function everywhere in a data structure and combines results:
/**
* Apply query function everywhere and combine results
*/
def everything[F <: Poly](f: F)(implicit ...): ...Usage Examples:
import shapeless._
case class Person(name: String, age: Int, address: Address)
case class Address(street: String, city: String, zip: String)
case class Company(name: String, employees: List[Person])
val person = Person("Alice", 30, Address("123 Main St", "Boston", "02101"))
val company = Company("Acme Corp", List(
Person("Bob", 25, Address("456 Oak Ave", "Cambridge", "02139")),
Person("Carol", 35, Address("789 Pine St", "Somerville", "02144"))
))
// Query to count all strings in a data structure
object countStrings extends Poly1 {
implicit def caseString = at[String](_ => 1)
implicit def default[T] = at[T](_ => 0)
}
// Count strings in person record
val personStringCount = person.everything(countStrings)
// Result: counts "Alice", "123 Main St", "Boston", "02101" = 4
// Count strings in company structure
val companyStringCount = company.everything(countStrings)
// Result: counts all string fields across the entire structureThe everywhere combinator applies a transformation function everywhere in a data structure:
/**
* Apply transformation function everywhere in data structure
*/
def everywhere[F <: Poly](f: F)(implicit ...): ...Usage Examples:
import shapeless._
// Transformation to uppercase all strings
object uppercaseStrings extends Poly1 {
implicit def caseString = at[String](_.toUpperCase)
implicit def default[T] = at[T](identity)
}
val person = Person("Alice", 30, Address("123 Main St", "Boston", "02101"))
val uppercasedPerson = person.everywhere(uppercaseStrings)
// Result: Person("ALICE", 30, Address("123 MAIN ST", "BOSTON", "02101"))
// Transformation to increment all integers
object incrementInts extends Poly1 {
implicit def caseInt = at[Int](_ + 1)
implicit def default[T] = at[T](identity)
}
val incrementedPerson = person.everywhere(incrementInts)
// Result: Person("Alice", 31, Address("123 Main St", "Boston", "02101"))import shapeless._
case class Product(id: Int, name: String, price: Double, categories: List[String])
case class Order(id: Int, products: List[Product], total: Double)
val order = Order(1001, List(
Product(1, "Widget", 19.99, List("tools", "hardware")),
Product(2, "Gadget", 29.99, List("electronics", "gadgets"))
), 49.98)
// Query to find all prices
object findPrices extends Poly1 {
implicit def caseDouble = at[Double](d => List(d))
implicit def default[T] = at[T](_ => List.empty[Double])
}
val allPrices = order.everything(findPrices)
// Result: List(19.99, 29.99, 49.98) - all Double values
// Query to collect all strings longer than 5 characters
object findLongStrings extends Poly1 {
implicit def caseString = at[String](s => if (s.length > 5) List(s) else List.empty)
implicit def default[T] = at[T](_ => List.empty[String])
}
val longStrings = order.everything(findLongStrings)
// Result: List("Widget", "Gadget", "electronics", "gadgets", "hardware")import shapeless._
// Query to compute statistics
object computeStats extends Poly1 {
case class Stats(count: Int, sum: Double, min: Double, max: Double)
implicit def caseDouble = at[Double](d => Stats(1, d, d, d))
implicit def caseInt = at[Int](i => Stats(1, i.toDouble, i.toDouble, i.toDouble))
implicit def default[T] = at[T](_ => Stats(0, 0.0, Double.MaxValue, Double.MinValue))
// Monoid for combining stats
implicit val statsMonoid = new Monoid[Stats] {
def zero = Stats(0, 0.0, Double.MaxValue, Double.MinValue)
def append(a: Stats, b: Stats) = Stats(
a.count + b.count,
a.sum + b.sum,
math.min(a.min, b.min),
math.max(a.max, b.max)
)
}
}
val stats = order.everything(computeStats)
// Result: Stats with count, sum, min, max of all numeric valuesimport shapeless._
case class User(id: Int, name: String, email: String, active: Boolean)
case class Department(name: String, users: List[User])
val dept = Department("Engineering", List(
User(1, "alice", "alice@example.com", true),
User(2, "bob", "bob@example.com", false),
User(3, "carol", "carol@example.com", true)
))
// Transform to normalize names and emails
object normalizeData extends Poly1 {
implicit def caseString = at[String] { s =>
if (s.contains("@")) s.toLowerCase // Email addresses
else s.split(" ").map(_.capitalize).mkString(" ") // Names
}
implicit def default[T] = at[T](identity)
}
val normalizedDept = dept.everywhere(normalizeData)
// Result: names capitalized, emails lowercased
// Transform to anonymize sensitive data in inactive users
object anonymizeInactive extends Poly1 {
implicit def caseUser = at[User] { user =>
if (!user.active) user.copy(name = "REDACTED", email = "REDACTED")
else user
}
implicit def default[T] = at[T](identity)
}
val anonymizedDept = dept.everywhere(anonymizeInactive)
// Result: inactive users have name and email redactedimport shapeless._
case class Document(title: String, content: String, tags: List[String], wordCount: Int)
case class Section(heading: String, documents: List[Document])
case class Library(name: String, sections: List[Section])
val library = Library("Technical Library", List(
Section("Programming", List(
Document("Scala Guide", "Scala is...", List("scala", "programming"), 1500),
Document("Haskell Intro", "Haskell is...", List("haskell", "functional"), 2000)
)),
Section("Mathematics", List(
Document("Linear Algebra", "Vectors and...", List("math", "algebra"), 3000)
))
))
// Transform to update word counts and normalize tags
object updateLibrary extends Poly1 {
implicit def caseDocument = at[Document] { doc =>
val actualWordCount = doc.content.split("\\s+").length
val normalizedTags = doc.tags.map(_.toLowerCase.trim)
doc.copy(wordCount = actualWordCount, tags = normalizedTags)
}
implicit def caseString = at[String](_.trim) // Trim whitespace from strings
implicit def default[T] = at[T](identity)
}
val updatedLibrary = library.everywhere(updateLibrary)
// Result: word counts updated, tags normalized, strings trimmedTo use SYB with custom data types, you need to provide appropriate type class instances:
import shapeless._
case class Tree[T](value: T, children: List[Tree[T]])
// Provide Data instance for Tree
implicit def treeData[F <: Poly, T, R](implicit
fT: F.Case1.Aux[T, R],
dataList: Data[F, List[Tree[T]], R],
monoid: Monoid[R]
): Data[F, Tree[T], R] = new Data[F, Tree[T], R] {
def gmapQ(f: F, tree: Tree[T]): R = {
val valueResult = f(tree.value)
val childrenResult = dataList.gmapQ(f, tree.children)
monoid.append(valueResult, childrenResult)
}
}
// Provide DataT instance for Tree
implicit def treeDataT[F <: Poly, T](implicit
fT: F.Case1.Aux[T, T],
dataListT: DataT[F, List[Tree[T]]]
): DataT[F, Tree[T]] = new DataT[F, Tree[T]] {
def gmapT(f: F, tree: Tree[T]): Tree[T] = {
val newValue = f(tree.value)
val newChildren = dataListT.gmapT(f, tree.children)
Tree(newValue, newChildren)
}
}
val tree = Tree("root", List(
Tree("child1", List(Tree("leaf1", Nil), Tree("leaf2", Nil))),
Tree("child2", List(Tree("leaf3", Nil)))
))
// Now can use everything/everywhere with Tree
val stringCount = tree.everything(countStrings)
val uppercased = tree.everywhere(uppercaseStrings)SYB works well with other shapeless features:
import shapeless._
// Use with HLists
val data = 42 :: "hello" :: true :: 3.14 :: HNil
val stringCount = data.everything(countStrings) // 1 (for "hello")
val uppercased = data.everywhere(uppercaseStrings) // 42 :: "HELLO" :: true :: 3.14 :: HNil
// Use with Records
val record = ("name" ->> "Alice") :: ("age" ->> 30) :: ("active" ->> true) :: HNil
val recordStringCount = record.everything(countStrings)
val recordUppercased = record.everywhere(uppercaseStrings)Scrap Your Boilerplate provides powerful generic programming capabilities that eliminate the need for writing repetitive traversal code, enabling concise and type-safe operations over complex nested data structures.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11