Dlaczego Math.Sqrt () jest funkcją statyczną?

31

W dyskusji na temat metod statycznych i metod instancji zawsze myślę, że Sqrt()powinna to być metoda instancji typów liczb zamiast metody statycznej. Dlaczego? Oczywiście działa na wartość.

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

Typy wartości oczywiście mogą mieć metody instancji, ponieważ w wielu językach istnieje metoda instancji ToString().

Aby odpowiedzieć na niektóre pytania z komentarzy: Dlaczego 1.Sqrt()nie powinno być legalne? 1.ToString()jest.

Niektóre języki nie pozwalają na stosowanie metod dla typów wartości, ale niektóre języki mogą. Mówię o nich, w tym Java, ECMAScript, C # i Python (ze __str__(self)zdefiniowanym). To samo odnosi się do innych funkcji, takich jak ceil(), floor()etc.

Pozostałość
źródło
18
W jakim języku to proponujesz? Byłby 1.sqrt()ważny?
20
W wielu językach (np. Java) dublery są prymitywami (ze względu na wydajność), więc nie mają metod
Richard Tingle
45
Więc typy liczbowe powinny być rozdęte każdą możliwą funkcją matematyczną, którą można by do nich zastosować?
D Stanley,
19
FWIW Wydaje mi się, że Sqrt(x)wygląda o wiele bardziej naturalnie niż x.Sqrt() jeśli oznacza to, że w niektórych językach oznacza to dodanie klasy do klasy. Gdyby była to metoda instancji x.GetSqrt(), bardziej odpowiednie byłoby wskazanie, że zwraca wartość, a nie modyfikuje instancję.
D Stanley,
23
To pytanie nie może być agnostyczne wobec języka w jego obecnej formie. To jest korzeń problemu.
rośnie Ciemność

Odpowiedzi:

20

Jest to całkowicie wybór projektu językowego. Zależy to również od podstawowej implementacji typów pierwotnych i związanych z tym względów dotyczących wydajności.

.NET ma tylko jedną statyczną Math.Sqrtmetodę, która działa na a doublei zwraca adouble . Wszystko, co mu przekażesz, musi zostać obsadzone lub awansowane na double.

double sqrt2 = Math.Sqrt(2d);

Z drugiej strony masz Rust, który udostępnia te operacje jako funkcje dla typów :

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

Ale Rust ma również uniwersalną składnię wywołań funkcji (ktoś wspomniał o tym wcześniej), więc możesz wybrać, co chcesz.

let sqrt2 = f32::sqrt(2.0f32);
let higher = f32::max(2.0f32, 3.0f32);
angelsl
źródło
1
Warto zauważyć, że w .NET możesz pisać metody rozszerzeń, więc jeśli naprawdę chcesz, aby ta implementacja wyglądała x.Sqrt(), możesz to zrobić. public static class DoubleExtensions { public static double Sqrt( this double self) { return Math.Sqrt(self); } }
Zachary Dow
1
Również w C # 6 może to być po prostu Sqrt(x) msdn.microsoft.com/en-us/library/sf0df423.aspx .
Den
65

Załóżmy, że projektujemy nowy język i chcemy Sqrtbyć metodą instancji. Patrzymy na doubleklasę i zaczynamy projektować. Oczywiście nie ma danych wejściowych (innych niż instancja) i zwraca a double. Piszemy i testujemy kod. Doskonałość.

Ale przyjmowanie pierwiastka kwadratowego z liczby całkowitej jest również poprawne i nie chcemy zmusić wszystkich do konwersji na podwójną wartość tylko do pierwiastka kwadratowego. Więc przechodzimy do intprojektowania i zaczynamy projektować. Co to zwraca? My mogli zwracać inti sprawiają, że działa tylko na idealnych kwadratów lub okrągły wynik z dokładnością do int(pomijając debaty na temat właściwego sposobu zaokrąglania do tej pory). Ale co, jeśli ktoś chce wyniku niecałkowitego? Powinniśmy mieć dwie metody - jedną, która zwraca an, inti jedną, która zwraca a double(co nie jest możliwe w niektórych językach bez zmiany nazwy). Postanawiamy więc, że powinien zwrócić double. Teraz wdrażamy. Ale implementacja jest identyczna z tą, której używaliśmydouble. Czy kopiujemy i wklejamy? Czy przekazujemy instancję do doublei wywołujemy metodę tej instancji? Dlaczego nie zastosować logiki w metodzie bibliotecznej, do której można uzyskać dostęp z obu klas. Wywołamy bibliotekę Mathi funkcję Math.Sqrt.

