Scala: ¿cómo definir parámetros de función “generics”?

Estoy tratando de aprender Scala ahora, con un poco de experiencia en Haskell. Una cosa que me llamó la atención es que todos los parámetros de función en Scala deben ser anotados con un tipo, algo que Haskell no requiere. ¿Por qué es esto? Para tratar de ponerlo como un ejemplo más concreto: una función de agregar se escribe así:

def add(x:Double, y:Double) = x + y 

Pero esto solo funciona para los dobles (bueno, los ints también funcionan debido a la conversión de tipo implícita). Pero, ¿qué ocurre si quiere definir su propio tipo que define su propio operador + ? ¿Cómo escribirías una función de agregar que funciona para cualquier tipo que defina un operador + ?

Haskell usa el algoritmo de inferencia de tipo Hindley-Milner, mientras que Scala, para soportar el lado Orientado a Objetos de las cosas, tuvo que renunciar a usarlo por el momento.

Para escribir fácilmente una función de agregar para todos los tipos aplicables, necesitará usar Scala 2.8.0:

 Welcome to Scala version 2.8.0.r18189-b20090702020221 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_15). Type in expressions to have them evaluated. Type :help for more information. scala> import Numeric._ import Numeric._ scala> def add[A](x: A, y: A)(implicit numeric: Numeric[A]): A = | numeric.plus(x, y) add: [A](x: A,y: A)(implicit numeric: Numeric[A])A scala> add(1, 2) res0: Int = 3 scala> add(1.1, 2.2) res1: Double = 3.3000000000000003 

Para solidificar el concepto de usar implícito para mí, escribí un ejemplo que no requiere Scala 2.8, pero usa el mismo concepto. Pensé que podría ser útil para algunos. Primero, define una clase genérica-abstracta Addable :

 scala> abstract class Addable[T]{ | def +(x: T, y: T): T | } defined class Addable 

Ahora puede escribir la función de agregar de esta manera:

 scala> def add[T](x: T, y: T)(implicit addy: Addable[T]): T = | addy.+(x, y) add: [T](T,T)(implicit Addable[T])T 

Esto se usa como una clase de tipo en Haskell. Luego, para realizar esta clase genérica para un tipo específico, escribiría (ejemplos aquí para Int, Double y String):

 scala> implicit object IntAddable extends Addable[Int]{ | def +(x: Int, y: Int): Int = x + y | } defined module IntAddable scala> implicit object DoubleAddable extends Addable[Double]{ | def +(x: Double, y: Double): Double = x + y | } defined module DoubleAddable scala> implicit object StringAddable extends Addable[String]{ | def +(x: String, y: String): String = x concat y | } defined module StringAddable 

En este punto, puede llamar a la función de agregar con los tres tipos:

 scala> add(1,2) res0: Int = 3 scala> add(1.0, 2.0) res1: Double = 3.0 scala> add("abc", "def") res2: java.lang.String = abcdef 

Ciertamente no tan agradable como Haskell, que básicamente hará todo esto por ti. Pero, ahí es donde radica la disyuntiva.

Haskell usa la inferencia tipo Hindley-Milner . Este tipo de inferencia de tipo es poderoso, pero limita el tipo de sistema del lenguaje. Supuestamente, por ejemplo, la subclasificación no funciona bien con HM.

En cualquier caso, el sistema de tipo Scala es demasiado poderoso para HM, por lo que se debe utilizar un tipo de inferencia de tipo más limitado.

Creo que la razón por la que Scala requiere la anotación de tipo en los parámetros de una función recién definida proviene del hecho de que Scala usa un análisis de inferencia de tipo más local que el utilizado en Haskell.

Si todas sus clases se mezclaron en un rasgo, digamos Addable[T] , que declaró el operador + , podría escribir su función genérica de agregar como:

 def add[T <: Addable[T]](x : T, y : T) = x + y 

Esto restringe la función de agregar a los tipos T que implementan el rasgo Addable.

Lamentablemente, no existe ese rasgo en las bibliotecas actuales de Scala. Pero puede ver cómo se haría al observar un caso similar, el rasgo Ordered[T] . Este rasgo declara operadores de comparación y está mezclado por las RichInt , RichFloat , etc. Luego puede escribir una función de clasificación que puede tomar, por ejemplo, una List[T] donde [T <: Ordered[T]] para ordenar una lista de elementos que se mezclan en el rasgo ordenado. Debido a las conversiones de tipo implícitas como Float to RichFloat , incluso puede usar su función de clasificación en listas de Int , Float o Double .

Como dije, desafortunadamente, no hay un rasgo correspondiente para el operador + . Entonces, tendrías que escribir todo tú mismo. Debería hacer el rasgo Aditable [T], crear AddableInt , AddableFloat , etc., clases que amplíen Int, Float, etc. y mezclar en el rasgo Addable, y finalmente agregar funciones de conversión implícita para activar, por ejemplo, y Int en un AddableInt , para que el comstackdor pueda instanciar y usar su función de agregar con él.

La función en sí será bastante sencilla:

 def add(x: T, y: T): T = ... 

Mejor aún, puedes sobrecargar el método +:

 def +(x: T, y: T): T = ... 

Sin embargo, falta una pieza, que es el parámetro de tipo en sí. Como está escrito, al método le falta su clase. El caso más probable es que está llamando al método + en una instancia de T, pasándole otra instancia de T. Lo hice recientemente, definiendo un rasgo que decía: “un grupo aditivo consiste en una operación de sum más los medios para invierte un elemento ”

 trait GroupAdditive[G] extends Structure[G] { def +(that: G): G def unary_- : G } 

Luego, más adelante, defino una clase Real que sabe cómo agregar instancias de sí mismo (Field extends GroupAdditive):

 class Real private (s: LargeInteger, err: LargeInteger, exp: Int) extends Number[Real] with Field[Real] with Ordered[Real] { ... def +(that: Real): Real = { ... } ... } 

Eso puede ser más de lo que realmente quería saber en este momento, pero muestra cómo definir los argumentos generics y cómo realizarlos.

En última instancia, los tipos específicos no son necesarios, pero el comstackdor necesita saber al menos los límites de tipo.