Por que Math.Sqrt () é uma função estática?


31

Em uma discussão sobre métodos estáticos e de instância, eu sempre penso que Sqrt()deveria ser um método de instância de tipos numéricos em vez de um método estático. Por que é que? Obviamente, funciona com um valor.

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

Os tipos de valor obviamente podem ter métodos de instância, pois em muitos idiomas, existe um método de instância ToString().

Para responder a algumas perguntas dos comentários: Por que 1.Sqrt()não deve ser legal? 1.ToString()é.

Alguns idiomas não permitem ter métodos em tipos de valor, mas outros podem. Eu estou falando sobre isso, incluindo Java, ECMAScript, C # e Python (com __str__(self)definido). O mesmo se aplica a outras funções ceil(), como floor()etc.


18
Em que idioma você está propondo isso? Seria 1.sqrt()válido?

20
Em muitos idiomas (por exemplo, java) duplos são primatives (por motivos de desempenho) para que eles não têm métodos
Richard Tingle

45
Portanto, os tipos numéricos devem ser inchados com todas as funções matemáticas possíveis que possam ser aplicadas a eles?
D # Stanley #

19
FWIW: Acho que Sqrt(x)parece muito mais natural do que x.Sqrt() Se isso significa anexar a função à classe em alguns idiomas, estou bem com isso. Se fosse um método de instância, x.GetSqrt()seria mais apropriado indicar que ele está retornando um valor em vez de modificar a instância.
D # Stanley #

23
Esta questão não pode ser independente de idioma em sua forma atual. Essa é a raiz do problema.
risingDarkness

Respostas:


20

É inteiramente uma escolha de design de linguagem. Também depende da implementação subjacente de tipos primitivos e de considerações de desempenho devido a isso.

O .NET possui apenas um Math.Sqrtmétodo estático que atua sobre doublee retorna adouble . Qualquer outra coisa que você passar para ele deve ser convertida ou promovida para a double.

double sqrt2 = Math.Sqrt(2d);

Por outro lado, você possui o Rust, que expõe essas operações como funções nos tipos :

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

Mas o Rust também possui sintaxe de chamada de função universal (alguém mencionou isso anteriormente), para que você possa escolher o que quiser.

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

11
Vale ressaltar que no .NET você pode escrever métodos de extensão; portanto, se você realmente deseja que essa implementação seja semelhante x.Sqrt(), isso pode ser feito. public static class DoubleExtensions { public static double Sqrt( this double self) { return Math.Sqrt(self); } }
Zachary Dow

11
Também no C # 6 , pode ser apenas Sqrt(x) msdn.microsoft.com/en-us/library/sf0df423.aspx .
Den

65

Suponha que estamos projetando uma nova linguagem e queremos Sqrtser um método de instância. Então, olhamos para a doubleclasse e começamos a projetar. Obviamente, não possui entradas (além da instância) e retorna a double. Nós escrevemos e testamos o código. Perfeição.

Mas pegar a raiz quadrada de um número inteiro também é válido, e não queremos forçar todos a converterem em um duplo apenas para obter uma raiz quadrada. Então, passamos inte começamos a projetar. O que isso retorna? Nós poderia retornar um inte fazê-lo funcionar apenas para quadrados perfeitos, ou arredondar o resultado para o mais próximo int(ignorando o debate sobre o método de arredondamento adequado para agora). Mas e se alguém quiser um resultado não inteiro? Devemos ter dois métodos - um que retorna um inte outro que retorna a double(o que não é possível em alguns idiomas sem alterar o nome). Então, decidimos que ele deve retornar a double. Agora implementamos. Mas a implementação é idêntica à que usamos paradouble. Copiamos e colamos? Nós convertemos a instância para a doublee chamamos esse método de instância? Por que não colocar a lógica em um método de biblioteca que pode ser acessado de ambas as classes. Vamos chamar a biblioteca Mathe a função Math.Sqrt.

Por que Math.Sqrtuma função estática ?:

  • Como a implementação é a mesma, independentemente do tipo numérico subjacente
  • Porque não afeta uma instância específica (recebe um valor e retorna um resultado)
  • Como os tipos numéricos não dependem dessa funcionalidade, faz sentido tê-lo em uma classe separada

Ainda não abordamos outros argumentos:

  • Deve ser nomeado, GetSqrtpois retorna um novo valor em vez de modificar a instância?
  • Que tal Square? Abs? Trunc? Log10? Ln? Power? Factorial? Sin? Cos? ArcTan?

