CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-chuusai--shapeless-2-11

An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns

Pending
Overview
Eval results
Files

sybclass.mddocs/

Scrap Your Boilerplate

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.

Core Type Classes

Data - Generic Queries

/**
 * 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
}

DataT - Generic Transformations

/**
 * 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
}

Generic Combinators

Everything - Generic Queries

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 structure

Everywhere - Generic Transformations

The 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"))

Advanced Query Patterns

Selective Queries

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")

Statistical Queries

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 values

Advanced Transformation Patterns

Conditional Transformations

import 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 redacted

Type-specific Transformations

import 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 trimmed

Custom Data Type Support

To 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)

Integration with Other Shapeless Features

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

docs

conversions.md

generic.md

hlist.md

hmap.md

index.md

lift.md

nat.md

poly.md

records.md

sized.md

sybclass.md

typeable.md

typeoperators.md

tile.json