Perché Math.Sqrt () è una funzione statica?


31

In una discussione sui metodi statici e di istanza, penso sempre che Sqrt()dovrebbe essere un metodo di istanza di tipi numerici anziché un metodo statico. Perché? Ovviamente funziona su un valore.

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

I tipi di valore ovviamente possono avere metodi di istanza, poiché in molte lingue esiste un metodo di istanza ToString().

Per rispondere ad alcune domande dai commenti: Perché 1.Sqrt()non dovrebbe essere legale? 1.ToString()è.

Alcune lingue non consentono di avere metodi sui tipi di valore, ma alcune lingue possono. Sto parlando di questi, tra cui Java, ECMAScript, C # e Python (con __str__(self)definito). Lo stesso vale per altre funzioni come ceil(), floor()ecc.


18
In che lingua lo stai proponendo? Sarebbe 1.sqrt()valido?

20
In molte lingue (es. Java) i doppi sono primitivi (per motivi di performance) quindi non hanno metodi
Richard Tingle,

45
Quindi i tipi numerici dovrebbero essere gonfiati con ogni possibile funzione matematica che potrebbe essere applicata a loro?
D Stanley,

19
FWIW Credo che Sqrt(x)sembra molto più naturale che x.Sqrt() se questo significa anteporre la funzione con la classe in alcune lingue sto bene con quello. Se fosse un metodo di istanza allora x.GetSqrt()sarebbe più opportuno indicare che sta restituendo un valore piuttosto che modifica l'istanza.
D Stanley,

23
Questa domanda non può essere agnostica nella lingua nella sua forma attuale. Questa è la radice del problema.
risingDarkness,

Risposte:


20

È interamente una scelta di design del linguaggio. Dipende anche dall'implementazione di base dei tipi primitivi e dalle considerazioni sulle prestazioni dovute a ciò.

.NET ha solo un Math.Sqrtmetodo statico che agisce su a doublee restituisce adouble . Qualsiasi altra cosa che passi ad essa deve essere lanciata o promossa a double.

double sqrt2 = Math.Sqrt(2d);

D'altra parte, hai Rust che espone queste operazioni come funzioni sui tipi :

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

