Suite à un précédent article sur DiamondBack Ruby, j'ai un peu réfléchis à la question de la sécurisation de l'appel de fonctions/procédures/méthodes, au niveau des types dans un langage dynamique tel que Ruby (/Python,/Perl/Etc...).
Finalement, quel est le problème ? Quelque soit le paradigme, il se situe au niveau de la capacité d'une fonction/procédure/méthode à traiter ses arguments en fonction de leur type, classe ou comportement. Problème simple donc. (Les aspects plus bas niveau sont ici peu intéressants : on est actuellement capables de les éluder).
Tout est dans la notion d'interface : dans les langages statiques, une interface n'est pas seulement un moyen d'appeler un "service" (fonction/méthode/...) mais c'est également un "contrat" qui impose des conditions sur les informations passées à l'appel (le plus souvent sur leur type). Par exemple, la fonction acheter_une_baguette ne peut se contenter de prendre 0.45 en argument : il faut que 0.45 soit en €, "de type €".
Ruby est un langage dynamique comme tant d'autres. Mais de par sa conception, sa "philosophie", il fait le choix de se concentrer sur ce que fait un objet plutôt que sur ce qu'il est : c'est le Duck Typing. On peut facilement critiquer cette approche du typage pour plusieurs raisons. Cet article sur Otaku en expose plusieurs. Pour ma part, je me contenterais de dire qu'elle réside dangereusement sur la langue (quel rapport entre Balançoire.balancer et Coupable.balancer ?).
Mais les langages statiques ne sont pas nécessairement satisfaisants sur ce point non plus. Pour faire un parallèle avec les mathématiques, on peut considérer que :
x |-> x + 2
avec R l'ensemble des réels, est une déclaration de type statique.
Pour autant, f reste valable de N dans N (naturels), de C dans C (complexes), etc... Le Duck Typing assure que f est valable du moment que x possède une méthode "+", ce qui évite de redéfinir f pour chaque espace où cette fonction est valable.
Mais est-ce suffisant ? Prenons :
f : x |-> 2 / x
Dans ce cas, on doit de plus assurer que x n'est pas nul. Si mathématiquement, cela s'exprime sur l'espace sur lequel est défini x, "informatiquement" on aura le plus souvent x entier ou flottant. Les contraintes sur un argument ne se limite donc pas à son type/classe, ni à son comportement (possède méthode "+"), mais aussi à d'autres paramètres, comme son état (ici sa valeur). De plus, s'il s'agit ici d'un exemple simple : il existe probablement d'autres exemples mettant en évidence l'influence du contexte d'appel.
Je vous propose ici un module (naïf) permettant d'exprimer assez simplement, lors de l'implémentation d'une méthode, les contraintes associées à ses arguments. Le but est d'assurer ces contraintes sans pour autant surcharger le code de structures conditionnelles avec gestion d'exceptions : on déclare simplement notre contrat.
(sources également disponibles **sans fuck1n' coupure** ICI)
module Safety
# Negator for error message
@@neg_converter = {:is_a? => "is not a",
:respond_to? => "does not respond to",
:include? => "does not include",
:each_element_is_a? => "contains element(s) that is(are) not"}
# Ensure that constraints are respected
def ensure_it cstr
cstr.each do |arg, cstrs|
value = cstrs[0]
cstrs[1...cstrs.size].each do |pair|
if not value.method(pair[0]).call(pair[1]) then
raise "Argument #{arg} (#{value.inspect}) \
#{@@neg_converter[pair[0].to_sym]} #{pair[1]}."
end
end
end
end
private
# A simple example of constraint method
def each_element_is_a? klass
assertion = true
self.each do |elt|
assertion = false if not elt.is_a? klass
end
assertion
end
end
La méthode ensure_it checke les contraintes passées. Une contrainte est facilement exprimable par une méthode telle que each_element_is_a?.
Exemples :
require 'Safety'
class Fixnum
include Safety
def mult_by_plus num1, num2
ensure_it({:num1 => [num1, [:is_a?, Fixnum]],
:num2 => [num2, [:respond_to?, :next]]})
self*num1+num2
end
end
a = 1
puts a.mult_by_plus(1,2)
puts a.mult_by_plus(1,1)
puts a.mult_by_plus(1,1.0) #=> Error raised
Et :
require 'Safety'
include Safety
def potamok tab
ensure_it({:tab => [tab, [:respond_to?, :each],
[:each_element_is_a?, Fixnum]]})
val = 0
tab.each do |elt|
val += elt**2
end
val
end
puts potamok [42,33,59]
puts potamok [1,2,3,6,59.3] #=> Error raised
Toute remarque est la bienvenue.