@@ -18,6 +18,23 @@ import scala.language.implicitConversions
1818 * combinations with static type checking, making them ideal for configuration, data transformation, and API integrations where the shape
1919 * of data needs to be flexible but still type-safe.
2020 *
21+ * =Record vs Row=
22+ * Record and [[Row ]] both provide type-safe named fields, but represent different abstractions:
23+ *
24+ * '''Record''' is a '''labeled set''' — fields are unordered, a record with more fields can be used where fewer are expected (structural
25+ * subtyping), and the same name can appear with different types. Use Record when different consumers need different field subsets or when
26+ * composing fields from multiple sources.
27+ *
28+ * '''Row''' is a '''labeled sequence''' — fields have a fixed order, each name is unique, and the type must match exactly. Use Row when
29+ * field order matters, when you need positional operations (head/tail/take/drop), or when working with case classes.
30+ *
31+ * The two interconvert freely:
32+ * {{{
33+ * val record = "name" ~ "Alice" & "age" ~ 30
34+ * val row = Row.fromRecord(record) // Record -> Row (rejects duplicate field names)
35+ * val back = row.toRecord // Row -> Record
36+ * }}}
37+ *
2138 * =Creation=
2239 * Records can be created through direct field construction using the `~` operator and combined with `&`:
2340 * {{{
@@ -128,22 +145,9 @@ export Record.`~`
128145object Record :
129146 /** Creates an empty Record
130147 */
131- val empty : Record [Any ] = Record [Any ](Map ())
132-
133- /** Returns the set of fields in this Record.
134- *
135- * @return
136- * A Set of Field instances
137- */
138- def fieldsOf [Fields ](record : Record [Fields ]): Set [Field [? , ? ]] =
139- record.toMap.keySet
140-
141- /** Returns the number of fields in this Record.
142- */
143- def sizeOf [Fields ](record : Record [Fields ]): Int =
144- record.toMap.size
148+ val empty : Record [Any ] = Record [Any ](Map .empty)
145149
146- private def unsafeFrom [Fields ](map : Map [Field [? , ? ], Any ]): Record [Fields ] = Record (map)
150+ private [kyo] def unsafeFrom [Fields ](map : Map [Field [? , ? ], Any ]): Record [Fields ] = Record (map)
147151
148152 inline def stage [Fields ]: StageOps [Fields ] = new StageOps [Fields ](())
149153
@@ -172,13 +176,31 @@ object Record:
172176
173177 type MapValue [F [_]] = Map [[n, v] =>> F [v]]
174178
175- type MapName [F [_]] = Map [[n, v] =>> F [n]]
176179 end `~`
177180
181+ type FieldsOf [Names <: Tuple , Values <: Tuple ] = (Names , Values ) match
182+ case (EmptyTuple , EmptyTuple ) => Any
183+ case (n *: EmptyTuple , v *: EmptyTuple ) => n ~ v
184+ case (n *: ns, v *: vs) => (n ~ v) & FieldsOf [ns, vs]
185+
186+ type ZipLookup [N <: String , T <: Tuple ] = T match
187+ case (N ~ v) *: _ => v
188+ case _ *: rest => ZipLookup [N , rest]
189+
190+ type ZipFields [T1 <: Tuple , T2 <: Tuple ] = T1 match
191+ case EmptyTuple => Any
192+ case (n ~ v1) *: EmptyTuple => n ~ (v1, ZipLookup [n, T2 ])
193+ case (n ~ v1) *: rest => (n ~ (v1, ZipLookup [n, T2 ])) & ZipFields [rest, T2 ]
194+
178195 /** Creates a Record from a product type (case class or tuple).
179196 */
180197 def fromProduct [A ](value : A )(using ar : AsRecord [A ]): Record [ar.Fields ] = ar.asRecord(value)
181198
199+ /** Creates a Record from a Row.
200+ */
201+ inline def fromRow [A <: NamedTuple .AnyNamedTuple ](row : Row [A ]): Record [Row .ToRecordFields [Row .Names [A ], Row .Values [A ]]] =
202+ row.toRecord
203+
182204 /** A field in a Record, containing a name and associated type information.
183205 *
184206 * @param name
@@ -189,11 +211,54 @@ object Record:
189211 case class Field [Name <: String , Value ](name : Name , tag : Tag [Value ])
190212
191213 extension [Fields ](self : Record [Fields ])
192- /** Creates a new Record containing only the fields specified in the type parameter Fields.
193- *
194- * @return
195- * A new Record with only the specified fields
196- */
214+
215+ def size : Int = self.toMap.size
216+
217+ inline def fields (using ti : TypeIntersection [Fields ]): List [String ] =
218+ collectFieldNames[ti.AsTuple ]
219+
220+ inline def values (using ti : TypeIntersection [Fields ]): Row .FieldValues [ti.AsTuple ] =
221+ collectValues[ti.AsTuple ](self.toMap).asInstanceOf [Row .FieldValues [ti.AsTuple ]]
222+
223+ def update [Name <: String & Singleton , Value ](name : Name , value : Value )(using
224+ @ implicitNotFound("""
225+ Invalid field update: ${Name}
226+
227+ Record[${Fields}]
228+
229+ Possible causes:
230+ 1. The field does not exist in this Record
231+ 2. The field exists but has a different type than expected
232+ """ )
233+ ev : Fields <:< Name ~ Value ,
234+ tag : Tag [Value ]
235+ ): Record [Fields ] =
236+ Record .unsafeFrom(self.toMap.updated(Field (name, tag), value))
237+
238+ inline def map [F [_]](using
239+ ti : TypeIntersection [Fields ]
240+ )(
241+ f : [t] => t => F [t]
242+ ): Record [ti.Map [~ .MapValue [F ]]] =
243+ mapFields([t] => (_ : Field [? , t], v : t) => f[t](v))
244+
245+ inline def mapFields [F [_]](using
246+ ti : TypeIntersection [Fields ]
247+ )(
248+ f : [t] => (Field [? , t], t) => F [t]
249+ ): Record [ti.Map [~ .MapValue [F ]]] =
250+ Record .unsafeFrom[ti.Map [~ .MapValue [F ]]](
251+ mapFieldsImpl[ti.AsTuple , F ](self.toMap, f)
252+ )
253+
254+ inline def zip [Fields2 ](other : Record [Fields2 ])(using
255+ ti1 : TypeIntersection [Fields ],
256+ ti2 : TypeIntersection [Fields2 ]
257+ ): Record [ZipFields [ti1.AsTuple , ti2.AsTuple ]] =
258+ Record .unsafeFrom[ZipFields [ti1.AsTuple , ti2.AsTuple ]](
259+ zipImpl[ti1.AsTuple , ti2.AsTuple ](self.toMap, other.toMap)
260+ )
261+
197262 def compact (using AsFields [Fields ]): Record [Fields ] =
198263 Record (self.toMap.view.filterKeys(AsFields [Fields ].contains(_)).toMap)
199264 end extension
@@ -226,46 +291,37 @@ object Record:
226291
227292 object AsRecord :
228293
229- type FieldsOf [Names <: Tuple , Values <: Tuple ] = Names match
230- case nHead *: EmptyTuple => Values match
231- case vHead *: _ => (nHead ~ vHead)
232- case nHead *: nTail => Values match
233- case vHead *: vTail => (nHead ~ vHead) & FieldsOf [nTail, vTail]
234- case _ => Any
235-
236294 type RMirror [A , Names <: Tuple , Values <: Tuple ] = Mirror .ProductOf [A ] {
237295 type MirroredElemLabels = Names
238296 type MirroredElemTypes = Values
239297 }
240298
241299 trait RecordContents [Names <: Tuple , Values <: Tuple ]:
242- def values (product : Tuple ) : List [( Field [? , ? ], Any ) ]
300+ def addTo (product : Product , idx : Int , map : Map [ Field [? , ? ], Any ]) : Map [ Field [ ? , ? ], Any ]
243301
244302 object RecordContents :
245303 given empty : RecordContents [EmptyTuple , EmptyTuple ] with
246- def values (product : Tuple ) : List [( Field [? , ? ], Any )] = Nil
304+ def addTo (product : Product , idx : Int , map : Map [ Field [? , ? ], Any ]) : Map [ Field [ ? , ? ], Any ] = map
247305
248306 given nonEmpty [NH <: (String & Singleton ), NT <: Tuple , VH , VT <: Tuple ](
249307 using
250308 tag : Tag [VH ],
251309 vo : ValueOf [NH ],
252310 next : RecordContents [NT , VT ]
253311 ): RecordContents [NH *: NT , VH *: VT ] with
254- def values (product : Tuple ) : List [( Field [? , ? ], Any ) ] =
255- ( Field [NH , VH ](vo.value, tag), product.head) +: next.values(product.tail )
312+ def addTo (product : Product , idx : Int , map : Map [ Field [? , ? ], Any ]) : Map [ Field [ ? , ? ], Any ] =
313+ next.addTo(product, idx + 1 , map.updated( Field [NH , VH ](vo.value, tag), product.productElement(idx)) )
256314 end nonEmpty
257315 end RecordContents
258316
259317 given [A <: Product , Names <: Tuple , Values <: Tuple ](using
260318 mir : RMirror [A , Names , Values ],
261319 rc : RecordContents [Names , Values ]
262320 ): AsRecord [A ] with
263- type Fields = FieldsOf [Names , Values ]
321+ type Fields = Record . FieldsOf [Names , Values ]
264322
265323 def asRecord (value : A ): Record [Fields ] =
266- val record_contents = rc.values(Tuple .fromProduct(value))
267- val map = Map (record_contents* )
268- Record (map).asInstanceOf [Record [Fields ]]
324+ Record (rc.addTo(value, 0 , Map .empty)).asInstanceOf [Record [Fields ]]
269325 end asRecord
270326 end given
271327 end AsRecord
@@ -318,20 +374,16 @@ object Record:
318374 inline def apply [T ]: (String , Render [? ]) =
319375 inline erasedValue[T ] match
320376 case _ : (n ~ v) =>
321- val ev = summonInline[n <:< String ]
322- val inst = summonInline[Render [v]]
323- ev(constValue[n]) -> inst
377+ constValue[n & String ] -> summonInline[Render [v]]
324378 end RenderInliner
325379
326380 inline given [Fields : TypeIntersection ]: Render [Record [Fields ]] =
327381 val insts = TypeIntersection .inlineAll[Fields ](RenderInliner ).toMap
328382 Render .from: (value : Record [Fields ]) =>
329- value.toMap.foldLeft(Vector [String ]()) { case (acc, (field, value)) =>
330- insts.get(field.name) match
331- case Some (r : Render [x]) =>
332- acc :+ (field.name + " ~ " + r.asText(value.asInstanceOf [x]))
333- case None => acc
334- end match
383+ value.toMap.iterator.collect {
384+ case (field, v) if insts.contains(field.name) =>
385+ val r = insts(field.name).asInstanceOf [Render [Any ]]
386+ field.name + " ~ " + r.asText(v)
335387 }.mkString(" & " )
336388 end given
337389
@@ -350,6 +402,54 @@ object Record:
350402 ForSome2 (stage[n, v](Field (name, prevTag)))
351403 )
352404 end StageAs
405+
406+ private inline def collectFieldNames [T <: Tuple ]: List [String ] =
407+ inline erasedValue[T ] match
408+ case _ : EmptyTuple => Nil
409+ case _ : ((n ~ v) *: rest) =>
410+ constValue[n & String ] :: collectFieldNames[rest]
411+ case _ : (_ *: rest) =>
412+ collectFieldNames[rest]
413+
414+ private inline def collectValues [T <: Tuple ](
415+ map : Map [Field [? , ? ], Any ]
416+ ): Tuple =
417+ inline erasedValue[T ] match
418+ case _ : EmptyTuple => EmptyTuple
419+ case _ : ((n ~ v) *: rest) =>
420+ val name = constValue[n & String ]
421+ val tag = summonInline[Tag [v]]
422+ map(Field (name, tag)) *: collectValues[rest](map)
423+
424+ private inline def mapFieldsImpl [T <: Tuple , F [_]](
425+ map : Map [Field [? , ? ], Any ],
426+ f : [t] => (Field [? , t], t) => F [t]
427+ ): Map [Field [? , ? ], Any ] =
428+ inline erasedValue[T ] match
429+ case _ : EmptyTuple => Map .empty
430+ case _ : ((n ~ v) *: rest) =>
431+ val name = constValue[n & String ]
432+ val prevTag = summonInline[Tag [v]]
433+ val nextTag = summonInline[Tag [F [v]]]
434+ val field = Field (name, prevTag)
435+ val value = map(field).asInstanceOf [v]
436+ val result = f[v](field, value)
437+ mapFieldsImpl[rest, F ](map, f).updated(Field (name, nextTag), result)
438+
439+ private inline def zipImpl [T1 <: Tuple , T2 <: Tuple ](
440+ map1 : Map [Field [? , ? ], Any ],
441+ map2 : Map [Field [? , ? ], Any ]
442+ ): Map [Field [? , ? ], Any ] =
443+ inline erasedValue[T1 ] match
444+ case _ : EmptyTuple => Map .empty
445+ case _ : ((n ~ v1) *: rest) =>
446+ val name = constValue[n & String ]
447+ val tag1 = summonInline[Tag [v1]]
448+ val tag2 = summonInline[Tag [ZipLookup [n, T2 ]]]
449+ val pairTag = summonInline[Tag [(v1, ZipLookup [n, T2 ])]]
450+ val value1 = map1(Field (name, tag1))
451+ val value2 = map2(Field (name, tag2))
452+ zipImpl[rest, T2 ](map1, map2).updated(Field (name, pairTag), (value1, value2))
353453end Record
354454
355455object AsFieldsInternal :
0 commit comments