J'essaie d'apprendre Scala maintenant, avec un peu d'expérience dans Haskell. Une chose qui m'a paru étrange est que tous les paramètres de fonction dans Scala = doit être annoté avec un type - quelque chose que Haskell ne nécessite pas. Pourquoi est-ce? Pour essayer de le mettre comme un exemple plus concret: une fonction d'ajout est écrite comme ceci:
def add(x:Double, y:Double) = x + y
Mais cela ne fonctionne que pour les doubles (enfin, les ints fonctionnent aussi à cause de la conversion de type implicite). Mais que se passe-t-il si vous souhaitez définir votre propre type qui définit son propre opérateur +. Comment écririez-vous une fonction d'ajout qui fonctionne pour tout type qui définit un opérateur +?
Haskell utilise l'algorithme d'inférence de type Hindley-Milner alors que Scala, pour prendre en charge le côté orienté objet, a dû renoncer à l'utiliser pour l'instant.
Afin d'écrire facilement une fonction d'ajout pour tous les types applicables, vous devrez utiliser 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
Afin de solidifier le concept d'utilisation de implicite pour moi-même, j'ai écrit un exemple qui ne nécessite pas scala 2.8, mais utilise le même concept. J'ai pensé que cela pourrait être utile pour certains. Tout d'abord, vous définissez une classe générique-abstraite Addable:
scala> abstract class Addable[T]{
| def +(x: T, y: T): T
| }
defined class Addable
Vous pouvez maintenant écrire la fonction add comme ceci:
scala> def add[T](x: T, y: T)(implicit addy: Addable[T]): T =
| addy.+(x, y)
add: [T](T,T)(implicit Addable[T])T
Ceci est utilisé comme une classe de type dans Haskell. Ensuite, pour réaliser cette classe générique pour un type spécifique, vous écririez (exemples ici pour Int, Double et 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
À ce stade, vous pouvez appeler la fonction ajouter avec les trois types:
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
Certainement pas aussi sympa que Haskell qui fera essentiellement tout cela pour vous. Mais c'est là que réside le compromis.
Je pense que la raison Scala nécessite l'annotation de type sur les paramètres d'une fonction nouvellement définie vient du fait que Scala utilise une analyse d'inférence de type plus locale que celle utilisé à Haskell.
Si toutes vos classes se mélangent dans un trait, dites Addable[T]
, qui a déclaré le +
, vous pouvez écrire votre fonction d'ajout générique comme suit:
def add[T <: Addable[T]](x : T, y : T) = x + y
Cela limite la fonction d'ajout aux types T qui implémentent la caractéristique Addable.
Malheureusement, il n'y a pas un tel trait dans les bibliothèques Scala actuelles. Mais vous pouvez voir comment cela serait fait en examinant un cas similaire, le Ordered[T]
trait. Ce trait déclare des opérateurs de comparaison et est mélangé par les classes RichInt
, RichFloat
, etc. Ensuite, vous pouvez écrire une fonction de tri qui peut prendre, par exemple, un List[T]
où [T <: Ordered[T]]
pour trier une liste d'éléments qui se mélangent dans le trait ordonné. En raison des conversions de types implicites comme Float
en RichFloat
, vous pouvez même utiliser votre fonction de tri sur les listes de Int
, ou Float
ou Double
.
Comme je l'ai dit, malheureusement, il n'y a pas de trait correspondant pour le +
opérateur. Il vous faudrait donc tout écrire vous-même. Vous feriez le trait Addable [T], créer AddableInt
, AddableFloat
, etc., des classes qui étendent Int, Float, etc. et mélanger dans le trait Addable, et enfin ajouter des fonctions de conversion implicites pour transformer, par exemple, et Int en AddableInt
, afin que le compilateur puisse instancier et utiliser votre fonction add avec.
Haskell utilise l'inférence de type Hindley-Milner . Ce type d'inférence de type est puissant, mais limite le système de type de la langue. Soi-disant, par exemple, le sous-classement ne fonctionne pas bien avec H-M.
En tout cas, Scala est trop puissant pour H-M, donc un type plus limité d'inférence de type doit être utilisé.
La fonction elle-même sera assez simple:
def add(x: T, y: T): T = ...
Mieux encore, vous pouvez simplement surcharger la méthode +:
def +(x: T, y: T): T = ...
Il manque cependant une pièce, qui est le paramètre de type lui-même. Comme écrit, la méthode manque sa classe. Le cas le plus probable est que vous appelez la méthode + sur une instance de T, en lui passant une autre instance de T. Je l'ai fait récemment, en définissant un trait qui disait: "un groupe additif consiste en une opération d'ajout plus les moyens de inverser un élément "
trait GroupAdditive[G] extends Structure[G] {
def +(that: G): G
def unary_- : G
}
Ensuite, plus tard, je définis une classe Real qui sait comment ajouter des instances d'elle-même (Field étend GroupAdditive):
class Real private (s: LargeInteger, err: LargeInteger, exp: Int) extends Number[Real] with Field[Real] with Ordered[Real] {
...
def +(that: Real): Real = { ... }
...
}
C'est peut-être plus que ce que vous vouliez vraiment savoir en ce moment, mais cela montre à la fois comment définir des arguments génériques et comment les réaliser.
En fin de compte, les types spécifiques ne sont pas requis, mais le compilateur a besoin de connaître au moins les limites des types.