¿Por qué Math.Sqrt () es una función estática?

31

En una discusión sobre métodos estáticos y de instancia, siempre pienso que ese Sqrt()debería ser un método de instancia de tipos de números en lugar de un método estático. ¿Porqué es eso? Obviamente funciona en un valor.

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

Los tipos de valor obviamente pueden tener métodos de instancia, ya que en muchos idiomas hay un método de instancia ToString().

Para responder algunas preguntas de los comentarios: ¿Por qué 1.Sqrt()no debería ser legal? 1.ToString()es.

Algunos idiomas no permiten tener métodos sobre tipos de valores, pero algunos idiomas sí. Estoy hablando de estos, incluidos Java, ECMAScript, C # y Python (con __str__(self)definido). Lo mismo se aplica a otras funciones como ceil(), floor()etc.

Residuo
fuente
18
¿En qué idioma estás proponiendo esto? Sería 1.sqrt()valido?
20
En muchos idiomas (por ejemplo, Java), los dobles son primitivos (por razones de rendimiento), por lo que no tienen métodos
Richard Tingle
45
Entonces, ¿los tipos numéricos deberían hincharse con todas las funciones matemáticas posibles que podrían aplicarse a ellos?
D Stanley
19
FWIW Creo que se Sqrt(x)ve mucho más natural que x.Sqrt() si eso significa anteponer la función con la clase en algunos idiomas, estoy de acuerdo con eso. Si se tratara de un método de instancia, x.GetSqrt()sería más apropiado indicar que está devolviendo un valor en lugar de modificar la instancia.
D Stanley
23
Esta pregunta no puede ser independiente del lenguaje en su forma actual. Esa es la raíz del problema.
risingDarkness el

Respuestas:

20

Es completamente una elección del diseño del lenguaje. También depende de la implementación subyacente de los tipos primitivos y las consideraciones de rendimiento debido a eso.

.NET tiene solo un Math.Sqrtmétodo estático que actúa sobre ay doubledevuelve adouble . Cualquier otra cosa que le pases debe ser lanzada o promovida a a double.

double sqrt2 = Math.Sqrt(2d);

Por otro lado, tiene Rust que expone estas operaciones como funciones en los tipos :

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

Pero Rust también tiene una sintaxis de llamada de función universal (alguien lo mencionó anteriormente), por lo que puede elegir lo que quiera.

let sqrt2 = f32::sqrt(2.0f32);
let higher = f32::max(2.0f32, 3.0f32);
angelsl
fuente
1
Vale la pena señalar que en .NET puede escribir métodos de extensión, por lo que si realmente desea que se vea esta implementación x.Sqrt(), puede hacerlo. public static class DoubleExtensions { public static double Sqrt( this double self) { return Math.Sqrt(self); } }
Zachary Dow
1
También en C # 6 puede ser simplemente Sqrt(x) msdn.microsoft.com/en-us/library/sf0df423.aspx .
Den
65

Supongamos que estamos diseñando un nuevo lenguaje y queremos Sqrtser un método de instancia. Entonces miramos la doubleclase y comenzamos a diseñar. Obviamente no tiene entradas (aparte de la instancia) y devuelve a double. Escribimos y probamos el código. Perfección.

Pero tomar la raíz cuadrada de un número entero también es válido, y no queremos obligar a todos a convertir a un doble solo para sacar una raíz cuadrada. Entonces nos movemos inty comenzamos a diseñar. ¿Qué devuelve? Nos podríamos devolver una inty hacer que funcione sólo para los cuadrados perfectos, o redondear el resultado al más cercano int(ignorando el debate sobre el método de redondeo adecuado por ahora). Pero, ¿qué pasa si alguien quiere un resultado no entero? Deberíamos tener dos métodos: uno que devuelve un inty otro que devuelve un double(que no es posible en algunos idiomas sin cambiar el nombre). Entonces decidimos que debería devolver a double. Ahora lo implementamos. Pero la implementación es idéntica a la que usamos paradouble. ¿Copiamos y pegamos? ¿Lanzamos la instancia a ay doublellamamos a ese método de instancia? ¿Por qué no poner la lógica en un método de biblioteca al que se pueda acceder desde ambas clases? Llamaremos a la biblioteca Mathy a la función Math.Sqrt.

¿Por qué es Math.Sqrtuna función estática ?:

  • Porque la implementación es la misma independientemente del tipo numérico subyacente
  • Porque no afecta a una instancia en particular (toma un valor y devuelve un resultado)
  • Debido a que los tipos numéricos no dependen de esa funcionalidad, por lo tanto, tiene sentido tenerlo en una clase separada

Ni siquiera hemos abordado otros argumentos:

  • ¿Debería nombrarse GetSqrtya que devuelve un nuevo valor en lugar de modificar la instancia?
  • ¿Qué hay de Square? Abs? Trunc? Log10? Ln? Power? Factorial? Sin? Cos? ArcTan?