Ma Rust ha anche una sintassi della chiamata di funzione universale (qualcuno l'ha menzionato prima), quindi puoi scegliere quello che ti piace.

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

1
Vale la pena notare che in .NET è possibile scrivere metodi di estensione, quindi se si desidera davvero che questa implementazione appaia x.Sqrt(), può essere fatto. public static class DoubleExtensions { public static double Sqrt( this double self) { return Math.Sqrt(self); } }
Zachary Dow,

1
Anche in C # 6 può essere solo Sqrt(x) msdn.microsoft.com/en-us/library/sf0df423.aspx .
Den

65

Supponiamo che stiamo progettando un nuovo linguaggio e che vogliamo Sqrtessere un metodo di istanza. Quindi guardiamo la doubleclasse e iniziamo a progettare. Ovviamente non ha input (diversi dall'istanza) e restituisce a double. Scriviamo e testiamo il codice. Perfezione.

Ma anche prendere la radice quadrata di un numero intero è valido e non vogliamo forzare tutti a convertirsi in un doppio solo per prendere una radice quadrata. Quindi ci spostiamo inte iniziamo a progettare. Cosa ritorna? Abbiamo potuto restituire un inte farlo funzionare solo per i quadrati perfetti, o arrotondare il risultato al più vicino int(ignorando il dibattito circa il metodo di arrotondamento corretta per ora). E se qualcuno volesse un risultato non intero? Dovremmo avere due metodi: uno che restituisce un inte uno che restituisce un double(che non è possibile in alcune lingue senza cambiare il nome). Quindi decidiamo che dovrebbe restituire a double. Ora implementiamo. Ma l'implementazione è identica a quella per cui abbiamo usatodouble. Copia e incolla? Trasmettiamo l'istanza a doublee chiamiamo quel metodo di istanza? Perché non inserire la logica in un metodo di libreria a cui è possibile accedere da entrambe le classi. Chiameremo la libreria Mathe la funzione Math.Sqrt.

Perchè è Math.Sqrtuna funzione statica ?:

  • Perché l'implementazione è la stessa indipendentemente dal tipo numerico sottostante
  • Perché non influisce su una particolare istanza (accetta un valore e restituisce un risultato)
  • Poiché i tipi numerici non dipendono da quella funzionalità, è quindi logico averla in una classe separata

Non abbiamo nemmeno affrontato altri argomenti:

  • Dovrebbe essere nominato GetSqrtpoiché restituisce un nuovo valore anziché modificare l'istanza?
  • Che dire Square? Abs? Trunc? Log10? Ln? Power? Factorial? Sin? Cos? ArcTan?

6
Per non parlare delle gioie di 1.sqrt()vs 1.1.sqrt()(ghads, che sembra brutto) hanno una classe base comune? Qual è il contratto per il suo sqrt()metodo?

5
@MichaelT Bel esempio. Mi ci sono volute quattro letture per capire cosa 1.1.Sqrtrappresentasse. Intelligente.
D Stanley,

17
Non sono molto chiaro da questa risposta come la classe statica ti aiuti con il tuo motivo principale. Se hai doppio Sqrt (int) e doppio Sqrt (doppio) nella tua classe Math, hai due opzioni: converti int in doppio, quindi chiama nella doppia versione o copia e incolla il metodo con le opportune modifiche (se presenti ). Ma quelle sono esattamente le stesse opzioni che hai descritto per la versione dell'istanza. Il tuo altro ragionamento (in particolare il tuo terzo punto elenco) sono d'accordo con altro.
Ben Aaronson,

14
-1 questa risposta è assurdo, che cosa qualsiasi di questo ha a che fare con l'essere statica? Avrai intenzione di decidere le risposte a quelle stesse domande in entrambi i casi (e "[con una funzione statica] l'implementazione è la stessa" è falso, o almeno non più vero di quanto sarebbe per esempio i metodi ..)
BlueRaja - Danny Pflughoeft,

20
"L'implementazione è la stessa indipendentemente dal tipo numerico sottostante" è una totale assurdità. Le implementazioni della funzione radice quadrata devono differire in modo significativo in base al tipo con cui stanno lavorando per non essere orribilmente inefficienti.
R ..

25

Le operazioni matematiche sono spesso molto sensibili alle prestazioni. Pertanto, vorremmo utilizzare metodi statici che possono essere completamente risolti (e ottimizzati o integrati) al momento della compilazione. Alcune lingue non offrono alcun meccanismo per specificare i metodi spediti staticamente. Inoltre, il modello a oggetti di molte lingue ha un notevole sovraccarico di memoria inaccettabile per tipi "primitivi" come double.

Alcune lingue ci consentono di definire funzioni che utilizzano la sintassi di invocazione del metodo, ma in realtà vengono inviate staticamente. I metodi di estensione in C # 3.0 o versioni successive sono un esempio. I metodi non virtuali (ad esempio il valore predefinito per i metodi in C ++) sono un altro caso, sebbene C ++ non supporti i metodi sui tipi primitivi. Naturalmente puoi creare la tua classe wrapper in C ++ che decora un tipo primitivo con vari metodi, senza alcun sovraccarico di runtime. Tuttavia, dovrai convertire manualmente i valori in quel tipo di wrapper.

Esistono un paio di lingue che definiscono i metodi sui loro tipi numerici. Questi sono di solito linguaggi altamente dinamici in cui tutto è un oggetto. Qui, le prestazioni sono una considerazione secondaria dell'eleganza concettuale, ma quei linguaggi non sono generalmente usati per lo scricchiolio dei numeri. Tuttavia, queste lingue potrebbero avere un ottimizzatore in grado di "decomprimere" le operazioni sulle primitive.


Con le considerazioni tecniche fuori mano, possiamo considerare se una tale interfaccia matematica basata su metodi sarebbe una buona interfaccia. Sorgono due problemi:

  • la notazione matematica si basa su operatori e funzioni, non su metodi. Un'espressione come 42.sqrtapparirà molto più estranea a molti utenti di sqrt(42). Come utente matematico, preferirei la possibilità di creare i miei operatori sulla sintassi del metodo punto-metodo.
  • il principio di responsabilità unica ci incoraggia a limitare il numero di operazioni che fanno parte di un tipo alle operazioni essenziali. Rispetto alla moltiplicazione, è necessario raramente la radice quadrata. Se la lingua è specificamente progettato per anlysis statistiche, quindi fornendo più primitivi (come le operazioni mean, median, variance, std, normalizesu liste numeriche o la funzione Gamma per i numeri) può essere utile. Per un linguaggio generico, questo appesantisce semplicemente l'interfaccia. Relegare le operazioni non essenziali in uno spazio dei nomi separato rende il tipo più accessibile per la maggior parte degli utenti.

Python è un buon esempio di un linguaggio tutto-è-un-oggetto usato per lo scricchiolio di numeri pesanti. Gli scalari NumPy in realtà hanno dozzine e dozzine di metodi, ma sqrt non è ancora uno di questi. Molti di questi sono cose simili transposee meanche sono lì solo per fornire un'interfaccia uniforme con gli array NumPy, che sono la vera struttura di dati del cavallo di battaglia.
user2357112 supporta Monica il

7
@ user2357112: Il fatto è che NumPy stesso è scritto in una miscela di C e Cython, con della colla Python. Altrimenti non potrebbe mai essere così veloce come è.
Kevin,

1
Penso che questa risposta colpisca abbastanza da vicino una sorta di compromesso del mondo reale che è stato raggiunto nel corso degli anni del design. In altre notizie, ha davvero senso in .Net essere in grado di fare "Hello World" .Max () come le estensioni LINQ ci consentono E rende molto visibile in Intellisense. Punti bonus: qual è il risultato? Bonus Bonus, qual è il risultato in Unicode ...?
Andyz Smith,

15

Sarei motivato dal fatto che ci sono un sacco di funzioni matematiche per scopi speciali, e piuttosto che popolare ogni tipo di matematica con tutte (o un sottoinsieme casuale) di quelle funzioni che le metti in una classe di utilità. Altrimenti, inquineresti la descrizione del completamento automatico o costringerai le persone a guardare sempre in due punti. (È sinabbastanza importante essere un membro di Double, o è in Mathclasse insieme a dei fratelli come htane exp1p?)

Un altro motivo pratico è che si scopre che ci possono essere diversi modi per implementare metodi numerici, con diversi compromessi di prestazioni e precisione. Java ha Math, e ha anche StrictMath.


Spero che i progettisti del linguaggio non si preoccupino delle descrizioni automatiche del completamento. Inoltre, cosa succede Math.<^space>? Anche quella descrizione del completamento automatico verrà inquinata. Inversamente, penso che il tuo secondo paragrafo sia probabilmente una delle risposte migliori qui.
Qix,

@Qix Lo fanno. Tuttavia, altre persone qui potrebbero chiamarlo "rendere gonfiata un'interfaccia".
Aleksandr Dubinsky,

6

Hai correttamente osservato che c'è una curiosa simmetria in gioco qui.

Sia che dico sqrt(n)o n.sqrt()meno, entrambi esprimono la stessa cosa e quale preferisci è più una questione di gusti personali che altro.

Questo è anche il motivo per cui vi è una forte argomentazione di alcuni progettisti di linguaggi per rendere intercambiabili le due sintassi. Il linguaggio di programmazione D lo consente già in una funzione chiamata Sintassi chiamata funzione uniforme . Una funzionalità simile è stata proposta anche per la standardizzazione in C ++ . Come sottolinea Mark Amery nei commenti , Python lo consente anche.

Questo non è senza problemi. L'introduzione di un cambiamento di sintassi fondamentale come questo ha conseguenze ad ampio raggio per il codice esistente ed è ovviamente anche un argomento di discussioni controverse tra gli sviluppatori che sono stati addestrati per decenni a pensare alle due sintassi come a descrivere cose diverse.

Immagino che solo il tempo dirà se l'unificazione dei due è fattibile a lungo termine, ma è sicuramente una considerazione interessante.


Python supporta già entrambe queste sintassi. Ogni metodo non statico prende selfcome primo parametro e quando si chiama il metodo come proprietà di un'istanza, anziché come proprietà della classe, l'istanza viene implicitamente passata come primo argomento. Quindi posso scrivere "foo".startswith("f")o str.startswith("foo", "f"), e posso scrivere my_list.append(x)o list.append(my_list, x).
Mark Amery,

@MarkAmery Ottimo punto. Non è così drastico come fanno le proposte D o C ++, ma si adatta all'idea generale. Grazie per la segnalazione!
ComicSansMS

3

Oltre alla risposta di D Stanley devi pensare al polimorfismo. Metodi come Math.Sqrt dovrebbero sempre restituire lo stesso valore allo stesso input. Rendere statico il metodo è un buon modo per chiarire questo punto, poiché i metodi statici non sono sovrascrivibili.

Hai menzionato il metodo ToString (). Qui potresti voler sovrascrivere questo metodo, quindi la (sotto) classe è rappresentata in un altro modo come String come sua classe genitore. Quindi lo trasformi in un metodo di istanza.


2

Bene, in Java esiste un wrapper per ogni tipo di base.
E i tipi di base non sono tipi di classe e non hanno funzioni membro.

Quindi, hai le seguenti scelte:

  1. Raccogli tutte quelle funzioni di supporto in una classe di tipo pro-forma Math.
  2. Renderlo una funzione statica sul wrapper corrispondente.
  3. Renderlo una funzione membro sul wrapper corrispondente.
  4. Cambia le regole di Java.

Escludiamo l'opzione 4, perché ... Java è Java e gli aderenti professano di amarla in quel modo.

Ora, possiamo anche escludere l'opzione 3 perché mentre l'allocazione degli oggetti è abbastanza economica, non è gratuita e farlo ripetutamente si somma.

Due giù, uno ancora da uccidere: anche l'opzione 2 è una cattiva idea, perché significa che ogni funzione deve essere implementata per ogni tipo, non si può fare affidamento sull'ampliamento della conversione per colmare le lacune o le incoerenze danneggeranno davvero.
E dando un'occhiata java.lang.Math, ci sono molte lacune, specialmente per i tipi più piccoli dei intrispettivi double.

Quindi, alla fine, il chiaro vincitore è l'opzione 1, raccogliendoli tutti in un posto in una classe di utilità-funzione.

Tornando all'opzione 4, qualcosa in quella direzione è accaduto in realtà molto più tardi: puoi chiedere al compilatore di considerare tutti i membri statici di qualsiasi classe che desideri quando risolvi i nomi da molto tempo ormai. import static someclass.*;

A parte questo, altre lingue non hanno questo problema, sia perché non hanno alcun pregiudizio contro le funzioni libere (opzionalmente usando gli spazi dei nomi) o molti meno tipi piccoli.


1
Considera le gioie dell'implementazione delle variazioni Math.min()in tutti i tipi di wrapper.

Trovo il n. 4 poco convincente. Math.sqrt()è stato creato contemporaneamente al resto di Java, quindi quando è stata presa la decisione di inserire sqrt () Mathnon si è verificata alcuna inerzia storica degli utenti Java a cui "piace così". Sebbene non vi siano molti problemi sqrt(), il comportamento eccessivo di Math.round()è atroce. Essere in grado di usare la sintassi del membro con valori di tipo floate doubleavrebbe evitato quel problema.
supercat,

2

Un punto che non vedo esplicitamente menzionato (anche se Amon allude ad esso) è che la radice quadrata può essere pensata come un'operazione "derivata": se l'implementazione non ce la fornisce, possiamo scrivere la nostra.

Poiché la domanda è taggata con il design del linguaggio, potremmo prendere in considerazione una descrizione indipendente dal linguaggio. Sebbene molte lingue abbiano filosofie diverse, è molto comune tra i paradigmi usare l'incapsulamento per preservare gli invarianti; cioè per evitare di avere un valore che non si comporta come suggerisce il suo tipo.

Ad esempio, se abbiamo un'implementazione di numeri interi che usano parole macchina, probabilmente vorremmo incapsulare la rappresentazione in qualche modo (ad esempio per impedire che i bit shift cambino il segno), ma allo stesso tempo abbiamo ancora bisogno di accedere a quei bit per implementare operazioni come Inoltre.

Alcune lingue possono implementarlo con classi e metodi privati:

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

Alcuni con sistemi di moduli:

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

Alcuni con ambito lessicale:

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

E così via. Tuttavia, nessuno di questi meccanismi è necessario per implementare la radice quadrata: può essere implementato utilizzando l' interfaccia pubblica di tipo numerico, e quindi non ha bisogno di accedere ai dettagli dell'implementazione incapsulata.

Quindi la posizione della radice quadrata si riduce alla filosofia / ai gusti della lingua e del progettista della biblioteca. Alcuni potrebbero scegliere di metterlo "dentro" ai valori numerici (ad esempio renderlo un metodo di istanza), alcuni potrebbero scegliere di metterlo allo stesso livello delle operazioni primitive (questo potrebbe significare un metodo di istanza o potrebbe significare vivere al di fuori del valori numerici, ma all'interno dello stesso modulo / classe / spazio dei nomi, ad esempio come una funzione autonoma o un metodo statico), alcuni potrebbero scegliere di inserirlo in una raccolta di funzioni "helper", altri potrebbero scegliere di delegarlo a librerie di terze parti.


Nulla impedirebbe a una lingua di consentire la chiamata di un metodo statico utilizzando la sintassi dei membri (come con i metodi di estensione C # o vb.net) o con una variazione della sequenza dei membri (mi sarebbe piaciuto vedere una sintassi a doppio punto, quindi come consentire ai vantaggi di Intellisense di essere in grado di elencare solo le funzioni che erano adatte per l'argomento principale, ma evitare ambiguità con gli operatori membri reali).
Supercat,

-2

In Java e C # ToString è un metodo di oggetto, la radice della gerarchia di classi, quindi ogni oggetto implementerà il metodo ToString. Per un numero intero è naturale che l'implementazione di ToString funzioni in questo modo.

Quindi il tuo ragionamento è sbagliato. Il motivo per cui i tipi di valore implementano ToString non è che alcune persone fossero come: ehi, abbiamo un metodo ToString per i tipi di valore. È perché ToString è già lì ed è la cosa "più naturale" da produrre.


1
Naturalmente è una decisione, cioè la decisione di objectavere un ToString()metodo. Cioè nelle tue parole "alcune persone erano come: ehi, abbiamo un metodo ToString per i tipi di valore".
Residuum

-3

A differenza di String.substring, Number.sqrt non è in realtà un attributo del numero ma piuttosto un nuovo risultato basato sul tuo numero. Penso che passare il tuo numero a una funzione di quadratura sia più intuitivo.

Inoltre, l'oggetto Math contiene altri membri statici e ha più senso raggrupparli e usarli in modo uniforme.


3
Esempio di contatore: BigInteger.pow () .
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.