Pourquoi Math.Sqrt () est une fonction statique?

31

Dans une discussion sur les méthodes statiques et d'instance, je pense toujours que cela 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, car dans de nombreux langages, il existe une méthode d'instance ToString().

Pour répondre à quelques questions des commentaires: Pourquoi 1.Sqrt()ne devrait 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éfini). La même chose s'applique à d'autres fonctions comme ceil(), floor()etc.

Résidu
la source
18
Dans quelle langue proposez-vous cela? Serait 1.sqrt()valide?
20
Dans de nombreuses langues (par exemple, java), les doubles sont primitifs (pour des raisons de performances), ils n'ont donc pas de méthodes
Richard Tingle
45
Donc, les types numériques devraient être gonflés de toutes les fonctions mathématiques possibles qui pourraient leur être appliquées?
D Stanley
19
FWIW Je pense que cela Sqrt(x)semble beaucoup plus naturel que x.Sqrt() si cela signifie ajouter la fonction à la classe dans certaines langues, je suis d'accord avec ça. S'il s'agissait d' une méthode d'instance, il x.GetSqrt()serait plus approprié d'indiquer qu'elle renvoie une valeur plutôt que de modifier l'instance.
D Stanley
23
Cette question ne peut pas être indépendante de la langue dans sa forme actuelle. C'est la racine du problème.
risingDarkness

Réponses:

20

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 n'a qu'une seule Math.Sqrtméthode statique qui agit sur a doubleet renvoie adouble . Tout ce que vous lui passez doit être lancé ou promu a double.

double sqrt2 = Math.Sqrt(2d);

D'un autre côté, vous avez Rust qui expose ces opérations comme des 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);
angelsl
la source
1
Il convient de noter que dans .NET, vous pouvez écrire des méthodes d'extension, donc si vous voulez vraiment que cette implémentation ressemble x.Sqrt(), cela peut être fait. public static class DoubleExtensions { public static double Sqrt( this double self) { return Math.Sqrt(self); } }
Zachary Dow
1
En C # 6 également, il peut s'agir simplement de Sqrt(x) msdn.microsoft.com/en-us/library/sf0df423.aspx .
Den
65

Supposons que nous concevons un nouveau langage et que nous voulons Sqrtêtre une méthode d'instance. Nous examinons donc la doubleclasse et commençons à concevoir. Il n'a évidemment aucune entrée (autre que l'instance) et renvoie a 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 à intet commençons à concevoir. Que revient-il? Nous pourrions retourner un intet le faire fonctionner uniquement pour des carrés parfaits, ou arrondir le résultat au plus proche int(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 intet une qui retourne un double(ce qui n'est pas possible dans certaines langues sans changer le nom). Nous décidons donc qu'il devrait retourner a double. Maintenant, nous mettons en œuvre. Mais la mise en œuvre est identique à celle que nous avons utilisée pourdouble. Copions-collons-nous? Allons-nous convertir l'instance en a doubleet appeler cette méthode d'instance? Pourquoi ne pas mettre la logique dans une méthode de bibliothèque accessible à partir des deux classes. Nous appellerons la bibliothèque Mathet la fonction Math.Sqrt.

Pourquoi Math.Sqrtune fonction statique?:

  • Parce que l'implémentation est la même quel que soit le type numérique sous-jacent
  • Parce qu'il n'affecte pas une instance particulière (il prend une valeur et renvoie un résultat)
  • Étant donné que les types numériques ne dépendent pas 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é GetSqrtcar il renvoie une nouvelle valeur plutôt que de modifier l'instance?
  • Et alors Square? Abs? Trunc? Log10? Ln? Power? Factorial? Sin? Cos? ArcTan?
