An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Heterogeneous maps (HMap) in shapeless provide type-safe maps where keys and values can have different types, with the relationships between key types and value types enforced at compile time through a type-level relation.
/**
* Heterogeneous map with type-level key/value associations fixed by relation R
* Also extends Poly, making HMaps polymorphic function values
*/
class HMap[R[_, _]](underlying: Map[Any, Any] = Map.empty) extends Poly {
def get[K, V](k: K)(implicit ev: R[K, V]): Option[V]
def +[K, V](kv: (K, V))(implicit ev: R[K, V]): HMap[R]
def -[K](k: K): HMap[R]
}object HMap {
def apply[R[_, _]] = new HMapBuilder[R]
def empty[R[_, _]] = new HMap[R]
def empty[R[_, _]](underlying: Map[Any, Any]) = new HMap[R]
}HMaps are parameterized by a type-level relation R[_, _] that constrains which key types can be associated with which value types.
Usage Examples:
import shapeless._
// Define a relation: Strings map to Ints, Ints map to Strings
class BiMapIS[K, V]
implicit val stringToInt = new BiMapIS[String, Int]
implicit val intToString = new BiMapIS[Int, String]
// Create HMap with this relation
val hm = HMap[BiMapIS](23 -> "foo", "bar" -> 13)
// Type-safe access - return types are inferred
val s1: Option[String] = hm.get(23) // Some("foo")
val i1: Option[Int] = hm.get("bar") // Some(13)
// This would not compile - violates the relation:
// val hm2 = HMap[BiMapIS](23 -> "foo", 23 -> 13) // Error: 23 can't map to Int/**
* Type-safe retrieval - return type determined by relation R
*/
def get[K, V](k: K)(implicit ev: R[K, V]): Option[V]Usage Examples:
import shapeless._
// Relation for demo: Symbols map to their string names
class SymbolNames[K, V]
implicit val symbolToString = new SymbolNames[Symbol, String]
val syms = HMap[SymbolNames]('name -> "Alice", 'age -> "30", 'active -> "true")
val name: Option[String] = syms.get('name) // Some("Alice")
val age: Option[String] = syms.get('age) // Some("30")
val missing: Option[String] = syms.get('missing) // None
// This would not compile - wrong type association:
// val invalid: Option[Int] = syms.get('name) // Error: Symbol doesn't map to Int/**
* Add key-value pair (type-safe according to relation R)
*/
def +[K, V](kv: (K, V))(implicit ev: R[K, V]): HMap[R]Usage Examples:
import shapeless._
// Relation: different numeric types map to their string representations
class NumericToString[K, V]
implicit val intToString = new NumericToString[Int, String]
implicit val doubleToString = new NumericToString[Double, String]
implicit val longToString = new NumericToString[Long, String]
val empty = HMap.empty[NumericToString]
val hm1 = empty + (42 -> "forty-two")
val hm2 = hm1 + (3.14 -> "pi")
val hm3 = hm2 + (999L -> "nine-nine-nine")
val intVal: Option[String] = hm3.get(42) // Some("forty-two")
val doubleVal: Option[String] = hm3.get(3.14) // Some("pi")
val longVal: Option[String] = hm3.get(999L) // Some("nine-nine-nine")/**
* Remove key from map
*/
def -[K](k: K): HMap[R]Usage Examples:
import shapeless._
class StringToAny[K, V]
implicit def stringToAny[V] = new StringToAny[String, V]
val hm = HMap[StringToAny]("name" -> "Bob", "age" -> 30, "active" -> true)
val withoutAge = hm - "age"
val name: Option[String] = withoutAge.get("name") // Some("Bob")
val age: Option[Int] = withoutAge.get("age") // None
val active: Option[Boolean] = withoutAge.get("active") // Some(true)Since HMap extends Poly, it can be used as a polymorphic function:
/**
* Implicit conversion makes HMap act as polymorphic function
*/
implicit def caseRel[K, V](implicit ev: R[K, V]) = Case1Aux[HMap[R], K, V](k => get(k).get)Usage Examples:
import shapeless._
class ConfigMap[K, V]
implicit val stringToInt = new ConfigMap[String, Int]
implicit val stringToString = new ConfigMap[String, String]
implicit val stringToBool = new ConfigMap[String, Boolean]
val config = HMap[ConfigMap](
"port" -> 8080,
"host" -> "localhost",
"debug" -> true
)
// Use as polymorphic function with HList
val keys = "port" :: "host" :: "debug" :: HNil
val values = keys.map(config) // 8080 :: "localhost" :: true :: HNil
// Individual function calls
val port: Int = config("port") // 8080 (unsafe - throws if missing)
val host: String = config("host") // "localhost"
val debug: Boolean = config("debug") // trueimport shapeless._
// Define configuration relation
trait ConfigKey[T] { type Out = T }
case object Port extends ConfigKey[Int]
case object Host extends ConfigKey[String]
case object Debug extends ConfigKey[Boolean]
case object Timeout extends ConfigKey[Long]
class Config[K <: ConfigKey[_], V]
implicit def configRelation[K <: ConfigKey[V], V] = new Config[K, V]
val config = HMap[Config](
Port -> 9000,
Host -> "api.example.com",
Debug -> false,
Timeout -> 30000L
)
val port: Option[Int] = config.get(Port) // Some(9000)
val host: Option[String] = config.get(Host) // Some("api.example.com")
val debug: Option[Boolean] = config.get(Debug) // Some(false)
// Update configuration
val devConfig = config + (Debug -> true) + (Port -> 3000)import shapeless._
// Registry relation - classes map to their instances
class Registry[K, V]
implicit def classToInstance[T] = new Registry[Class[T], T]
trait Service
class DatabaseService extends Service
class EmailService extends Service
class LoggingService extends Service
val services = HMap[Registry](
classOf[DatabaseService] -> new DatabaseService,
classOf[EmailService] -> new EmailService,
classOf[LoggingService] -> new LoggingService
)
def getService[T](implicit tag: Class[T]): Option[T] = services.get(tag)
val dbService: Option[DatabaseService] = getService[DatabaseService]
val emailService: Option[EmailService] = getService[EmailService]import shapeless._
// Use singleton types as witnesses for relations
trait Witness[K, V]
val dbKey = "database"
val cacheKey = "cache"
val queueKey = "queue"
implicit val dbWitness = new Witness[dbKey.type, DatabaseConfig]
implicit val cacheWitness = new Witness[cacheKey.type, CacheConfig]
implicit val queueWitness = new Witness[queueKey.type, QueueConfig]
case class DatabaseConfig(host: String, port: Int)
case class CacheConfig(maxSize: Int, ttl: Long)
case class QueueConfig(workers: Int, timeout: Long)
val systemConfig = HMap[Witness](
dbKey -> DatabaseConfig("localhost", 5432),
cacheKey -> CacheConfig(1000, 3600),
queueKey -> QueueConfig(4, 30000)
)
val dbConfig: Option[DatabaseConfig] = systemConfig.get(dbKey)
val cacheConfig: Option[CacheConfig] = systemConfig.get(cacheKey)import shapeless._
// HMapBuilder provides convenient construction
class TypeMapping[K, V]
implicit val stringToInt = new TypeMapping[String, Int]
implicit val stringToString = new TypeMapping[String, String]
implicit val stringToBool = new TypeMapping[String, Boolean]
// Use apply method for building
val builder = HMap[TypeMapping]
val hmap = builder("count" -> 100, "name" -> "test", "enabled" -> true)
// Chain operations
val extended = hmap + ("timeout" -> 5000) - "count"HMaps provide a powerful abstraction for type-safe heterogeneous data storage while maintaining the benefits of compile-time type checking and enabling sophisticated type-level programming patterns.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11