¿Cómo puedo crear subtipos de Int en Scala?

¿Cómo puedo crear varios tipos que son esencialmente Ints, es decir, puede obtener el valor int, puede utilizar operadores de matemáticas como + BUT donde las instancias de diferentes tipos no pueden ser mezcladas.

Por ejemplo:

val density1 = new Density(100)
val density2 = new Density(200)
density1 + density2 should be(new Density(300))

val variability = new Variability(1)
variability.value should be(1)
density1 + variability // does not compile

Podría haber cientos de estos tipos y no quiero tener que implementar operadores como + en cada clase de hojas.

Idealmente, quisiera evitar todos los mecanismos implícitos de conversión (sólo preferencia personal). Los tipos adicionales no deben requerir alterar los tipos existentes.

Pregunta hecha hace 3 años, 4 meses, 29 días - Por techtrailblazer


4 Respuestas:

  • Aquí hay una solución en Scala 3 que no creo que use boxing/unboxing:

    object Wrappers:
      opaque type Wrapper = Int
      
      extension[T <: Wrapper](t: T)(using util.NotGiven[T =:= Wrapper]):
        def +(other: T): T = (t + other).asInstanceOf[T]
        //other methods
      
      opaque type Density <: Wrapper = Int
      def Density(i: Int): Density = i
      
      opaque type Variability <: Wrapper = Int
      def Variability(i: Int): Variability = i
    

    Pruébalo en Scastie

    Pruebas:

    val density1 = Density(1)
    val density2 = Density(2)
    val density3: Density = density1 + density2    //compiles
    
    val check1: Variability = density1 + density2  //doesn't compile
    
    val variability = Variability(1)
    val check2 = (variability: Wrapper) + density2 //doesn't compile
    val check3 = variability + density2            //doesn't compile
    
    println(density1) //1
    println(density2) //2
    println(density3) //3
    

    El asInstanceOf está descontrolado y no debe afectar el rendimiento. Este diseño debería también guardar el Ints de ser boxeado, pero no puedo garantizar eso, y también depende de cómo usted utiliza esto. Otra cosa agradable sobre esto es que cada nuevo tipo requiere sólo 2 líneas de código más. Y para facilitar la adición de nuevos métodos, probablemente también puede hacer un nuevo método propio para acortar asInstanceOf[T].

    Respondida el Dec 17, 2020 a las 14:14 - por codechampionx

    Votos positivos: 0 | Votos negativos: 0

  • trait TaggedInt[T <: TaggedInt[T]] {
      val value: Int
      protected def apply(value: Int): T
      def +(other: T) = apply(value + other.value)
      // etc.
    }
    
    case class Density(value: Int) extends TaggedInt[Density] {
      override protected def apply(value: Int) = Density(value)
    }
    

    Estaba desesperadamente tratando de rodearme teniendo que repetir override protected def apply(value: Int) = ... todo el tiempo

    Puedes hacerlo un parámetro constructor entonces. Ligeramente menos eficiente pero probablemente no importará en la práctica:

    abstract class TaggedInt[T <: TaggedInt[T]](constructor: Int => T) {
      val value: Int
      def +(other: T) = constructor(value + other.value)
      // etc.
    }
    
    case class Density(value: Int) extends TaggedInt[Density](Density)
    

    Yo originalmente tenía

    case class TaggedInt[Tag](value: Int) extends AnyVal {
      def +(other: TaggedInt[Tag]) = TaggedInt[Tag](value + other.value)
      // etc.
    }
    
    trait DensityTag
    type Density = TaggedInt[DensityTag]
    
    trait VariabilityTag
    type Variability = TaggedInt[VariabilityTag]
    

    pero tiene al menos 2 problemas para esta maleta:

    1. Density(100).toString es TaggedInt(100) en lugar de Density(100);
    2. Density(100) es igual a Variability(100).

    Respondida el Dec 17, 2020 a las 14:23 - por codesculptor

    Votos positivos: 0 | Votos negativos: 0

  • Puedes lograr esto utilizando el sistema de tipos algebraicos de Scala y el uso de traits junto con clases case. Aquí te dejo un ejemplo de cómo podrías implementar esta estructura para los tipos Density y Variability:

    scala
    // Trait base que define las operaciones comunes para las clases que representan un tipo Int
    trait IntType {
      def intValue: Int
    }
    
    // Clase base para representar los diferentes tipos Int
    case class IntValue(value: Int) extends IntType {
      def intValue: Int = value
    }
    
    // Clase para representar el tipo Density que es esencialmente un Int
    case class Density(value: Int) extends IntValue(value)
    
    // Clase para representar el tipo Variability que es esencialmente un Int
    case class Variability(value: Int) extends IntValue(value)
    
    // Función genérica para sumar dos IntType
    def sumIntTypesA <: IntType, B <: IntType: IntValue = {
      IntValue(a.intValue + b.intValue)
    }
    
    // Ejemplo de uso
    val density1 = Density(100)
    val density2 = Density(200)
    val sumDensity = sumIntTypes(density1, density2)
    println(sumDensity) // Imprime: IntValue(300)
    
    val variability = Variability(1)
    println(variability.intValue) // Imprime: 1
    
    // Esto no compilará ya que los tipos Density y Variability son incompatibles en la suma
    //val invalidSum = sumIntTypes(density1, variability)
    

    Con esta estructura, has definido un trait base IntType que define las operaciones comunes para los tipos que son esencialmente Int, y luego has creado clases Density y Variability que extienden IntValue (que implementa IntType). La función sumIntTypes puede sumar dos objetos de cualquier tipo que extienda IntType. La última línea en el ejemplo no compilará, lo que demuestra que los tipos Density y Variability no pueden ser mezclados en operaciones aritméticas.

    Este enfoque te permite definir múltiples tipos que son esencialmente Int y realizar operaciones comunes sobre ellos sin necesidad de definir los operadores en cada clase de hojas, y evitando el uso de mecanismos implícitos de conversión.

    Respondida el Dec 18, 2020 a las 14:18 - por Gpt

    Votos positivos: 0 | Votos negativos: 0

  • Puedes crear subtipos de Int en Scala utilizando clases selladas y clases de casos. Aquí tienes un ejemplo:

    scala
    sealed abstract class MyInt
    case class Density(value: Int) extends MyInt
    case class Variability(value: Int) extends MyInt
    

    Esta definición crea una clase sellada MyInt y dos clases de casos, Density y Variability, que extienden MyInt. Las clases de casos encapsulan el valor Int en cada subtipo.

    Ahora puedes definir operadores para la clase sellada MyInt:

    scala
    object MyInt {
      def +(a: MyInt, b: MyInt): MyInt = (a, b) match {
        case (Density(x), Density(y)) => Density(x + y)
        case (Variability(x), Variability(y)) => Variability(x + y)
        case _ => throw new IllegalArgumentException("Tipos incompatibles")
      }
    }
    

    Este operador + está definido para valores de MyInt, y solo permite sumar valores del mismo subtipo. Si se intenta sumar valores de diferentes subtipos, se lanza una excepción IllegalArgumentException.

    Ahora puedes utilizar los subtipos y el operador + de la siguiente manera:

    scala
    val density1 = new Density(100)
    val density2 = new Density(200)
    val result = density1 + density2 // Density(300)
    
    val variability = new Variability(1)
    // variability.value debería ser 1
    // density1 + variability // No compila
    

    En este ejemplo, density1 y density2 son del mismo subtipo Density, por lo que se pueden sumar utilizando el operador + definido para MyInt. La suma de density1 y variability no compila porque son de diferentes subtipos.

    Este enfoque te permite crear subtipos de Int con comportamientos específicos sin modificar las clases existentes.

    Respondida el Dec 18, 2020 a las 14:23 - por Gemini

    Votos positivos: 0 | Votos negativos: 0