oop - unitarios - jerarquía de clases adecuada para vectores 2D y 3D
vectores en 3d fisica (4)
No estoy seguro de la sintaxis correcta de Scala, pero puede implementar el CRTP , es decir, definir el tipo real a través de un parámetro genérico.
trait Vec[V <: Vec[V]] {
def +(v:V):V
...
}
class Vec2D extends Vec[Vec2D] { }
class Vec3D extends Vec[Vec3D] { }
class Polygon[V <: Vec[V]] {
...
}
Quiero tener una clase / rasgo abstracto de vector general que especifique ciertos métodos, por ejemplo:
trait Vec
{
def +(v:Vec):Vec
def *(d:Double):Vec
def dot(v:Vec):Double
def norm:Double
}
Quiero que Vec2D
y Vec3D
extiendan Vec
:
class Vec2D extends Vec { /* implementation */ }
class Vec3D extends Vec { /* implementation */ }
Pero, ¿cómo puedo, por ejemplo, hacer que Vec2D
solo se pueda agregar a otro Vec2D
y no a Vec3D
?
En este momento solo estoy implementando Vec2D
y Vec3D
sin un antecesor Vec
común, pero esto se está Vec3D
tedioso con código duplicado. Tengo que implementar todas mis clases de geometría que dependen de estas clases (por ejemplo, Triangle
, Polygon
, Mesh
, ...) dos veces, una para Vec2D
y otra para Vec3D
.
Veo las implementaciones de Java: javax.vecmath.Vector2d
y javax.vecmath.Vector3d
no tienen un antecesor común. ¿Cuál es el motivo de esto? ¿Hay alguna manera de superarlo en Scala?
Puedes usar self-types:
trait Vec[T] { self:T =>
def +(v:T):T
def *(d:Double):T
def dot(v:T):Double
def norm:Double
}
class Vec2D extends Vec[Vec2D] { /* implementation */ }
class Vec3D extends Vec[Vec3D] { /* implementation */ }
Pero si ambas implementaciones son muy similares, también podría intentar abstraerse sobre la Dimensión.
sealed trait Dimension
case object Dim2D extends Dimension
case object Dim3D extends Dimension
sealed abstract class Vec[D <: Dimension](val data: Array[Double]) {
def +(v:Vec[D]):Vec[D] = ...
def *(d:Double):Vec[D] = ...
def dot(v:Vec[D]):Double = ...
def norm:Double = math.sqrt(data.map(x => x*x).sum)
}
class Vec2D(x:Double, y:Double) extends Vec[Dim2D.type](Array(x,y))
class Vec3D(x:Double, y:Double, z:Double) extends Vec[Dim3D.type](Array(x,y,z))
Por supuesto, depende de cómo quiera representar los datos y si quiere tener instancias mutables o inmutables. Y para aplicaciones del "mundo real" debería considerar http://code.google.com/p/simplex3d/
Hay un gran problema con tener un ancestro común con el patrón CRTP en JVM. Cuando ejecuta el mismo código abstracto con diferentes implementaciones, JVM desoptimizará el código (sin enlining + llamadas virtuales). No lo notarás si solo pruebas con Vec3D, pero si pruebas tanto con Vec2D como con Vec3D verás una gran caída en el rendimiento. Además, el Análisis de escape no se puede aplicar al código de optimización (sin reemplazo escalar, sin eliminación de nuevas instancias). La falta de estas optimizaciones reducirá la velocidad de su programa en un factor de 3 (una estimación muy completa que depende de su código).
Pruebe algunos puntos de referencia que se ejecutan durante aproximadamente 10 segundos. En la misma prueba de ejecución con Vec2D, luego Vec3D, luego Vec2D, luego Vec3D nuevamente. Verás este patrón:
- Vec2D ~ 10 segundos
- Vec3D ~ 30 segundos
- Vec2D ~ 30 segundos
- Vec3D ~ 30 segundos
Tal como se solicitó , la forma más útil de diseñar el rasgo base implica tanto la anotación CRTP como la anotación de tipo propio .
trait Vec[T <: Vec[T]] { this: T =>
def -(v: T): T
def *(d: Double): T
def dot(v: T): Double
def norm: Double = math.sqrt(this dot this)
def dist(v: T) = (this - v).norm
}
Sin el tipo propio, no es posible llamar this.dot(this)
como dot
espera una T
; por lo tanto, debemos aplicarlo con la anotación.
Por otro lado, sin CRTP, no llamaremos a norm
(this - v)
as -
devuelve una T
y, por lo tanto, debemos asegurarnos de que nuestro tipo T
tiene este método, por ejemplo, declarar que T
es un Vec[T]