Inférence de type

Def inférence: Opération par laquelle on passe d'une assertion considérée comme vraie a une autre assertion au moyen d'un système de règles qui rend cette deuxième assertion également vraie.

La philosophie de Crystal est de nécessiter aussi peu d'annotations que possible. Néanmoins, certaines annotations sont nécessaires.

Considérons la définition de classe suivante:

class Person
  def initialize(@name)
    @age = 0
  end
end

On se rend rapidement compte que @age est un entier, mais nous ne connaissons pas le type de @name. Le compilateur peut inférer ce type à partir des utilisations de la classe Person. Néanmoins, ceci présente quelques problèmes:

  • Le type n'est pas évident pour quelqu'un à la lecture du code: il lui faudrait vérifier les utilisations de la classe Person pour le découvrir,
  • Certaines optimisations du compilateur, comme analyser qu'une fois une méthode, et la compilation incrémentale, sont quasi-impossible.

Lorsque que l'ensemble d'un code source s'accroit, ces problèmes prennent encore plus d'ampleur: comprendre un projet devient plus difficile, et les temps de compilation deviennent insupportablement longs.

C'est pourquoi Crystal doit savoir, de manière évidente (aussi évidente que pour un humain), les types des variables d'instance, de classe et globales.

Il y a plusieurs moyens de faire savoir cela à Crystal.

Utiliser une annotation de type explicite

Le moyen le plus simple, mais sûrement le plus fastidieux, est d'utiliser une annotation de type explicite.

class Person
  @name : String
  @age : Int32

  def initialize(@name)
    @age = 0
  end
end

Ne pas utiliser une annotation de type explicite

Si vous ometez l'annotation de type explicite le compilateur va essayer d'inférer le type des variables d'instance, de classe et globales en utilisant plusieurs règles syntaxiques.

Pour une variable d'instance/classe/globale donnée, quand une règle peut s'appliquer et un type est inféré, le type est ajouté à un ensemble. Quand plus aucune règle ne peut être appliquée, le type inféré sera l'union de ces types. De plus, si le compilateur déduit qu'une variable d'instance n'est pas toujours initialisée, il inclura aussi le type Nil.

Les règles sont nombreuses, mais généralement les trois premières sont les plus utilisées. Il n'y pas besoin de toutes les connaîtres. Si le compilateur renvoie une erreur signifiant que le type d'une instance ne peut être déduit vous pouvez toujours ajouter une annotation de type explicite.

Les règles suivantes ne mentionnent que les variables d'instance, mais elles s'appliquent aussi bien aux variables de classe et aux varibales globales. Ces règles sont les suivantes.

1. Affecter une valeur litérale

Quand un litéral est affecté à une variable d'instance, le type du litéral est ajouté à l'ensemble. Tous les litéraux sont associés à un type.

Dans l'exemple qui suit, @name est déduit comme une String et @age comme un Int32.

class Person
  def initialize
    @name = "John Doe"
    @age = 0
  end
end

Cette règle, et toutes les suivantes, seront aussi appliquées dans d'autres méthodes queinitialize. Par exemple:

class SomeObject
  def lucky_number
    @lucky_number = 42
  end
end

Dans l'exemple suivant, @lucky_number sera déduit comme Int32 | Nil: Int32 car 42 lui a été affecté, et Nil car il n'a pas été affecté dans toutes les méthodes initialize de la classe.

2. Affecter le résultat de l'invocation de la méthode de classe new

Quand une expression comme Type.new(...) est affectée à une variable d'instance, le type Type est ajouté à l'ensemble.

Dans l'exemple suivant, @address est déduit comme étant Address.

class Person
  def initialize
    @address = Address.new("somewhere")
  end
end

Cela s'applique également aux types génériques. Ici @values est déduit comme étant Array(Int32).

class Something
  def initialize
    @values = Array(Int32).new
  end
end

Note: une méthode new peut être redéfinie par un type. Dans ce cas le type inféré sera celui renvoyé par new, s'il peut être déduit en utilisant les règles suivantes.

3. Affecter une variable qui est un argument de méthode avec une restriction de type

Dans l'exemple suivant @name est déduit comme étant une String car l'argument de méthode name a une restriction de type du type String, et cet argument est affecté à @name.

class Person
  def initialize(name : String)
    @name = name
  end
end

Remarquez que le nom de l'argument de la méthode n'est pas important, ce qui suit est tout autant valide:

class Person
  def initialize(obj : String)
    @name = obj
  end
