You have to explain a few things before arriving at a possible solution, although I have already told you that it is not as simple as using Generics , although it has something to do with types .
Let's see first why it fails, then a solution following the "Object Oriented Programming" and, finally, a solution with "Types Classes" using implicit .
Why it fails
Before a scala code, the question that needs to be asked is whether the compiler has all the necessary information to infer the type of all the objects. Sometimes, when an ambiguity of types occurs, the compiler uses the rule of using the most specific type (when there is contravariance ); in others it uses the most general type (when there is covariance ).
In your case, the function Motor.trasladar
has a signature Figura => Figura
since it has to serve equal to Circulos
that for Rectangulos
, and Figura
is the general type that covers both.
When you apply Motor.trasladar
to circuloBase
you then get a Figura
, not a Circulo
, and hence fail. In order to obtain your radio
attribute you will have to convert it first to Circulo
:
val trasladar = Motor.trasladar(2,-1)
val circuloBase = Circulo(1,1,43)
val circulo = trasladar(circuloBase).asInstanceOf[Circulo]
It is not a good solution since the "conversion" of types is carried out in execution, with the consequent risk of errors. We have to look for a better solution.
Object Orientation
The most common way to solve the problem would be to refactor the classes to contain the method:
trait Figura {
val x: Int
val y: Int
def trasladar(x: Int, y: Int): Figura
}
case class Circulo(x: Int, y: Int, radio: Double) extends Figura {
def trasladar(x: Int, y: Int) = this.copy(x = this.x + x, y = this.y + y)
}
case class Rectangulo(x: Int, y: Int, base: Int, altura: Int) extends Figura {
def trasladar(x: Int, y: Int) = this.copy(x = this.x + x, y = this.y + y)
}
To use it:
val circuloBase = Circulo(1, 1, 43)
val circulo = circuloBase.trasladar(2, -1)
I guess the drawback is that you want to create more operations on these objects, but without modifying the model. Let's say you have sources you do not want to touch. Then this solution is not feasible. Let's see something else ...
Generic types
Let's see if we can use generic types:
def trasladar[T <: Figura](x: Int, y: Int) = (figura: T) =>
figura match {
case Circulo(xPos, yPos, radio) => Circulo(xPos+x, yPos+y, radio)
case Rectangulo(xPos, yPos, base, altura) => Rectangulo(xPos+x, yPos+y, base, altura)
}
In this parameterization of types we indicate that the type T
must be a subtype of Figura
, indicated by T <: Figura
. The transformation trasladar
converts an object of type T
into another type T
.
We can think that it will go well, because when applied to an object Circulo
, T
will be equal to Circulo
and then return a Circulo
, and the same when applied to Rectangulo
.
The problem is that the compiler does not know to what the function will be applied, so it can not relate T
with some of the subtypes of Figura
.
It is possible that in new implementations of scala (eg: with dotty ) have compound types that could solve this problem. But today it is clear that the best way is using typeclasses with implicit, and increasingly will be present in scala bookstores.
Type classes (typeclass)
The typeclass has no relation to the "Object Classes" that you see in object-oriented programming. They come from the Theory of Categories that they have known so well implement in haskell and that already extends to other functional languages such as scala.
Let's say we want to define a series of operations and then implement them on different types of data. The set of these operations would be our "type class" ( typeclass ):
trait Transform[T <: Figura] {
def trasladar(x: Int, y: Int): T => T
}
We only have one operation, but we could define all that we need, or even add more typeclasses with other sets of operations.
We define the transformations for each of the types we have (what is called "create instances of the typeclass" ):
object TransformCirculos extends Transform[Circulo] {
def trasladar(x: Int, y: Int) =
circulo => circulo.copy(x = x + circulo.x, y = y + circulo.y)
}
object TransformRectangulos extends Transform[Rectangulo] {
def trasladar(x: Int, y: Int) =
rect => rect.copy(x = x + rect.x, y = y + rect.y)
}
With the function to move in this way:
def trasladar[T <: Figura](x: Int, y: Int)(figura: T)(tr: Transform[T]) =
tr.trasladar(x, y)(figura)
The inconvenience is that every time we do a trasladar
we will have to explicitly pass the corresponding Transform
object:
val trasladar = Motor.trasladar(2,-1)(_: Circulo)(Motor.TransformCirculos)
We can avoid this if we make the Transform objects implicit, as well as the argument of the function:
implicit object TransformCirculos extends Transform[Circulo] {
def trasladar(x: Int, y: Int) =
circulo => circulo.copy(x = x + circulo.x, y = y + circulo.y)
}
implicit object TransformRectangulos extends Transform[Rectangulo] {
def trasladar(x: Int, y: Int) =
rect => rect.copy(x = x + rect.x, y = y + rect.y)
}
def trasladar[T <: Figura](x: Int, y: Int)(figura: T)(implicit tr: Transform[T]) =
tr.trasladar(x, y)(figura)
In this way, we would not have to specify the transformation object as long as the trasladar
function finds it within its environment:
val trasladar = Motor.trasladar(2,-1)(_: Circulo)
Still, we could not do this:
val trasladar = Motor.trasladar(2,-1)
since the compiler does not have the necessary information about the types of data to which it will be applied. On the other hand, the alternative of specifying the type of data is possible:
val trasladar = Motor.trasladar[Circulo](2,-1) _
Observe the underline _
at the end indicating to the compiler that we want to operate with the function and do not give an error by taking it as a method.
End
Finally, I put the complete code to make it clearer:
trait Figura {
val x: Int
val y: Int
}
case class Circulo(x: Int, y: Int, radio: Double) extends Figura
case class Rectangulo(x: Int, y: Int, base: Int, altura: Int) extends Figura
object FiguraMain extends App {
object Motor {
trait Transform[T <: Figura] {
def trasladar(x: Int, y: Int): T => T
}
implicit object TransformCirculos extends Transform[Circulo] {
def trasladar(x: Int, y: Int) =
circulo => circulo.copy(x = x + circulo.x, y = y + circulo.y)
}
implicit object TransformRectangulos extends Transform[Rectangulo] {
def trasladar(x: Int, y: Int) =
rect => rect.copy(x = x + rect.x, y = y + rect.y)
}
def trasladar[T <: Figura](x: Int, y: Int)(figura: T)(implicit tr: Transform[T]) =
tr.trasladar(x, y)(figura)
}
def main = {
val trasladar = Motor.trasladar[Circulo](2, -1) _
val circuloBase = Circulo(1, 1, 43)
val circulo = trasladar(circuloBase)
assert(circulo.x == 3)
assert(circulo.y == 0)
assert(circuloBase != circulo)
assert(circulo.radio == 43)
}
}