web-dev-qa-db-fra.com

Pourquoi Math.Sqrt () est une fonction statique?

Dans une discussion sur les méthodes statiques et d'instance, je pense toujours que Sqrt() devrait être une méthode d'instance de types numériques au lieu d'une méthode statique. Pourquoi donc? Cela fonctionne évidemment sur une valeur.

 // looks wrong to me
 var y = Math.Sqrt(x);
 // looks better to me
 var y = x.Sqrt();

Les types de valeur peuvent évidemment avoir des méthodes d'instance, comme dans de nombreux langages, il existe une méthode d'instance ToString().

Pour répondre à certaines questions des commentaires: Pourquoi 1.Sqrt() ne devrait-il pas être légal? 1.ToString() est.

Certaines langues ne permettent pas d'avoir des méthodes sur les types de valeurs, mais certaines langues le peuvent. Je parle de ceux-ci, y compris Java, ECMAScript, C # et Python (avec __str__(self) définie). La même chose s'applique à d'autres fonctions comme ceil(), floor() etc.

31
Residuum

C'est entièrement un choix de conception de langage. Cela dépend également de l'implémentation sous-jacente des types primitifs et des considérations de performances qui en découlent.

.NET a juste ne méthode statique Math.Sqrt Qui agit sur un double et retourne un double . Tout ce que vous lui passez doit être converti ou promu en double.

double sqrt2 = Math.Sqrt(2d);

D'autre part, vous avez Rust which expose ces opérations en tant que fonctions sur les types :

let sqrt2 = 2.0f32.sqrt();
let higher = 2.0f32.max(3.0f32);

Mais Rust a également une syntaxe d'appel de fonction universelle (quelqu'un l'a mentionné plus tôt), vous pouvez donc choisir ce que vous voulez.

let sqrt2 = f32::sqrt(2.0f32);
let higher = f32::max(2.0f32, 3.0f32);
20
angelsl