6
Sem mencionar as alegrias de 1.sqrt()vs 1.1.sqrt()(ghads, que parece feio) eles têm uma classe base comum? Qual é o contrato para o seu sqrt()método?

5
@MichaelT Bom exemplo. Levei quatro leituras para entender o que 1.1.Sqrtrepresentava. Inteligente.
D # Stanley #

17
Não sou muito claro com esta resposta como a classe estática ajuda com o seu principal motivo. Se você tiver o dobro Sqrt (int) e o dobro Sqrt (double) em sua classe Math, você terá duas opções: converter o int em um double e, em seguida, chame na versão double ou copie e cole o método com as alterações apropriadas (se houver) ) Mas essas são exatamente as mesmas opções que você descreveu para a versão da instância. Seu outro raciocínio (particularmente seu terceiro ponto) eu concordo com mais.
quer

14
-1 esta resposta é um absurdo, o que faz qualquer isto tem a ver com ser estático? Você terá que decidir respostas para as mesmas perguntas de qualquer maneira (e "[com uma função estática] a implementação é a mesma" é falsa, ou pelo menos não mais verdadeira do que seria, por exemplo, métodos ..)
O que você precisa saber para começar o seu

20
"A implementação é a mesma, independentemente do tipo numérico subjacente", é um total absurdo. As implementações da função de raiz quadrada precisam diferir significativamente com base no tipo com o qual estão trabalhando, para não serem terrivelmente ineficientes.
R ..

25

As operações matemáticas geralmente são muito sensíveis ao desempenho. Portanto, queremos usar métodos estáticos que possam ser totalmente resolvidos (e otimizados ou incorporados) em tempo de compilação. Alguns idiomas não oferecem nenhum mecanismo para especificar métodos enviados estaticamente. Além disso, o modelo de objeto de muitas linguagens possui uma sobrecarga considerável de memória que é inaceitável para tipos "primitivos" como double.

Algumas linguagens nos permitem definir funções que usam sintaxe de chamada de método, mas são realmente despachadas estaticamente. Métodos de extensão no C # 3.0 ou posterior são um exemplo. Métodos não virtuais (por exemplo, o padrão para métodos em C ++) são outro caso, embora o C ++ não suporte métodos em tipos primitivos. Obviamente, você poderia criar sua própria classe de wrapper em C ++ que decora um tipo primitivo com vários métodos, sem qualquer sobrecarga de tempo de execução. No entanto, você precisará converter valores manualmente para esse tipo de invólucro.

Existem alguns idiomas que definem métodos em seus tipos numéricos. Geralmente, são linguagens altamente dinâmicas em que tudo é um objeto. Aqui, o desempenho é uma consideração secundária à elegância conceitual, mas essas linguagens geralmente não são usadas para processamento de números. No entanto, esses idiomas podem ter um otimizador que pode "unbox" operações em primitivas.


Com as considerações técnicas fora do caminho, podemos considerar se essa interface matemática baseada em método seria uma boa interface. Duas questões surgem:

  • notação matemática é baseada em operadores e funções, não em métodos. Uma expressão como 42.sqrtparecerá muito mais estranha para muitos usuários do que sqrt(42). Como um usuário pesado em matemática, prefiro a capacidade de criar meus próprios operadores em vez da sintaxe de chamada de método de ponto.
  • o princípio de responsabilidade única nos encoraja a limitar o número de operações que fazem parte de um tipo às operações essenciais. Comparado com a multiplicação, você precisa da raiz quadrada notavelmente raramente. Se o seu idioma é especificamente destinado a anlysis estatística, em seguida, fornecendo mais primitivas (tais como operações mean, median, variance, std, normalizeem listas numérico ou a função Gamma para números) pode ser útil. Para uma linguagem de uso geral, isso apenas sobrecarrega a interface. Relegar operações não essenciais a um espaço para nome separado torna o tipo mais acessível para a maioria dos usuários.

Python é um bom exemplo de uma linguagem tudo é um objeto usada para processamento de números pesados. Os escalares NumPy realmente têm dezenas e dezenas de métodos, mas o sqrt ainda não é um deles. A maioria deles é semelhante transposee meanexiste apenas para fornecer uma interface uniforme com matrizes NumPy, que são a estrutura de dados real do corpo de trabalho.
User2357112 suporta Monica

7
@ user2357112: O problema é que o NumPy está escrito em uma mistura de C e Cython, com um pouco de cola Python. Caso contrário, nunca poderia ser tão rápido quanto é.
Kevin