Dlaczego Math.Sqrtfunkcja statyczna ?:

  • Ponieważ implementacja jest taka sama bez względu na podstawowy typ liczbowy
  • Ponieważ nie wpływa na określoną instancję (przyjmuje jedną wartość i zwraca wynik)
  • Ponieważ typy numeryczne nie zależą od tej funkcjonalności, dlatego sensowne jest posiadanie jej w osobnej klasie

Nie zajęliśmy się nawet innymi argumentami:

  • Czy należy go nazwać, GetSqrtponieważ zwraca nową wartość zamiast modyfikować instancję?
  • Co Square? Abs? Trunc? Log10? Ln? Power? Factorial? Sin? Cos? ArcTan?
D Stanley
źródło
6
Nie wspominając już o radościach 1.sqrt()vs 1.1.sqrt()(ghady, które wyglądają brzydko) czy mają wspólną klasę podstawową? Jaki jest kontrakt na jego sqrt()metodę?
5
@MichaelT Ładny przykład. Zrozumienie tego, co 1.1.Sqrtreprezentuje, zajęło mi cztery czytania . Sprytny.
D Stanley,
17
Nie jestem do końca jasny z tej odpowiedzi, w jaki sposób klasa statyczna pomaga w głównym celu. Jeśli masz podwójny Sqrt (int) i podwójny Sqrt (double) w swojej klasie Math, masz dwie opcje: zamień int na double, następnie wywołaj do podwójnej wersji lub skopiuj i wklej metodę z odpowiednimi zmianami (jeśli występują) ). Ale są to dokładnie te same opcje, które opisałeś dla wersji instancji. Twoje inne rozumowanie (szczególnie twój trzeci punkt) zgadzam się z więcej.
Ben Aaronson
14
-1 odpowiedź ta jest absurdalna, co nie każdy to ma wspólnego z byciem statyczny? Będziesz decydował o odpowiedziach na te same pytania tak czy inaczej (a „[z funkcją statyczną] implementacja jest taka sama” jest fałszywe lub przynajmniej nie bardziej prawdziwe niż na przykład metody…)
BlueRaja - Danny Pflughoeft
20
„Implementacja jest taka sama bez względu na podstawowy typ liczbowy” to kompletny nonsens. Implementacje funkcji pierwiastka kwadratowego muszą się znacznie różnić w zależności od typu, z którym pracują, aby nie być okropnie nieefektywnym.
R ..
25

Operacje matematyczne są często bardzo wrażliwe na wyniki. Dlatego będziemy chcieli używać metod statycznych, które można w pełni rozwiązać (i zoptymalizować lub wstawić) w czasie kompilacji. Niektóre języki nie oferują żadnego mechanizmu określającego metody wysyłane statycznie. Co więcej, model obiektowy wielu języków ma znaczny narzut pamięci, który jest nie do przyjęcia dla typów „prymitywnych”, takich jak double.

Kilka języków pozwala nam definiować funkcje, które używają składni wywołania metody, ale są faktycznie wysyłane statycznie. Przykładem są metody rozszerzenia w C # 3.0 lub nowszym. Metody inne niż wirtualne (np. Domyślne dla metod w C ++) to inny przypadek, chociaż C ++ nie obsługuje metod na typach pierwotnych. Możesz oczywiście stworzyć własną klasę opakowań w C ++, która zdobi prymitywny typ różnymi metodami, bez żadnych narzutów w czasie wykonywania. Będziesz jednak musiał ręcznie przekonwertować wartości na ten typ opakowania.

Istnieje kilka języków, które definiują metody dla swoich typów numerycznych. Są to zwykle bardzo dynamiczne języki, w których wszystko jest przedmiotem. Wydajność jest tutaj drugim aspektem elegancji pojęciowej, ale języki te nie są zwykle używane do kruszenia liczb. Jednak te języki mogą mieć optymalizator, który może „rozpakować” operacje na operacjach podstawowych.