Supposons que nous concevons un nouveau langage et que nous voulons que Sqrt soit une méthode d'instance. Nous examinons donc la classe double et commençons la conception. Il n'a évidemment aucune entrée (autre que l'instance) et retourne un double. Nous écrivons et testons le code. La perfection.

Mais prendre la racine carrée d'un entier est également valable, et nous ne voulons pas forcer tout le monde à se convertir en double juste pour prendre une racine carrée. Nous passons donc à int et commençons à concevoir. Que revient-il? Nous pourrions retourner un int et le faire fonctionner uniquement pour des carrés parfaits, ou arrondir le résultat au int le plus proche (en ignorant le débat sur la méthode d'arrondi appropriée pour l'instant ). Mais que faire si quelqu'un veut un résultat non entier? Devrions-nous avoir deux méthodes - une qui retourne un int et une qui retourne un double (ce qui n'est pas possible dans certaines langues sans changer le nom). Nous décidons donc qu'il doit retourner un double. Maintenant, nous mettons en œuvre. Mais l'implémentation est identique à celle que nous avons utilisée pour double. Copions-collons-nous? Convertissons-nous l'instance en double et appelons-nous à la méthode d'instance that? Pourquoi ne pas mettre la logique dans une méthode de bibliothèque accessible à partir des deux classes. Nous appellerons la bibliothèque Math et la fonction Math.Sqrt.

Pourquoi est-ce Math.Sqrt une fonction statique?:

  • Parce que l'implémentation est la même quel que soit le type numérique sous-jacent
  • Parce qu'elle n'affecte pas une instance particulière (elle prend une valeur et renvoie un résultat)
  • Parce que les types numériques ne dépendent de cette fonctionnalité, il est donc logique de l'avoir dans une classe distincte

Nous n'avons même pas abordé d'autres arguments:

  • Doit-il être nommé GetSqrt car il retourne une nouvelle valeur plutôt que de modifier l'instance?
  • Qu'en est-il de Square? Abs? Trunc? Log10? Ln? Power? Factorial? Sin? Cos? ArcTan?
65
D Stanley

Les opérations mathématiques sont souvent très sensibles aux performances. Par conséquent, nous voudrons utiliser des méthodes statiques qui peuvent être entièrement résolues (et optimisées ou intégrées) au moment de la compilation. Certains langages n'offrent aucun mécanisme pour spécifier des méthodes distribuées statiquement. De plus, le modèle objet de nombreux langages a une surcharge de mémoire considérable qui est inacceptable pour les types "primitifs" tels que double.

Quelques langages nous permettent de définir des fonctions qui utilisent la syntaxe d'invocation de méthode, mais sont en fait distribuées statiquement. Les méthodes d'extension en C # 3.0 ou version ultérieure en sont un exemple. Les méthodes non virtuelles (par exemple, la méthode par défaut pour les méthodes en C++) sont un autre cas, bien que C++ ne prenne pas en charge les méthodes sur les types primitifs. Vous pouvez bien sûr créer votre propre classe wrapper en C++ qui décore un type primitif avec diverses méthodes, sans surcharge d'exécution. Cependant, vous devrez convertir manuellement les valeurs dans ce type d'encapsuleur.

Il existe quelques langages qui définissent des méthodes sur leurs types numériques. Ce sont généralement des langages très dynamiques où tout est un objet. Ici, la performance est une considération secondaire à l'élégance conceptuelle, mais ces langages ne sont généralement pas utilisés pour le calcul des nombres. Cependant, ces langages peuvent avoir un optimiseur qui peut "décompresser" les opérations sur les primitives.


Avec les considérations techniques à l'écart, nous pouvons examiner si une telle interface mathématique basée sur une méthode serait une bonne interface. Deux problèmes se posent:

  • la notation mathématique est basée sur des opérateurs et des fonctions, pas sur des méthodes. Une expression telle que 42.sqrt Apparaîtra beaucoup plus étrangère à de nombreux utilisateurs que sqrt(42). En tant qu'utilisateur lourd en mathématiques, je préférerais plutôt la possibilité de créer mes propres opérateurs plutôt que la syntaxe d'appel par méthode point.
  • le principe de responsabilité unique nous encourage à limiter le nombre d'opérations faisant partie d'un type aux opérations essentielles. Par rapport à la multiplication, vous avez rarement besoin de la racine carrée. Si votre langue est spécifiquement destinée à l'analyse statistique, fournir plus de primitives (telles que les opérations mean, median, variance, std, normalize sur les listes numériques, ou la fonction Gamma pour les nombres) peut être utile. Pour un langage à usage général, cela alourdit simplement l'interface. Reléguer les opérations non essentielles dans un espace de noms séparé rend le type plus accessible à la majorité des utilisateurs.
25
amon

Je serais motivé par le fait qu'il y a une tonne de fonctions mathématiques à usage spécial, et plutôt que de remplir chaque type mathématique avec toutes (ou un sous-ensemble aléatoire) de ces fonctions, vous les mettez dans une classe utilitaire. Sinon, vous pollueriez votre info-bulle de saisie semi-automatique ou vous forceriez les gens à toujours regarder à deux endroits. (Est-ce que sin est suffisamment important pour être membre de Double, ou est-ce dans la classe Math avec des consanguins comme htan et exp1p?)

Une autre raison pratique est qu'il s'avère qu'il peut y avoir différentes façons de mettre en œuvre des méthodes numériques, avec des compromis de performances et de précision différents. Java a Math, et il a également StrictMath.

15

Vous avez correctement observé qu'il y a ici une curieuse symétrie.

Que je dise que sqrt(n) ou n.sqrt() n'a pas vraiment d'importance, ils expriment tous deux la même chose et celle que vous préférez est plus une question de goût personnel qu'autre chose.

C'est aussi la raison pour laquelle certains concepteurs de langage plaident fortement pour rendre les deux syntaxes interchangeables. Le langage de programmation D le permet déjà sous une fonctionnalité appelée Syntaxe d'appel de fonction uniforme . Une fonctionnalité similaire a également été proposée pour la normalisation en C++ . Comme Mark Amery le souligne dans les commentaires , Python le permet aussi.

Ce n'est pas sans problèmes. L'introduction d'un changement de syntaxe fondamental comme celui-ci a de vastes conséquences sur le code existant et est bien sûr également un sujet de discussions controversées parmi les développeurs qui ont été formés pendant des décennies à penser que les deux syntaxes décrivent des choses différentes.

Je suppose que seul le temps nous dira si l'unification des deux est réalisable à long terme, mais c'est certainement une considération intéressante.

6
ComicSansMS

En plus de la réponse de D Stanley vous devez penser au polymorphisme. Des méthodes comme Math.Sqrt doivent toujours renvoyer la même valeur à la même entrée. Rendre la méthode statique est un bon moyen de clarifier ce point, car les méthodes statiques ne sont pas remplaçables.

Vous avez mentionné la méthode ToString (). Ici, vous voudrez peut-être remplacer cette méthode, de sorte que la (sous) classe est représentée d'une autre manière que String comme sa classe parente. Vous en faites donc une méthode d'instance.

3
falx

Eh bien, dans Java il y a un wrapper pour chaque type de base.
Et les types de base ne sont pas des types de classe et n'ont pas de fonctions membres.

Vous avez donc les choix suivants:

  1. Collectez toutes ces fonctions d'assistance dans une classe pro-forma comme Math.
  2. Faites-en une fonction statique sur le wrapper correspondant.
  3. Faites-en une fonction membre sur le wrapper correspondant.
  4. Changez les règles de Java.

Excluons l'option 4, car ... Java est Java, et les adhérents professent l'aimer de cette façon.

Maintenant, nous pouvons également exclure l'option 3, car si l'allocation d'objets est assez bon marché, elle n'est pas gratuite, et le faire encore et encore ajoute vers le haut.

Deux vers le bas, un pour tuer: l'option 2 est également une mauvaise idée, car cela signifie que chaque fonction doit être implémentée pour tous les types , on ne peut pas compter sur l'élargissement de la conversion pour combler les lacunes, ou les incohérences seront vraiment douloureuses.
Et jetez un œil à Java.lang.Math , il y a beaucoup d'écarts, en particulier pour les types plus petits que int respectifs double.

Donc, en fin de compte, le vainqueur clair est l'option un, les rassemblant tous au même endroit dans une classe de fonctions utilitaires.

En revenant à l'option 4, quelque chose dans ce sens s'est produit beaucoup plus tard: vous pouvez demander au compilateur de prendre en compte tous les membres statiques de n'importe quelle classe lorsque vous résolvez des noms depuis assez longtemps. import static someclass.*;

Soit dit en passant, les autres langages n'ont pas ce problème, soit parce qu'ils n'ont aucun préjugé contre les fonctions libres (utilisant éventuellement des espaces de noms) ou bien moins de petits types.

2
Deduplicator

Un point que je ne vois pas mentionné explicitement (bien que amon y fasse allusion) est que la racine carrée peut être considérée comme une opération "dérivée": si l'implémentation ne nous la fournit pas, nous pouvons écrire la nôtre.

Étant donné que la question est étiquetée avec la conception de la langue, nous pourrions envisager une description indépendante de la langue. Bien que de nombreuses langues aient des philosophies différentes, il est très courant dans les paradigmes d'utiliser l'encapsulation pour préserver les invariants; c'est-à-dire pour éviter d'avoir une valeur qui ne se comporte pas comme le suggère son type.

Par exemple, si nous avons une implémentation d'entiers utilisant des mots machine, nous voulons probablement encapsuler la représentation d'une manière ou d'une autre (par exemple pour empêcher les décalages de bits de changer le signe), mais en même temps, nous avons toujours besoin d'accéder à ces bits pour implémenter des opérations comme une addition.

Certains langages peuvent implémenter cela avec des classes et des méthodes privées:

class Int {
    public Int add(Int x) {
      // Do something with the bits
    }
    private List<Boolean> getBits() {
      // ...
    }
}

Certains avec des systèmes de modules:

signature INT = sig
  type int
  val add : int -> int -> int
end

structure Word : INT = struct
  datatype int  = (* ... *)
  fun add x y   = (* Do something with the bits *)
  fun getBits x = (* ... *)
end

Certains à portée lexicale:

(defun getAdder ()
   (let ((getBits (lambda (x) ; ...
         (add     (lambda (x y) ; Do something with the bits
     'add))

Etc. Cependant, aucun de ces mécanismes sont nécessaires pour implémenter la racine carrée: il peut être implémenté en utilisant l'interface public de type numérique, et donc il n'a pas besoin d'accéder à la détails de mise en œuvre encapsulés.

Par conséquent, l'emplacement de la racine carrée se résume à la philosophie/aux goûts de la langue et du concepteur de la bibliothèque. Certains peuvent choisir de le mettre "à l'intérieur" des valeurs numériques (par exemple, en faire une méthode d'instance), certains peuvent choisir de le mettre au même niveau que les opérations primitives (cela peut signifier une méthode d'instance, ou cela peut signifier vivre - extérieur les valeurs numériques, mais intérieur le même module/classe/espace de noms, par exemple en tant que fonction autonome ou méthode statique), certains pourraient choisir de le mettre dans une collection de "helper" fonctions, certains pourraient choisir de le déléguer à des bibliothèques tierces.

2
Warbo