scalacustom-typetypelist

using custom scala types in List


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):

thank you, Ehud


Solution

  • 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).