Nieuwzględniając względów technicznych, możemy zastanowić się, czy taki oparty na metodach interfejs matematyczny byłby dobrym interfejsem. Powstają dwa problemy:

  • notacja matematyczna oparta jest na operatorach i funkcjach, a nie na metodach. Wyrażenie takie jak 42.sqrtdla wielu użytkowników będzie znacznie bardziej obce niż sqrt(42). Jako użytkownik wymagający matematyki wolałbym możliwość tworzenia własnych operatorów niż składni wywołania metody kropki.
  • zasada jednolitej odpowiedzialności zachęca nas do ograniczenia liczby operacji wchodzących w skład danego rodzaju do operacji podstawowych. W porównaniu z mnożeniem pierwiastek kwadratowy jest niezwykle rzadki. Jeśli Twój język jest przeznaczony specjalnie dla anlysis statystycznej, następnie zapewniając więcej prymitywów (takich jak operacje mean, median, variance, std, normalizena listach numerycznych lub funkcji gamma numerów) może być przydatna. W przypadku języka ogólnego, to tylko obciąża interfejs. Przeniesienie niepotrzebnych operacji do oddzielnej przestrzeni nazw sprawia, że ​​ten typ jest bardziej dostępny dla większości użytkowników.
amon
źródło
Python jest dobrym przykładem języka typu wszystko-to-obiekt, używanego do kruszenia dużych liczb. Skalary NumPy faktycznie mają dziesiątki metod, ale sqrt wciąż nie jest jedną z nich. Większość z nich jest podobna transposei meanma na celu zapewnienie jednolitego interfejsu z tablicami NumPy, które są prawdziwą strukturą danych konia roboczego.
użytkownik2357112 obsługuje Monikę
7
@ user2357112: Chodzi o to, że sama NumPy jest napisana w mieszaninie C i Cython, z pewnym klejem Python. W przeciwnym razie nigdy nie byłoby tak szybko, jak jest.
Kevin,
1
Myślę, że ta odpowiedź dość ściśle pasuje do pewnego rodzaju kompromisu w świecie rzeczywistym, który został osiągnięty przez lata projektowania. Z innych wiadomości, czy naprawdę ma sens w .Net być w stanie „Hello World” .Max (), ponieważ rozszerzenia LINQ na to pozwalają, ORAZ sprawia, że ​​jest bardzo widoczny w Intellisense. Punkty bonusowe: Jaki jest wynik? Bonus Bonus, jaki jest wynik w Unicode ...?
Andyz Smith
15

Byłbym motywowany faktem, że istnieje mnóstwo funkcji matematycznych specjalnego przeznaczenia, i zamiast zapełniać każdy typ matematyki wszystkimi (lub losowym podzbiorem) tych funkcji, które umieściłeś w klasie użytkowej. W przeciwnym razie zanieczyścisz etykietkę automatycznego uzupełniania lub zmusisz ludzi, by zawsze patrzyli w dwa miejsca. (Czy jest sinwystarczająco ważny, aby być członkiem Doublelub jest w Mathklasie wraz z wsobnymi, takimi jak htani exp1p?)

Innym praktycznym powodem jest fakt, że mogą istnieć różne sposoby wdrażania metod numerycznych, z różnymi kompromisami w zakresie wydajności i precyzji. Java ma Mathi ma również StrictMath.

Aleksandr Dubinsky
źródło
Mam nadzieję, że projektanci języków nie dbają o automatyczne uzupełnianie podpowiedzi. A także, co się dzieje Math.<^space>? Ta etykieta z autouzupełnianiem również zostanie zanieczyszczona. Odwrotnie, myślę, że twój drugi akapit jest prawdopodobnie jedną z lepszych odpowiedzi tutaj.
Qix
@Qix Oni robią. Chociaż inni ludzie mogą to nazwać „rozdętym interfejsem”.
Aleksandr Dubinsky
6

Prawidłowo zauważyłeś, że gra tutaj ciekawa symetria.

Niezależnie od tego, czy powiem, sqrt(n)czy n.sqrt()nie ma to tak naprawdę znaczenia, oba wyrażają to samo, a który preferujesz, jest bardziej kwestią osobistego gustu niż czegokolwiek innego.

Dlatego też niektórzy projektanci języków mają mocny argument za zamianą tych dwóch składni. Język programowania D już na to pozwala w ramach funkcji o nazwie Jednolita składnia wywołania funkcji . Podobna funkcja została również zaproponowana do standaryzacji w C ++ . Jak zaznacza Mark Ameryk w komentarzach , Python również na to pozwala.