D Stanley
fuente
66
Sin mencionar las alegrías de 1.sqrt()vs 1.1.sqrt()(ghads, que se ve feo) ¿tienen una clase base común? ¿Cuál es el contrato para su sqrt()método?
55
@MichaelT Buen ejemplo. Me llevó cuatro lecturas entender lo que 1.1.Sqrtrepresentaba. Inteligente.
D Stanley
17
De esta respuesta no estoy realmente claro cómo la clase estática ayuda con su razón principal. Si tiene doble Sqrt (int) y doble Sqrt (doble) en su clase de Matemáticas, tiene dos opciones: convertir el int en un doble, luego llamar a la versión doble, o copiar y pegar el método con los cambios apropiados (si corresponde) ) Pero esas son exactamente las mismas opciones que describió para la versión de instancia. Su otro razonamiento (particularmente su tercer punto) Estoy de acuerdo con más.
Ben Aaronson el
14
-1 esta respuesta es absurdo, ¿qué ninguna de esto tiene que ver con ser estático? Tendrás que decidir las respuestas a esas mismas preguntas de cualquier manera (y "[con una función estática] la implementación es la misma" es falsa, o al menos no más verdadera de lo que sería, por ejemplo, con los métodos ...)
BlueRaja - Danny Pflughoeft
20
"La implementación es la misma independientemente del tipo numérico subyacente" es una tontería. Las implementaciones de la función de raíz cuadrada deben diferir significativamente según el tipo con el que están trabajando para no ser terriblemente ineficientes.
R ..
25

Las operaciones matemáticas a menudo son muy sensibles al rendimiento. Por lo tanto, querremos utilizar métodos estáticos que puedan resolverse por completo (y optimizarse o incorporarse) en tiempo de compilación. Algunos idiomas no ofrecen ningún mecanismo para especificar métodos despachados estáticamente. Además, el modelo de objetos de muchos idiomas tiene una sobrecarga de memoria considerable que es inaceptable para tipos "primitivos" como double.

Algunos idiomas nos permiten definir funciones que utilizan la sintaxis de invocación de métodos, pero en realidad se envían estáticamente. Los métodos de extensión en C # 3.0 o posterior son un ejemplo. Los métodos no virtuales (por ejemplo, el predeterminado para los métodos en C ++) son otro caso, aunque C ++ no admite métodos en tipos primitivos. Por supuesto, podría crear su propia clase de contenedor en C ++ que decora un tipo primitivo con varios métodos, sin ningún gasto de tiempo de ejecución. Sin embargo, tendrá que convertir manualmente los valores a ese tipo de contenedor.

Hay un par de idiomas que definen métodos en sus tipos numéricos. Estos suelen ser lenguajes muy dinámicos donde todo es un objeto. Aquí, el rendimiento es una consideración secundaria a la elegancia conceptual, pero esos lenguajes generalmente no se usan para la combinación de números. Sin embargo, estos lenguajes pueden tener un optimizador que puede "desempaquetar" operaciones en primitivas.


Sin tener en cuenta las consideraciones técnicas, podemos considerar si una interfaz matemática basada en métodos sería una buena interfaz. Surgen dos problemas:

  • La notación matemática se basa en operadores y funciones, no en métodos. Una expresión como 42.sqrtparecerá mucho más ajena a muchos usuarios que sqrt(42). Como usuario con muchas matemáticas, prefiero la posibilidad de crear mis propios operadores sobre la sintaxis de llamada al método de puntos.
  • El Principio de responsabilidad única nos anima a limitar el número de operaciones que son parte de un tipo a las operaciones esenciales. En comparación con la multiplicación, necesita la raíz cuadrada muy raramente. Si el idioma está destinado específicamente para fines de análisis estadístico, a continuación, proporcionando más primitivas (como operaciones mean, median, variance, std, normalizeen las listas numéricas, o la función gamma para los números) puede ser útil. Para un lenguaje de propósito general, esto solo pesa la interfaz. Relegar operaciones no esenciales a un espacio de nombres separado hace que el tipo sea más accesible para la mayoría de los usuarios.
amon
fuente
Python es un buen ejemplo de un lenguaje de todo-es-un-objeto usado para la gran cantidad de números. Los escalares NumPy en realidad tienen docenas y docenas de métodos, pero sqrt todavía no es uno de ellos. La mayoría de ellos son cosas similares transposey meansolo están ahí para proporcionar una interfaz uniforme con las matrices NumPy, que son la estructura de datos real del caballo de batalla.
user2357112 es compatible con Monica el
77
@ user2357112: La cuestión es que NumPy está escrito en una mezcla de C y Cython, con algo de pegamento de Python. De lo contrario, nunca podría ser tan rápido como es.
Kevin
1
Creo que esta respuesta coincide bastante con algún tipo de compromiso del mundo real que se ha alcanzado a lo largo de los años del diseño. En otras noticias, ¿tiene sentido en .Net poder hacer "Hello World" .Max () ya que las extensiones LINQ nos permiten Y son muy visibles en Intellisense. Puntos de bonificación: ¿Cuál es el resultado? Bonus Bonus, ¿cuál es el resultado en Unicode ...?
Andyz Smith el
15