11
Penso que esta resposta atinge muito de perto algum tipo de compromisso do mundo real que foi alcançado ao longo dos anos de design. Em outras notícias, realmente faz sentido no .Net poder "Hello World" .Max () porque as extensões LINQ nos permitem AND se tornam muito visíveis no Intellisense. Pontos de bônus: Qual é o resultado? Bônus Bônus, qual é o resultado em Unicode ...?
### Andyz Smith #

15

Eu ficaria motivado pelo fato de haver uma tonelada de funções matemáticas para fins especiais e, em vez de preencher todos os tipos de matemática com todas (ou um subconjunto aleatório) daquelas funções que você as coloca em uma classe de utilidade. Caso contrário, você poluirá sua dica de ferramenta de preenchimento automático ou forçará as pessoas a procurar sempre em dois lugares. (É sinimportante o suficiente para ser um membro de Double, ou está na Mathclasse junto com os parentes como htane exp1p?)

Outra razão prática é que pode haver diferentes maneiras de implementar métodos numéricos, com diferentes trocas de desempenho e precisão. Java tem Math, e também tem StrictMath.


Espero que os designers de idiomas não se importem com as dicas de ferramentas de preenchimento automático. Também, o que acontece Math.<^space>? Essa dica de ferramenta de preenchimento automático também será poluída. Inversamente, acho que seu segundo parágrafo é provavelmente uma das melhores respostas aqui.
Qix

@Qix Eles fazem. Embora outras pessoas aqui possam chamá-lo de "tornar uma interface inchada".
Aleksandr Dubinsky

6

Você observou corretamente que existe uma curiosa simetria em jogo aqui.

Se digo sqrt(n)ou n.sqrt()não realmente importa, ambos expressam a mesma coisa e qual você prefere é mais uma questão de gosto pessoal do que qualquer outra coisa.

É também por isso que há um forte argumento de certos designers de linguagem para tornar as duas sintaxes intercambiáveis. A linguagem de programação D já permite isso em um recurso chamado Sintaxe de chamada de função uniforme . Um recurso semelhante também foi proposto para padronização em C ++ . Como Mark Amery aponta nos comentários , o Python também permite isso.

Isso não é sem problemas. A introdução de uma mudança fundamental de sintaxe como essa tem conseqüências abrangentes para o código existente e, é claro, também é um tópico de discussões controversas entre desenvolvedores treinados há décadas para pensar nas duas sintaxes como descrevendo coisas diferentes.

Acho que apenas o tempo dirá se a unificação dos dois é viável a longo prazo, mas é definitivamente uma consideração interessante.


O Python já suporta essas duas sintaxes. Todo método não estático assume selfcomo primeiro parâmetro e, quando você chama o método como propriedade de uma instância, em vez de propriedade da classe, a instância é passada implicitamente como o primeiro argumento. Portanto, eu posso escrever "foo".startswith("f")ou str.startswith("foo", "f"), e eu posso escrever my_list.append(x)ou list.append(my_list, x).
Mark Amery

@MarkAmery Bom ponto. Isso não é tão drástico quanto o que as propostas de D ou C ++ fazem, mas se encaixa na idéia geral. Obrigado por apontar!
ComicSansMS

3

Além da resposta de D Stanley, você tem que pensar em polimorfismo. Métodos como Math.Sqrt sempre devem retornar o mesmo valor para a mesma entrada. Tornar o método estático é uma boa maneira de esclarecer esse ponto, pois os métodos estáticos não são substituíveis.

Você mencionou o método ToString (). Aqui você pode substituir esse método, para que a (sub) classe seja representada de outra maneira como String como sua classe pai. Então você o torna um método de instância.


2

Bem, em Java, existe um invólucro para cada tipo básico.
E tipos básicos não são tipos de classe e não têm funções-membro.

Então, você tem as seguintes opções:

  1. Colete todas essas funções auxiliares em uma classe pró-forma Math.
  2. Torne uma função estática no invólucro correspondente.
  3. Torne-a uma função de membro no wrapper correspondente.
  4. Mude as regras do Java.

Vamos descartar a opção 4, porque ... Java é Java, e os aderentes professam gostar dessa maneira.

Agora, nós também podemos descartar a opção 3, porque ao alocar objetos é bastante barato, não é livre, e fazer isso uma e outra vez não se somam.

Dois a menos, um ainda a ser eliminado: a opção 2 também é uma péssima idéia, porque significa que todas as funções devem ser implementadas para todos os tipos, não se pode contar com a ampliação da conversão para preencher as lacunas, ou as inconsistências realmente prejudicarão.
E, observando java.lang.Math, existem muitas lacunas, especialmente para os tipos menores que os intrespectivos double.

