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

hmap.mddocs/

Heterogeneous Maps (HMap)

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.

Core Type

HMap Definition

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

HMap Factory

object HMap {
  def apply[R[_, _]] = new HMapBuilder[R]
  def empty[R[_, _]] = new HMap[R]
  def empty[R[_, _]](underlying: Map[Any, Any]) = new HMap[R]
}

Type-Level Relations

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

Basic Operations

Get Operation

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

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

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

Polymorphic Function Integration

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

Advanced Usage Patterns

Type-safe Configuration

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

Multi-type Registry

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]

Witness Relations

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)

Building HMaps

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

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