Me motivaría el hecho de que hay un montón de funciones matemáticas de propósito especial, y en lugar de llenar cada tipo de matemáticas con todas (o un subconjunto aleatorio) de esas funciones, las coloca en una clase de utilidad. De lo contrario, contaminaría la información sobre herramientas de autocompletar o forzaría a las personas a mirar siempre en dos lugares. (¿Es lo sinsuficientemente importante como para ser miembro Double, o está en la Mathclase junto con endogámicos como htany exp1p?)

Otra razón práctica es que resulta que puede haber diferentes formas de implementar métodos numéricos, con diferentes compensaciones de rendimiento y precisión. Java tiene Math, y también tiene StrictMath.

Aleksandr Dubinsky
fuente
Espero que los diseñadores de idiomas no se preocupen por la información sobre herramientas de autocompletar. Además, ¿qué pasa Math.<^space>? Esa información sobre herramientas de autocompletar también se contaminará. Inversamente, creo que su segundo párrafo es probablemente una de las mejores respuestas aquí.
Qix
@Qix lo hacen. Aunque, otras personas aquí podrían llamarlo "hacer una interfaz hinchada".
Aleksandr Dubinsky
6

Has observado correctamente que hay una curiosa simetría en juego aquí.

Si digo sqrt(n)o n.sqrt()no realmente importa, ambos expresan lo mismo y cuál prefieres es más una cuestión de gusto personal que otra cosa.

Esa es también la razón por la cual existe un fuerte argumento de ciertos diseñadores de lenguaje para hacer que las dos sintaxis sean intercambiables. El lenguaje de programación D ya lo permite bajo una característica llamada Sintaxis uniforme de llamada de función . También se ha propuesto una característica similar para la estandarización en C ++ . Como Mark Amery señala en los comentarios , Python también lo permite.

Esto no está exento de problemas. La introducción de un cambio de sintaxis fundamental como este tiene consecuencias de gran alcance para el código existente y, por supuesto, también es un tema de debates controvertidos entre desarrolladores que han sido entrenados durante décadas para pensar que las dos sintaxis describen cosas diferentes.

Supongo que solo el tiempo dirá si la unificación de los dos es factible a largo plazo, pero definitivamente es una consideración interesante.

ComicSansMS
fuente
Python ya es compatible con ambas sintaxis. Cada método no estático toma selfcomo primer parámetro, y cuando llama al método como una propiedad de una instancia, en lugar de como una propiedad de la clase, la instancia se pasa implícitamente como el primer argumento. Por lo tanto, puedo escribir "foo".startswith("f")o str.startswith("foo", "f"), y puedo escribir my_list.append(x)o list.append(my_list, x).
Mark Amery
@MarkAmery Buen punto. Esto no es tan drástico como lo que hacen las propuestas D o C ++, pero se ajusta a la idea general. Gracias por señalar!
ComicSansMS
3

Además de la respuesta de D Stanley, debes pensar en el polimorfismo. Métodos como Math.Sqrt siempre deben devolver el mismo valor a la misma entrada. Hacer que el método sea estático es una buena manera de aclarar este punto, ya que los métodos estáticos no se pueden anular.

Usted mencionó el método ToString (). Aquí es posible que desee anular este método, por lo que la (sub) clase se representa de otra manera como String como su clase principal. Entonces lo convierte en un método de instancia.

falx
fuente
2

Bueno, en Java hay un contenedor para cada tipo básico.
Y los tipos básicos no son tipos de clase y no tienen funciones miembro.

Entonces, tiene las siguientes opciones:

  1. Recoge todas esas funciones auxiliares en una clase pro-forma como Math.
  2. Conviértalo en una función estática en el contenedor correspondiente.
  3. Conviértalo en una función miembro en el contenedor correspondiente.
  4. Cambia las reglas de Java.

Descartemos la opción 4, porque ... Java es Java, y los adherentes dicen que les gusta de esa manera.

Ahora, también podemos descartar la opción 3 porque si bien la asignación de objetos es bastante barata, no es gratis, y hacerlo una y otra vez sí se suma.

