Can one represent a type as a value?

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:

  • A type class Gen for generating instances.
  • A natural number representation Nat for the column count.
  • The GenSpec type.
  • A GenTarget match type for the corresponding result type.
  • A 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 withI 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…

1 Like

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)
2 Likes