it-swarm.dev

Scala: ¿Cómo definir parámetros de función "genéricos"?

Estoy tratando de aprender Scala ahora, con un poco de experiencia en Haskell. Una cosa que me pareció extraña es que todos los parámetros de función en Scala debe deben ser anotados con un tipo - algo que Haskell no requiere. ¿Por qué es esto? Para intentar ponerlo como un ejemplo más concreto: una función de adición 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 tipos implícita). Pero, ¿qué ocurre si desea definir su propio tipo que define su propio operador +? ¿Cómo escribiría una función de adición que funcione para cualquier tipo que defina un operador +?

49
airportyh

Haskell usa el algoritmo de inferencia de tipo Hindley-Milner, mientras que Scala, para apoyar el lado orientado a objetos, tuvo que renunciar a usarlo por ahora.

Para poder escribir fácilmente una función de agregar para todos los tipos aplicables, deberá 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
68
Walter Chang

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 puedes escribir la función 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 darse cuenta esta clase genérica para un tipo específico, escribirías (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 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 es tan agradable como Haskell, que esencialmente hará todo esto por ti. Pero, ahí es donde radica la compensación.

18
airportyh

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 utiliza 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 de adición genérica 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.

Desafortunadamente, no existe tal 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 se mezcla con las clases RichInt, RichFloat, etc. Luego puede escribir una función de clasificación que puede tomar, por ejemplo, un List[T] donde [T <: Ordered[T]] para ordenar una lista de elementos que se mezclan en el rasgo ordenado. Debido a las conversiones de tipos implícitas como Float a RichFloat, incluso puede usar su función de clasificación en las listas de Int, o 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 Addable [T], crear AddableInt, AddableFloat, etc., clases que extiendan Int, Float, etc. y mezclar el rasgo Addable, y finalmente agregar funciones de conversión implícitas para convertir, por ejemplo, e Int en una AddableInt, para que el compilador pueda crear una instancia y usar su función de adición con él.

3
fxt

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

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

3
Daniel C. Sobral

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 propio parámetro de tipo. 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ándolo a otra instancia de T. Hice esto recientemente, definiendo un rasgo que decía "un grupo de aditivos consiste en una operación de adición más los medios para invertir un elemento "

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

Luego, más tarde, defino una clase Real que sabe cómo agregar instancias de sí mismo (el campo extiende 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 ahora, pero muestra cómo definir los argumentos genéricos y cómo realizarlos.

En última instancia, los tipos específicos no son necesarios, pero el compilador sí necesita conocer al menos los límites de los tipos.

1
mtnygard