Dos abajo, uno aún por matar: la opción 2 también es una mala idea, porque significa que cada función debe implementarse para cada tipo, no se puede confiar en ampliar la conversión para llenar los vacíos, o las inconsistencias realmente dolerán.
Y echando un vistazo java.lang.Math, hay muchas lagunas, especialmente para los tipos más pequeños que los intrespectivos double.

Entonces, al final, el claro vencedor es la opción uno, reuniéndolos a todos en un solo lugar en una clase de función de utilidad.

Volviendo a la opción 4, algo en esa dirección sucedió mucho más tarde: puede pedirle al compilador que considere todos los miembros estáticos de cualquier clase que desee al resolver nombres durante bastante tiempo. import static someclass.*;

Por otro lado, otros lenguajes no tienen ese problema, ya sea porque no tienen prejuicios contra las funciones libres (opcionalmente usando espacios de nombres) o muchos menos tipos pequeños.

Deduplicador
fuente
1
Considere las alegrías de implementar las variaciones Math.min()en todos los tipos de envoltorios.
Me parece # 4 poco convincente. Math.sqrt()fue creado al mismo tiempo que el resto de Java, por lo que cuando se tomó la decisión de poner sqrt () Mathno hubo inercia histórica de los usuarios de Java que "les gusta de esa manera". Si bien no hay muchos problemas sqrt(), el comportamiento de sobrecarga de Math.round()es atroz. Ser capaz de usar la sintaxis de miembro con valores de tipo floaty doublehabría evitado ese problema.
supercat
2

Un punto que no veo mencionado explícitamente (aunque amon lo alude) es que la raíz cuadrada se puede considerar como una operación "derivada": si la implementación no nos la proporciona, podemos escribir la nuestra.

Dado que la pregunta está etiquetada con diseño de lenguaje, podríamos considerar alguna descripción independiente del lenguaje. Aunque muchos idiomas tienen filosofías diferentes, es muy común en todos los paradigmas utilizar la encapsulación para preservar invariantes; es decir, para evitar tener un valor que no se comporte como su tipo sugeriría.

Por ejemplo, si tenemos alguna implementación de enteros usando palabras de máquina, probablemente queremos encapsular la representación de alguna manera (por ejemplo, para evitar que los cambios de bit cambien el signo), pero al mismo tiempo todavía necesitamos acceso a esos bits para implementar operaciones como adición.

Algunos idiomas pueden implementar esto con clases y métodos privados:

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

Algunos con sistemas de módulos:

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

Algunos con alcance léxico:

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

Y así. Sin embargo, ninguno de estos mecanismos es necesario para implementar la raíz cuadrada: puede implementarse utilizando la interfaz pública de tipo numérico y, por lo tanto, no necesita acceso a los detalles de implementación encapsulados.

Por lo tanto, la ubicación de la raíz cuadrada se reduce a la filosofía / gustos del lenguaje y del diseñador de la biblioteca. Algunos pueden optar por ponerlo "dentro" de los valores numéricos (por ejemplo, convertirlo en un método de instancia), algunos pueden optar por ponerlo al mismo nivel que las operaciones primitivas (esto puede significar un método de instancia, o puede significar vivir fuera del valores numéricos, pero dentro del mismo módulo / clase / espacio de nombres, por ejemplo, como una función independiente o método estático), algunos podrían optar por ponerlo en una colección de funciones "auxiliares", algunos podrían optar por delegarlo en bibliotecas de terceros.

Warbo
fuente
Nada impediría que un lenguaje permita que se llame a un método estático utilizando la sintaxis de miembros (como con los métodos de extensión C # o vb.net) o con una variación de la secuencia de miembros (me hubiera gustado haber visto una sintaxis de doble punto, así que para permitir las ventajas de Intellisense de poder enumerar solo las funciones que eran adecuadas para el argumento principal, pero evitar la ambigüedad con operadores miembros reales).
supercat
-2

En Java y C # ToString es un método de objeto, la raíz de la jerarquía de clases, por lo que cada objeto implementará el método ToString. Para un Integertype es natural que la implementación de ToString funcione de esta manera.

Entonces tu razonamiento está mal. La razón por la que los tipos de valor implementan ToString no es que algunas personas fueran así: oye, tengamos un método ToString para los tipos de Valor. Es porque ToString ya está allí y es "lo más natural" de salida.

Pieter B
fuente
1
Por supuesto, es una decisión, es decir, la decisión de objecttener un ToString()método. Eso está en sus palabras "algunas personas eran como: oye, tengamos un método ToString para los tipos de valor".
Residuo
-3

A diferencia de String.substring, Number.sqrt no es realmente un atributo del número, sino un nuevo resultado basado en su número. Creo que pasar su número a una función de cuadratura es más intuitivo.

Además, el objeto Math contiene otros miembros estáticos y tiene más sentido agruparlos y usarlos de manera uniforme.

HaLeiVi
fuente
3
Ejemplo de contador: BigInteger.pow () .