Nie obyło się bez problemów. Wprowadzenie fundamentalnej zmiany składni, takiej jak ta, ma daleko idące konsekwencje dla istniejącego kodu i jest oczywiście tematem kontrowersyjnych dyskusji między programistami, którzy przez dziesięciolecia byli szkoleni, aby myśleć o tych dwóch składniach jako o różnych opisach.

Wydaje mi się, że tylko czas pokaże, czy unifikacja obu jest możliwa na dłuższą metę, ale jest to z pewnością interesująca uwaga.

ComicSansMS
źródło
Python obsługuje już obie te składnie. Każda metoda niestatyczna przyjmuje selfjako swój pierwszy parametr, a gdy wywołujesz metodę jako właściwość instancji, a nie jako właściwość klasy, instancja zostaje domyślnie przekazana jako pierwszy argument. Dlatego mogę pisać "foo".startswith("f")lub str.startswith("foo", "f"), i mogę pisać my_list.append(x)lub list.append(my_list, x).
Mark Amery
@MarkAmery Dobry punkt. Nie jest to tak drastyczne jak to, co robią propozycje D lub C ++, ale pasuje do ogólnej idei. Dzięki za wskazanie!
ComicSansMS
3

Oprócz odpowiedzi D. Stanleya musisz pomyśleć o polimorfizmie. Metody takie jak Math.Sqrt powinny zawsze zwracać tę samą wartość do tego samego wejścia. Uczynienie tej metody statyczną jest dobrym sposobem na wyjaśnienie tej kwestii, ponieważ metod statycznych nie można zastąpić.

Wspomniałeś o metodzie ToString (). Tutaj możesz zastąpić tę metodę, aby (pod) klasa była reprezentowana w inny sposób jako String jako jej klasa nadrzędna. Czynisz to instancją Metodą.

falx
źródło
2

Cóż, w Javie istnieje opakowanie dla każdego podstawowego typu.
Podstawowe typy nie są typami klas i nie mają żadnych funkcji składowych.

Masz więc następujące możliwości:

  1. Zbierz wszystkie te funkcje pomocnicze do klasy pro-forma Math.
  2. Ustaw funkcję statyczną na odpowiednim opakowaniu.
  3. Uczyń z niego funkcję członka na odpowiednim opakowaniu.
  4. Zmień reguły Java.

Wykluczmy opcję 4, ponieważ ... Java jest Javą, a zwolennicy twierdzą, że tak ją lubią.

Teraz możemy także wykluczyć opcję 3 ponieważ podczas przydzielania obiektów jest dość tanie, to nie jest za darmo, i robi to w kółko robi dodać.

Dwa w dół, jeden wciąż do zabicia: Opcja 2 to także zły pomysł, ponieważ oznacza to, że każda funkcja musi być zaimplementowana dla każdego typu, nie można polegać na poszerzeniu konwersji w celu wypełnienia luk, w przeciwnym razie niekonsekwencje naprawdę będą bolały.
I patrząc na to java.lang.Math, istnieje wiele luk, szczególnie w przypadku typów mniejszych niż intodpowiednie double.

Ostatecznie wyraźnym zwycięzcą jest opcja pierwsza, gromadząc je wszystkie w jednym miejscu w klasie funkcji-funkcji.

Wracając do opcji 4, coś w tym kierunku wydarzyło się znacznie później: możesz poprosić kompilator o rozważenie wszystkich statycznych elementów dowolnej klasy, kiedy rozwiązujesz nazwy od dłuższego czasu. import static someclass.*;

Nawiasem mówiąc, inne języki nie mają tego problemu, ponieważ nie mają żadnego uszczerbku dla wolnych funkcji (opcjonalnie korzystających z przestrzeni nazw) lub znacznie mniej małych typów.

Deduplikator
źródło
1
Rozważ radość z wprowadzania wariantów Math.min()we wszystkich typach opakowań.
Uważam # 4 za nieprzekonujący. Math.sqrt()został utworzony w tym samym czasie co reszta Java, więc kiedy podjęto decyzję o wprowadzeniu sqrt (), Mathnie było historycznej bezwładności użytkowników Java, którzy „lubią to w ten sposób”. Chociaż nie ma z tym większego problemu sqrt(), zachowanie przeciążeniowe Math.round()jest okropne. Możliwość użycia składni składowej z wartościami typu floati doublepozwoliłaby uniknąć tego problemu.
supercat
2