end

Utiliser la forme courte d'affectation d'une variable d'instance depuis un argument de méthode a le même effet:

class Person
  def initialize(@name : String)
  end
end

Remarquez aussi que le compilateur ne vérifie pas si une valeur différente a été ré-affectée à l'argument de méthode:

class Person
  def initialize(name : String)
    name = 1
    @name = name
  end
end

Dans le cas précédent, le compilateur déduit @name comme String, et renverra plus tard une erreur à la compilation, quand il typera complétement la méthode, déclarant que Int32 ne peut pas être affecté à une variable de type String. Utilisez une annotation de type explicite si @name n'est pas supposé être de type String.

4. Affecter le résultat d'une méthode de classe qui a pour retour une annotation de type

Dans l'exemple suivant, le type de @address est inféré comme Address, car la méthode de classe Address.unknown a pour retour une annotation de type Address.

class Person
  def initialize
    @address = Address.unknown
  end
end

class Address
  def self.unknown : Address
    new("unknown")
  end

  def initialize(@name : String)
  end
end

En fait, le code précédent n'a pas besoin de renvoyer une annotation de type self.unknown. La raison est que le compilateur va aussi regarder le corps d'une méthode de classe et s'il peut appliquer une des règles précédentes (c'est une méthode new, ou c'est un litéral, etc.) il déduira le type de cette expression. Alors, ce qui précède peut simplement être écrit comme suit:

class Person
  def initialize
    @address = Address.unknown
  end
end

class Address
  # No need for a return type annotation here
  def self.unknown
    new("unknown")
  end

  def initialize(@name : String)
  end
end

Cette règle supplémentaire est très pratique car il est très commun d'avoir de méthodes de classe "à la constructeur" en plus de new.

5. Affecter une variable qui est un argument de méthode avec une valeur par déafut

Dans l'exemple qui suit, parce-que la valeur par défaut de name est un litéral chaîne de caractères, et qu'elle est plus tard affectée à @name, String sera ajouté à l'ensemble des types inférés.

class Person
  def initialize(name = "John Doe")
    @name = name
  end
end

Cela fonctionne bien sûr aussi avec la syntaxe courte:

class Person
  def initialize(@name = "John Doe")
  end
end

La valeur par défaut peut aussi être une méthode Type.new(...) ou une méthode de classe avec un retour d'annotation de type.

6. Affecter le résultat de l'invocation de la fonction lib

Parce-qu'une fonction lib doit avoir des types explicites, le compilateur peut utiliser le type retour lors de l'affectation à une variable d'instance.

Dans l'exemple suivant @age est déduit comme étant Int32.

class Person
  def initialize
    @age = LibPerson.compute_default_age
  end
end

lib LibPerson
  fun compute_default_age : Int32
end

7. Utiliser une expression out de lib

Parce-qu'une fonction lib doit avoir des types explicites, le compilateur peut utiliser le type de l'argument out, qui devrait être un type pointeur, et utiliser le type déréférencé deviné.

Dans l'exemple suivant @age est déduit comme étant Int32.

class Person
  def initialize
    LibPerson.compute_default_age(out @age)
  end
end

lib LibPerson
  fun compute_default_age(age_ptr : Int32*)
end

Autres règles

Le compilateur essaiera d'être le plus intelligent possible afin de nécessiter le minimum d'annotations de type. Par exemple, lors de l'affectation d'une expression if, le type sera déduit des branches then et else:

class Person
  def initialize
    @age = some_condition ? 1 : 2
  end
end

Parce-que le if précédent (en fait, techniquement, un opérateur ternaire, mais c'est similaire à un if) a des litéraux d'entier, @age est déduit avec succès comme étant Int32 sans nécessiter une annotation de type redondante.

Un autre cas est || et ||=:

class SomeObject
  def lucky_number
    @lucky_number ||= 42
  end
end

Dans l'exemple précédent @lucky_number sera déduit comme étant Int32 | Nil. C'est très utile pour des variables sommairement initialisées.

Les constantes seront aussi suivies, étant donné que cela reste assez simple à faire pour le compilateur (et pour un humain).

class SomeObject
  DEFAULT_LUCKY_NUMBER = 42

  def initialize(@lucky_number = DEFAULT_LUCKY_NUMBER)
  end
end

Ici la règle 5 (valeur par défaut d'argument) est appliquée, et parce-que la constante est résolue en un litéral d'entier, @lucky_number est déduite comme étant Int32.

results matching ""

    No results matching ""