5
votes

I am looking to create a type family that would represent data sizes (Byte, KB...). for that, the idea is to build a base type to have the real sizes based on:

  type SizeUnit = Int
  type B = SizeUnit
  type KB = SizeUnit
  type MB = SizeUnit
  type GB = SizeUnit
  type TB = SizeUnit
  type PB = SizeUnit
  type EB = SizeUnit
  type ZB = SizeUnit
  type YB = SizeUnit

have an ordered list of them:

val sizes = List(B, KB, MB, GB, TB, PB, EX, ZB, TB)

and have a convertion method that takes a target type, finds the index difference between them and multiplies by 1024 in the power of difference. so:

def convertTo(targetType: SizeUnit): SizeUnit ={
  def power(itr: Int): Int = {
    if (itr == 0) 1
    else 1024*power(itr-1)
  }

  val distance = sizes.indexOf(targetType) - sizes.indexOf(this)
  distance match {
    //same type - same value
    case 0 => targetType
    //positive distance means larget unit - smaller number
    case x>0 => targetType / power(distance)
    //negative distance means smaller unit - larger number and take care of negitivity 
    case x<0 => targetType * power(distance) * (-1)
  }  
}

i have a few problems before i even check the validity of the method (as i am new to Scala):

  • is there a way to create a List (or any other Seq) that holds types rather than values? or rather - types as values?
  • if i understand correctly, types are not kept beyond compilation. does that mean that in runtime, if i pass a GB value to an existing KB, it cannot decipher the types?

thank you, Ehud

2

2 Answers

9
votes

All those types are simply type aliases, not independent types.

scala> type SizeUnit = Int
defined type alias SizeUnit

scala> type B = SizeUnit
defined type alias B

scala> type KB = SizeUnit
defined type alias KB

scala> (3 : KB) == (3 : B)
res0: Boolean = true

Type aliases are simply different names for the same type. So even if you could write it, your list would be equivalent to having written:

val sizes = List(Int, Int, Int, Int, Int, Int, Int, Int, Int)

And similarly, you could never use these types to write a function that is required to accept a quantity in MB, since all of these types are the same thing.

To separate out B, KB, MB, etc as different "kinds" of integer, you would need them to be subtypes of Int, not type aliases for Int. But Int is a final type, so you can't subtype it anyway.

A much better approach is to just let Int represent a raw number, and instead implement a type that represents an Int together with a unit. There are several approaches you can take for this, but I'd do it something like this:

abstract class SizeUnit

case object B extends SizeUnit
case object KB extends SizeUnit
case object MB extends SizeUnit


case class Storage(num : Int, unit : SizeUnit)

Now 3 megabytes is Storage(3, MB) and 17 bytes is Storage(17, B). You have nice statically enforced separation between arbitrary integers and Storage quantities, and you always have the unit as a data object (no need to be able to statically infer it) whenever you have a Storage quantity. You can put the objects B, KB, MB, etc in a list, and do whatever manipulation with them you want.

Alternatively you could make the unit objects themselves contain some information about their order or ratios between them, rather than storing that information in an external list.

You can even do wacky things with implicit conversions using this scheme. Something like this springs to mind:

object SizeableInt {
    // since I want to give the conversion methods the same name as the
    // units, I need different names to refer to the units in the
    // implementation of those methods. If B, KB, etc were defined in
    // a different qualified namespace, this wouldn't be necessary.
    private val _B = B
    private val _KB = KB
    private val _MB = MB

    case class SizeableInt(x : Int) {
        def B : Storage = Storage(x, _B)
        def KB : Storage = Storage(x, _KB)
        def MB : Storage = Storage(x, _MB)
    }

    implicit def makeSizeableInt(x : Int) : SizeableInt = SizeableInt(x)
}

With that, once you've imported the implicit, you can simply write things like 4 MB or 123456789 B instead of Storage(4, MB) or Storage(123456789, B).

0
votes

Types are not values in Scala.

You should probably look at singleton objects for your use case.