Portanto, no final, o vencedor claro é a opção um, coletando todos eles em um só lugar em uma classe de função de utilidade.

Voltando à opção 4, algo nessa direção realmente aconteceu muito mais tarde: você pode solicitar ao compilador que considere todos os membros estáticos de qualquer classe que desejar ao resolver nomes por um longo tempo. import static someclass.*;

Além disso, outros idiomas não têm esse problema, porque não têm preconceito contra funções livres (opcionalmente usando espaços para nome) ou muito menos tipos pequenos.


11
Considere as alegrias de implementar as variações Math.min()em todos os tipos de wrapper.

Acho # 4 não convincente. Math.sqrt()foi criado ao mesmo tempo que o restante do Java, portanto, quando foi tomada a decisão de colocar o sqrt (), Mathnão havia inércia histórica dos usuários do Java que "gostam dessa maneira". Embora não exista muito problema sqrt(), o comportamento de sobrecarga de Math.round()é atroz. Ser capaz de usar a sintaxe do membro com valores do tipo floate doubleteria evitado esse problema.
Supercat

2

Um ponto que não vejo mencionado explicitamente (embora amon faça alusão a ele) é que a raiz quadrada pode ser pensada como uma operação "derivada": se a implementação não fornecer isso para nós, podemos escrever nossos próprios.

Como a pergunta está marcada com design de linguagem, podemos considerar uma descrição independente de idioma. Embora muitas línguas tenham filosofias diferentes, é muito comum entre paradigmas usar encapsulamento para preservar invariantes; ou seja, para evitar ter um valor que não se comporte conforme o tipo sugerido.

Por exemplo, se tivermos alguma implementação de números inteiros usando palavras de máquina, provavelmente queremos encapsular a representação de alguma forma (por exemplo, para evitar que mudanças de bits alterem o sinal), mas ao mesmo tempo ainda precisamos acessar esses bits para implementar operações como Adição.

Alguns idiomas podem implementar isso com classes e métodos privados:

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

Alguns com 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

Alguns com escopo lexical:

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

E assim por diante. No entanto, nenhum desses mecanismos é necessário para implementar a raiz quadrada: ele pode ser implementado usando a interface pública de um tipo numérico e, portanto, não precisa acessar os detalhes da implementação encapsulada.

Portanto, a localização da raiz quadrada se resume à filosofia / gostos da linguagem e do designer da biblioteca. Alguns podem optar por colocá-lo "dentro" dos valores numéricos (por exemplo, torná-lo um método de instância), outros podem optar por colocá-lo no mesmo nível das operações primitivas (isso pode significar um método de instância ou pode viver fora do valores numéricos, mas dentro do mesmo módulo / classe / espaço para nome, por exemplo, como uma função autônoma ou método estático), alguns podem optar por colocá-lo em uma coleção de funções "auxiliares", outros podem optar por delegá-lo a bibliotecas de terceiros.


Nada impediria que uma linguagem permitisse que um método estático fosse chamado usando a sintaxe de membro (como nos métodos de extensão C # ou vb.net) ou com uma variação da sequência de membros (eu gostaria de ter visto uma sintaxe de ponto duplo, portanto para permitir às vantagens do Intellisense poder listar apenas funções adequadas ao argumento principal, mas evitar ambiguidade com operadores de membros reais).
Supercat

-2

Em Java e C #, o ToString é um método de objeto, a raiz da hierarquia de classes; portanto, todo objeto implementará o método ToString. Para um tipo inteiro, é natural que a implementação do ToString funcione dessa maneira.

Então, seu raciocínio está errado. A razão pela qual os tipos de valor implementam o ToString não é o que algumas pessoas pensam: ei, vamos usar o método ToString para os tipos Value. É porque o ToString já está lá e é "a coisa mais natural" a ser produzida.


11
Claro que é uma decisão, ou seja, a decisão de objectter um ToString()método. Ou seja, em suas palavras "algumas pessoas eram como: ei, vamos ter um método ToString para tipos de valor".
Residuum 04/11

-3

Ao contrário de String.substring, Number.sqrt não é realmente um atributo do número, mas um novo resultado com base no seu número. Eu acho que passar seu número para uma função quadrática é mais intuitivo.

Além disso, o objeto Math contém outros membros estáticos e faz mais sentido agrupá-los e usá-los de maneira uniforme.


3
Exemplo de contador: BigInteger.pow () .
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.