0
# Heterogeneous Maps (HMap)
1
2
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.
3
4
## Core Type
5
6
### HMap Definition
7
8
```scala { .api }
9
/**
10
* Heterogeneous map with type-level key/value associations fixed by relation R
11
* Also extends Poly, making HMaps polymorphic function values
12
*/
13
class HMap[R[_, _]](underlying: Map[Any, Any] = Map.empty) extends Poly {
14
def get[K, V](k: K)(implicit ev: R[K, V]): Option[V]
15
def +[K, V](kv: (K, V))(implicit ev: R[K, V]): HMap[R]
16
def -[K](k: K): HMap[R]
17
}
18
```
19
20
### HMap Factory
21
22
```scala { .api }
23
object HMap {
24
def apply[R[_, _]] = new HMapBuilder[R]
25
def empty[R[_, _]] = new HMap[R]
26
def empty[R[_, _]](underlying: Map[Any, Any]) = new HMap[R]
27
}
28
```
29
30
## Type-Level Relations
31
32
HMaps are parameterized by a type-level relation `R[_, _]` that constrains which key types can be associated with which value types.
33
34
**Usage Examples:**
35
36
```scala
37
import shapeless._
38
39
// Define a relation: Strings map to Ints, Ints map to Strings
40
class BiMapIS[K, V]
41
implicit val stringToInt = new BiMapIS[String, Int]
42
implicit val intToString = new BiMapIS[Int, String]
43
44
// Create HMap with this relation
45
val hm = HMap[BiMapIS](23 -> "foo", "bar" -> 13)
46
47
// Type-safe access - return types are inferred
48
val s1: Option[String] = hm.get(23) // Some("foo")
49
val i1: Option[Int] = hm.get("bar") // Some(13)
50
51
// This would not compile - violates the relation:
52
// val hm2 = HMap[BiMapIS](23 -> "foo", 23 -> 13) // Error: 23 can't map to Int
53
```
54
55
## Basic Operations
56
57
### Get Operation
58
59
```scala { .api }
60
/**
61
* Type-safe retrieval - return type determined by relation R
62
*/
63
def get[K, V](k: K)(implicit ev: R[K, V]): Option[V]
64
```
65
66
**Usage Examples:**
67
68
```scala
69
import shapeless._
70
71
// Relation for demo: Symbols map to their string names
72
class SymbolNames[K, V]
73
implicit val symbolToString = new SymbolNames[Symbol, String]
74
75
val syms = HMap[SymbolNames]('name -> "Alice", 'age -> "30", 'active -> "true")
76
77
val name: Option[String] = syms.get('name) // Some("Alice")
78
val age: Option[String] = syms.get('age) // Some("30")
79
val missing: Option[String] = syms.get('missing) // None
80
81
// This would not compile - wrong type association:
82
// val invalid: Option[Int] = syms.get('name) // Error: Symbol doesn't map to Int
83
```
84
85
### Add Operation
86
87
```scala { .api }
88
/**
89
* Add key-value pair (type-safe according to relation R)
90
*/
91
def +[K, V](kv: (K, V))(implicit ev: R[K, V]): HMap[R]
92
```
93
94
**Usage Examples:**
95
96
```scala
97
import shapeless._
98
99
// Relation: different numeric types map to their string representations
100
class NumericToString[K, V]
101
implicit val intToString = new NumericToString[Int, String]
102
implicit val doubleToString = new NumericToString[Double, String]
103
implicit val longToString = new NumericToString[Long, String]
104
105
val empty = HMap.empty[NumericToString]
106
val hm1 = empty + (42 -> "forty-two")
107
val hm2 = hm1 + (3.14 -> "pi")
108
val hm3 = hm2 + (999L -> "nine-nine-nine")
109
110
val intVal: Option[String] = hm3.get(42) // Some("forty-two")
111
val doubleVal: Option[String] = hm3.get(3.14) // Some("pi")
112
val longVal: Option[String] = hm3.get(999L) // Some("nine-nine-nine")
113
```
114
115
### Remove Operation
116
117
```scala { .api }
118
/**
119
* Remove key from map
120
*/
121
def -[K](k: K): HMap[R]
122
```
123
124
**Usage Examples:**
125
126
```scala
127
import shapeless._
128
129
class StringToAny[K, V]
130
implicit def stringToAny[V] = new StringToAny[String, V]
131
132
val hm = HMap[StringToAny]("name" -> "Bob", "age" -> 30, "active" -> true)
133
val withoutAge = hm - "age"
134
135
val name: Option[String] = withoutAge.get("name") // Some("Bob")
136
val age: Option[Int] = withoutAge.get("age") // None
137
val active: Option[Boolean] = withoutAge.get("active") // Some(true)
138
```
139
140
## Polymorphic Function Integration
141
142
Since HMap extends Poly, it can be used as a polymorphic function:
143
144
```scala { .api }
145
/**
146
* Implicit conversion makes HMap act as polymorphic function
147
*/
148
implicit def caseRel[K, V](implicit ev: R[K, V]) = Case1Aux[HMap[R], K, V](k => get(k).get)
149
```
150
151
**Usage Examples:**
152
153
```scala
154
import shapeless._
155
156
class ConfigMap[K, V]
157
implicit val stringToInt = new ConfigMap[String, Int]
158
implicit val stringToString = new ConfigMap[String, String]
159
implicit val stringToBool = new ConfigMap[String, Boolean]
160
161
val config = HMap[ConfigMap](
162
"port" -> 8080,
163
"host" -> "localhost",
164
"debug" -> true
165
)
166
167
// Use as polymorphic function with HList
168
val keys = "port" :: "host" :: "debug" :: HNil
169
val values = keys.map(config) // 8080 :: "localhost" :: true :: HNil
170
171
// Individual function calls
172
val port: Int = config("port") // 8080 (unsafe - throws if missing)
173
val host: String = config("host") // "localhost"
174
val debug: Boolean = config("debug") // true
175
```
176
177
## Advanced Usage Patterns
178
179
### Type-safe Configuration
180
181
```scala
182
import shapeless._
183
184
// Define configuration relation
185
trait ConfigKey[T] { type Out = T }
186
case object Port extends ConfigKey[Int]
187
case object Host extends ConfigKey[String]
188
case object Debug extends ConfigKey[Boolean]
189
case object Timeout extends ConfigKey[Long]
190
191
class Config[K <: ConfigKey[_], V]
192
implicit def configRelation[K <: ConfigKey[V], V] = new Config[K, V]
193
194
val config = HMap[Config](
195
Port -> 9000,
196
Host -> "api.example.com",
197
Debug -> false,
198
Timeout -> 30000L
199
)
200
201
val port: Option[Int] = config.get(Port) // Some(9000)
202
val host: Option[String] = config.get(Host) // Some("api.example.com")
203
val debug: Option[Boolean] = config.get(Debug) // Some(false)
204
205
// Update configuration
206
val devConfig = config + (Debug -> true) + (Port -> 3000)
207
```
208
209
### Multi-type Registry
210
211
```scala
212
import shapeless._
213
214
// Registry relation - classes map to their instances
215
class Registry[K, V]
216
implicit def classToInstance[T] = new Registry[Class[T], T]
217
218
trait Service
219
class DatabaseService extends Service
220
class EmailService extends Service
221
class LoggingService extends Service
222
223
val services = HMap[Registry](
224
classOf[DatabaseService] -> new DatabaseService,
225
classOf[EmailService] -> new EmailService,
226
classOf[LoggingService] -> new LoggingService
227
)
228
229
def getService[T](implicit tag: Class[T]): Option[T] = services.get(tag)
230
231
val dbService: Option[DatabaseService] = getService[DatabaseService]
232
val emailService: Option[EmailService] = getService[EmailService]
233
```
234
235
### Witness Relations
236
237
```scala
238
import shapeless._
239
240
// Use singleton types as witnesses for relations
241
trait Witness[K, V]
242
243
val dbKey = "database"
244
val cacheKey = "cache"
245
val queueKey = "queue"
246
247
implicit val dbWitness = new Witness[dbKey.type, DatabaseConfig]
248
implicit val cacheWitness = new Witness[cacheKey.type, CacheConfig]
249
implicit val queueWitness = new Witness[queueKey.type, QueueConfig]
250
251
case class DatabaseConfig(host: String, port: Int)
252
case class CacheConfig(maxSize: Int, ttl: Long)
253
case class QueueConfig(workers: Int, timeout: Long)
254
255
val systemConfig = HMap[Witness](
256
dbKey -> DatabaseConfig("localhost", 5432),
257
cacheKey -> CacheConfig(1000, 3600),
258
queueKey -> QueueConfig(4, 30000)
259
)
260
261
val dbConfig: Option[DatabaseConfig] = systemConfig.get(dbKey)
262
val cacheConfig: Option[CacheConfig] = systemConfig.get(cacheKey)
263
```
264
265
### Building HMaps
266
267
```scala
268
import shapeless._
269
270
// HMapBuilder provides convenient construction
271
class TypeMapping[K, V]
272
implicit val stringToInt = new TypeMapping[String, Int]
273
implicit val stringToString = new TypeMapping[String, String]
274
implicit val stringToBool = new TypeMapping[String, Boolean]
275
276
// Use apply method for building
277
val builder = HMap[TypeMapping]
278
val hmap = builder("count" -> 100, "name" -> "test", "enabled" -> true)
279
280
// Chain operations
281
val extended = hmap + ("timeout" -> 5000) - "count"
282
```
283
284
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.