D Stanley
la source
6
Sans parler des joies des 1.sqrt()vs 1.1.sqrt()(ghads, ça a l'air moche) ont-ils une classe de base commune? Quel est le contrat pour sa sqrt()méthode?
5
@MichaelT Bel exemple. Il m'a fallu quatre lectures pour comprendre ce que cela 1.1.Sqrtreprésentait. Intelligent.
D Stanley
17
Je ne suis pas vraiment clair à partir de cette réponse comment la classe statique aide avec votre raison principale. Si vous avez double Sqrt (int) et double Sqrt (double) sur votre classe Math, vous avez deux options: convertir l'int entier en double, puis appeler dans la version double, ou copier et coller la méthode avec les modifications appropriées (le cas échéant) ). Mais ce sont exactement les mêmes options que vous avez décrites pour la version d'instance. Votre autre raisonnement (en particulier votre troisième puce), je suis d'accord avec plus.
Ben Aaronson
14
-1 cette réponse est absurde, qu'est-ce que tout cela a à voir avec être statique? Vous allez devoir décider des réponses à ces mêmes questions de toute façon (et "[avec une fonction statique] l'implémentation est la même") est faux, ou du moins pas plus vrai que ce ne serait par exemple des méthodes ..)
BlueRaja - Danny Pflughoeft
20
"L'implémentation est la même quel que soit le type numérique sous-jacent" est un non-sens absolu. Les implémentations de la fonction racine carrée doivent différer considérablement en fonction du type avec lequel elles travaillent afin de ne pas être horriblement inefficaces.
R ..
25

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. En outre, 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 comme celle- 42.sqrtci 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 à anlysis statistique, puis fournir plus primitives (telles que les opérations mean, median, variance, std, normalizesur 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.
amon
la source
Python est un bon exemple d'un langage tout-est-un-objet utilisé pour le calcul des nombres lourds. Les scalaires NumPy ont en fait des dizaines et des dizaines de méthodes, mais sqrt n'en fait toujours pas partie. La plupart d'entre eux sont des choses comme transposeet meanqui ne sont là que pour fournir une interface uniforme avec les tableaux NumPy, qui sont la véritable structure de données de travail.
user2357112 prend en charge Monica
7
@ user2357112: Le fait est que NumPy lui-même est écrit dans un mélange de C et Cython, avec de la colle Python. Sinon, cela ne pourrait jamais être aussi rapide.
Kevin
1
Je pense que cette réponse se rapproche assez d'une sorte de compromis réel qui a été atteint au cours des années de conception. Dans d'autres nouvelles, cela a-t-il vraiment un sens dans .Net de pouvoir faire "Hello World" .Max () car les extensions LINQ nous le permettent ET est très visible dans Intellisense. Points bonus: quel est le résultat? Bonus Bonus, quel est le résultat en Unicode ...?
Andyz Smith
15

Je serais motivé par le fait qu'il y a une tonne de fonctions mathématiques spéciales, 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 sinassez important pour être membre de Double, ou est-ce dans la Mathclasse avec des consanguins comme htanet exp1p?)

Une autre raison pratique est qu'il s'avère qu'il peut y avoir différentes manières 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 aussi StrictMath.

Aleksandr Dubinsky
la source
J'espère que les concepteurs de langage ne se soucient pas des info-bulles de saisie semi-automatique. De plus, que se passe-t-il Math.<^space>? Cette info-bulle de saisie semi-automatique sera également polluée. Inversement, je pense que votre deuxième paragraphe est probablement l'une des meilleures réponses ici.
Qix
@Qix Ils le font. Cependant, d'autres personnes ici pourraient l'appeler "faire une interface gonflée".
Aleksandr Dubinsky
6

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

Que je dise sqrt(n)ou n.sqrt()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 pourquoi 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 le souligne Mark Amery dans les commentaires , Python le permet également.

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.

ComicSansMS
la source
Python prend déjà en charge ces deux syntaxes. Chaque méthode non statique prend selfcomme premier paramètre, et lorsque vous appelez la méthode en tant que propriété d'une instance, plutôt qu'en tant que propriété de la classe, l'instance est implicitement transmise comme premier argument. Je peux donc écrire "foo".startswith("f")ou str.startswith("foo", "f"), et je peux écrire my_list.append(x)ou list.append(my_list, x).
Mark Amery
@MarkAmery Bon point. Ce n'est pas aussi drastique que ce que font D ou les propositions C ++, mais cela correspond à l'idée générale. Merci d'avoir souligné!
ComicSansMS
3

En plus de la réponse de D Stanley, il faut 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 parent. Vous en faites donc une méthode d'instance.

falx
la source
2

Eh bien, en Java, il existe 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. Rassemblez 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 cela revient encore et encore .

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

Donc, à la fin, 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 considérer tous les membres statiques de n'importe quelle classe lorsque vous résolvez des noms depuis assez longtemps maintenant. import static someclass.*;

Soit dit en passant, d'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.

Déduplicateur
la source
1
Considérez les joies de l'implémentation des variations Math.min()dans tous les types d'encapsuleurs.
Je trouve le # 4 peu convaincant. Math.sqrt()a été créé en même temps que le reste de Java, donc quand la décision a été prise de mettre sqrt (), Mathil n'y avait aucune inertie historique des utilisateurs de Java qui "l'aiment comme ça". Bien qu'il n'y ait pas beaucoup de problème avec sqrt(), le comportement de surcharge de Math.round()est atroce. Être en mesure d'utiliser la syntaxe des membres avec des valeurs de type floatet doubleaurait évité ce problème.
supercat
2

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 à travers 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 n'est nécessaire pour implémenter la racine carrée: il peut être implémenté en utilisant l' interface publique de type numérique, et donc il n'a pas besoin d'accéder aux détails de l'implémentation encapsulée.

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 en dehors de la valeurs numériques, mais à l' intérieur du même module / classe / espace de noms, par exemple en tant que fonction autonome ou méthode statique), certains pourraient choisir de le placer dans une collection de fonctions "d'assistance", certains pourraient choisir de le déléguer à des bibliothèques tierces.

Warbo
la source
Rien n'empêcherait un langage de permettre d'appeler une méthode statique soit en utilisant la syntaxe des membres (comme avec les méthodes d'extension C # ou vb.net) soit avec une variation de la séquence des membres (j'aurais aimé voir une syntaxe à double point, donc afin de permettre aux avantages d'Intellisense de ne pouvoir lister que les fonctions adaptées à l'argument principal, mais éviter toute ambiguïté avec les opérateurs membres réels).
supercat
-2

En Java et C # ToString est une méthode d'objet, la racine de la hiérarchie des classes, donc chaque objet implémentera la méthode ToString. Pour un Integertype, il est naturel que l'implémentation de ToString fonctionne de cette façon.

Donc, votre raisonnement est faux. La raison pour laquelle les types de valeur implémentent ToString n'est pas que certaines personnes étaient comme: hé, ayons une méthode ToString pour les types de valeur. C'est parce que ToString est déjà là et que c'est "la chose la plus naturelle" à produire.

Pieter B
la source
1
Bien sûr, c'est une décision, c'est-à-dire la décision d' objectavoir une ToString()méthode. C'est dans vos mots "certaines personnes étaient comme: hé, ayons une méthode ToString pour les types de valeur".
Residuum
-3

Contrairement à String.substring, Number.sqrt n'est pas vraiment un attribut du nombre mais plutôt un nouveau résultat basé sur votre numéro. Je pense que passer votre numéro à une fonction d'équerrage est plus intuitif.

De plus, l'objet Math contient d'autres membres statiques et il est plus logique de les regrouper et de les utiliser de manière uniforme.

HaLeiVi
la source
3
Exemple de compteur: BigInteger.pow () .