I would like to use a Tuple or HList of something like:
val cols = (2,String) +: (2,Int) +: EmptyTuple
Is this possible?
TIA
I would like to use a Tuple or HList of something like:
val cols = (2,String) +: (2,Int) +: EmptyTuple
Is this possible?
TIA
What exactly do you want to accomplish?
The idea is to create a description of a number of objects to create. So in the example above, I would create a new HLlist of 2 strings followed by 2 integers when passing that to a function (simplified example).
Of course I could pass instances, but was wondering if I could just define the types. I have found that we have in Scala an Int (companion?) object So for now i will use instances of such objects.
Ok, so basically a run-length encoded builder spec?
(2,String) +: (2,Int) +: EmptyTuple
specifies a value of type
String *: String *: Int *: Int *: EmptyTuple
…?
Instances of what?
This doesn’t sound quite right. The Int
object only has a quite arbitrary relationship with the Int
type, and I don’t see how this approach generalizes to arbitrary types. I’d rather expect to carry the actual type (and/or a builder abstraction parametrized with this type) - as a phantom type, type member, TypeTag
/ClassTag
/Class
,…
Also, I could imagine some kind of pulling down to the value level when working with collections of some kind. But when working with tuples, I’d assume you need to express the exact type of the target tuple, which rather seems to call for lifting the numeric spec to the type level in some way…?
Perhaps it’d help if you provided a minimalist draft of how you intend to use your approach in practice.
Hmm. Keep in mind that types per se don’t really exist at runtime – they’re a compiler concept. At runtime you have classes instead: closely related, but not the same thing. (And as @sangamon says, companion objects are a red herring – they really have nothing much to do with this.)
That said, classes may be good enough for your purposes. So you might be able to do something like:
val cols = (2,classOf[String]) +: (2,classOf[Int]) +: EmptyTuple
Not the most elegant thing in the world, but you might be able to use reflection via those Class
objects to instantiate what you need? Really depends on the details of what you’re trying to do, though: getting everything to line up properly isn’t necessarily easy.
@sangamon Thanks for the feedback. Your conclusion that:
is correct.
For now I am only interested in the primitive types, but will later need to consider numeric types such as BigDecimal. But, yes I agree with you. Not a good solution.
Here is an excerpt:
type ColumnsOf[N,T] <: HList = (N,T) match
case (0,_) => HNil
case (N,String) => Strings &: ColumnsOf[int.-[N, 1], String]
case (N,Int) => Ints &: ColumnsOf[int.-[N, 1], Int]
case (N,Float) => Floats &: ColumnsOf[int.-[N, 1], Float]
case (N,Double) => Doubles &: ColumnsOf[int.-[N, 1], Double]
transparent inline def replicate01[T](n:Int): ColumnsOf[n.type,T] =
inline if n == 0
then
HNil.asInstanceOf[ColumnsOf[n.type,T]]
else
inline val next = n - 1
val tail : ColumnsOf[next.type, T] = replicate01[T](next)
val acc = HCons(Ints, tail)
acc.asInstanceOf[ColumnsOf[n.type,T]]
In the example above I map a Tuple
to an HList
. I am just experimenting with one element of the initial tuple. Each 2nd element of the Tuple2
is mapped to a new object. In the example above I am simply mapping to the same object, but the goal is to create a specific object based on a type. So an Int
is mapped to an instance of a class Ints
, a String
an instance of class Strings, and so on. Essentially these new objects represent data columns.
Here is a usage example:
val rep1 = replicate01[Int](4)
summon[rep1.type <:< &:[Table.Ints, &:[Table.Ints, &:[Table.Ints, &:[Table.Ints, HNil]]]] ]
Later I would go trough the initial “spec” tuple and use costValue
to deal with the first numeric type parameter to do the counting.
I think I am going to scrap this idea and see if I can come up with another solution.
@jducoeur appreciate the input.
Yes I agree, hence this question.
I neglected to mention I am using Scala 3. I think I could use quotes to do that. Regardless, I am going to see if I can do this another way without requiring this “spec” as explicit input.
Disclaimer: I’m not at all fluent with the new Scala 3 type level and metaprogramming features, so this is just me tinkering along.
What feels a bit suspicious to me in your code is the casts and the use of the companion objects as match discriminators.
Assuming that both target types and column counts are fixed at compile time, I’ve come up with a naive type level approach. In a nutshell:
Gen
for generating instances.Nat
for the column count.GenSpec
type.GenTarget
match type for the corresponding result type.GenBuilder
type class that recursively assembles a builder chain for the target type.import scala.compiletime.ops.int.S as IntSucc
import Tuple.*
trait Gen[A]:
def make(): A
object Gen:
import scala.util.*
given Gen[String] = () => Random.alphanumeric.take(8).mkString
given Gen[Int] = () => Random.nextInt
sealed trait Nat
object Nat:
sealed trait Z extends Nat
sealed trait S[N <: Nat] extends Nat
import Nat.*
type FromInt[I <: Int] <: Nat =
I match
case 0 => Z
case IntSucc[n] => S[FromInt[n]]
trait GenSpec[A, N <: Nat]
type GenTarget[G] <: Tuple =
G match
case GenSpec[?, Z] => EmptyTuple
case GenSpec[a, S[n]] => a *: GenTarget[GenSpec[a, n]]
case EmptyTuple => EmptyTuple
case h *: t => Concat[GenTarget[h], GenTarget[t]]
trait GenBuilder[G]:
def build(): GenTarget[G]
object GenBuilder:
given GenBuilder[EmptyTuple] = () => EmptyTuple
given[A]: GenBuilder[GenSpec[A, Z]] = () => EmptyTuple
given[A: Gen, N <: Nat](
using tailBld: GenBuilder[GenSpec[A, N]]
): GenBuilder[GenSpec[A, S[N]]] =
() => summon[Gen[A]].make() *: tailBld.build()
given[H: GenBuilder, T <: Tuple : GenBuilder]: GenBuilder[H *: T] =
() => summon[GenBuilder[H]].build() ++ summon[GenBuilder[T]].build()
def make[G: GenBuilder]: GenTarget[G] = summon[GenBuilder[G]].build()
type Cols =
GenSpec[String, FromInt[2]] *: GenSpec[Int, FromInt[3]] *: EmptyTuple
println(make[Cols])
// e.g. (SYyCt9fZ,IQTxx7yl,-295525142,1972992256,1047809665)
Not sure whether this makes sense at all, whether it would hold water in a production scenario and whether it matches your use case.
Any improvement suggestions are appreciated.
@sangamon Very nice. Worked as advertised. I find it unusual that you use the given
with the =
instead of the with
. Also, I was unaware that the one could define lambda functions.
I agree. But this seems to also be used in the Scala 3 library code. Try as I might, I could not get rid of these. Other people have also commented on this.
Thanks
Right. Never really thought about this, but probably it’s because so far I don’t really see the advantage of with
over alias givens that justifies the surplus syntax.
This is not specific to givens, it’s generic SAM syntax.
std lib precedence is not an entirely convincing argument - it also quite often employs imperative constructs behind a functional interface. Which is perfectly fine, but should ideally be restricted to library code in tight performance spots, IMHO.
But yeah, sometimes there really doesn’t seem to be a way around casting with tuples, or at least it may simplify things a lot. Still, requiring casts when doing type level stuff feels somewhat awkward.
@sangamon Thanks for the information above. As for the with
I think its useful when you want to define multiple methods for the same type.
given Headed[List] with
def head[A](f: List[A]): Option[A] = f.headOption
def tail[A](f: List[A]): Option[List[A]] = Option.when(f.nonEmpty)(f.tail)
vs
given Headed[List] =
new Headed[List] :
def head[A](f: List[A]): Option[A] = f.headOption
def tail[A](f: List[A]): Option[List[A]] = Option.when(f.nonEmpty)(f.tail)
Useful? Yes, probably. Sufficiently useful to justify an additional syntax quirk? Not that sure. But I may be missing something…
You can also write
given Headed[List] = new:
def head[A](f: List[A]): Option[A] = f.headOption
def tail[A](f: List[A]): Option[List[A]] = Option.when(f.nonEmpty)(f.tail)