Jedną kwestią, o której nie widzę wyraźnie wspomnianego (chociaż nawiązuje do niego amon), jest to, że pierwiastek kwadratowy można traktować jako operację „pochodną”: jeśli implementacja tego nie zapewnia, możemy napisać własną.

Ponieważ pytanie jest oznaczone wzorem językowym, możemy rozważyć opis zależny od języka. Chociaż wiele języków ma różne filozofie, w paradygmatach bardzo często stosuje się enkapsulację, aby zachować niezmienniki; tzn. aby uniknąć posiadania wartości, która nie zachowuje się tak, jak sugerowałby jej typ.

Na przykład, jeśli mamy jakąś implementację liczb całkowitych używających słów maszynowych, prawdopodobnie chcemy jakoś zakapsułować reprezentację (np. Aby zapobiec zmianie bitów przez zmianę znaku), ale jednocześnie nadal potrzebujemy dostępu do tych bitów, aby zaimplementować operacje takie jak dodanie.

Niektóre języki mogą to zaimplementować za pomocą klas i metod prywatnych:

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

Niektóre z systemami modułowymi:

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

Niektóre z zakresem leksykalnym:

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

I tak dalej. Jednak żaden z tych mechanizmów nie jest potrzebny do implementacji pierwiastka kwadratowego: można go zaimplementować za pomocą publicznego interfejsu typu liczbowego, a zatem nie potrzebuje dostępu do kapsułkowanych szczegółów implementacji.

Dlatego lokalizacja pierwiastka kwadratowego sprowadza się do filozofii / smaków języka i projektanta biblioteki. Niektórzy mogą wybrać, aby umieścić go „wewnątrz” wartości liczbowych (np sprawiają, że metody instancji), niektóre mogą wybrać, aby umieścić go na tym samym poziomie, co prymitywne operacji (może to oznaczać metodę instancji, czy może to oznaczać, żyjących poza wartości liczbowe, ale wewnątrz tego samego modułu / klasy / przestrzeni nazw, np. jako samodzielna funkcja lub metoda statyczna), niektórzy mogą zdecydować o umieszczeniu go w kolekcji funkcji „pomocniczych”, niektórzy mogą zdecydować o przekazaniu go do bibliotek stron trzecich.

Warbo
źródło
Nic nie stoi na przeszkodzie, aby język zezwalał na wywoływanie metody statycznej przy użyciu składni składowej (jak w przypadku metod rozszerzenia C # lub vb.net) lub przy zmianie sekwencji składowej (chciałbym widzieć składnię z podwójnymi kropkami, więc aby umożliwić Intellisense przewagę polegającą na tym, że można wymienić tylko funkcje, które były odpowiednie dla argumentu podstawowego, ale uniknąć dwuznaczności z prawdziwymi operatorami składowymi).
supercat
-2

W Javie i C # ToString jest metodą obiektową, rdzeniem hierarchii klas, więc każdy obiekt wdroży metodę ToString. Dla Integertype jest naturalne, że implementacja ToString będzie działać w ten sposób.

Więc twoje rozumowanie jest błędne. Powodem, dla którego typy wartości implementują ToString, nie jest to, że niektórzy ludzie byli tacy: hej, zastosujmy metodę ToString dla typów wartości. To dlatego, że ToString już tam jest i jest to „najbardziej naturalna” rzecz do wydrukowania.

Pieter B.
źródło
1
Oczywiście jest to decyzja, tj. Decyzja o objectzastosowaniu ToString()metody. To jest w twoich słowach „niektórzy ludzie byli tacy: hej, zastosujmy metodę ToString dla typów wartości”.
Residuum
-3

W przeciwieństwie do String.substring, Number.sqrt nie jest tak naprawdę atrybutem liczby, ale raczej nowym wynikiem opartym na twoim numerze. Myślę, że przekazanie numeru do funkcji kwadratu jest bardziej intuicyjne.

Co więcej, obiekt Math zawiera inne elementy statyczne i bardziej sensowne jest zebranie ich razem i użycie w jednolity sposób.

HaLeiVi
źródło
3
Przykład licznika: BigInteger.pow () .