Dlaczego twórca Ruby wybrał koncepcję Symboli?

15

tl; dr: Czy istniałaby zależna od języka definicja symboli i powód, aby mieć je w innych językach?

Dlaczego więc twórca języka Ruby zastosował tę koncepcję symbolsw języku?

Pytam o to z perspektywy programisty nie-rubinowego. Nauczyłem się wielu innych języków i w żadnym z nich nie znalazłem potrzeby określania, czy mam do czynienia z tym, co nazywa Ruby symbols.

Główne pytanie brzmi: czy symbolsw Ruby istnieje pojęcie wydajności, czy tylko coś, co jest potrzebne ze względu na sposób pisania języka?

Czy program w Rubim byłby lżejszy i / lub szybszy niż jego, powiedzmy, odpowiednik w Pythonie lub JavaScript? Jeśli tak, czy to z tego powodu symbols?

Skoro jednym z zamierzeń Ruby jest łatwość czytania i pisania dla ludzi, czy twórcy nie mogliby ułatwić procesu kodowania poprzez wdrożenie tych ulepszeń w samym tłumaczu (tak jak w innych językach)?

Wygląda na to, że każdy chce wiedzieć tylko, co to symbolsjest i jak z nich korzystać, a nie dlaczego.

Jurij Ghensev
źródło
Scala ma symbole na czubku głowy. Myślę, że robi to wielu Lispsów.
D. Ben Knoble,

Odpowiedzi:

17

Twórca Ruby, Yukihiro „Matz” Matsumoto, opublikował wyjaśnienie, w jaki sposób na Ruby wpłynęły Lisp, Smalltalk, Perl (a Wikipedia mówi także Ada i Eiffel):

Ruby to język zaprojektowany w następujących krokach:

  • weź prosty język lisp (taki jak przed CL).
  • usuń makra, s-expression.
  • dodaj prosty system obiektowy (znacznie prostszy niż CLOS).
  • dodawaj bloki, inspirowane funkcjami wyższego rzędu.
  • dodaj metody znalezione w Smalltalk.
  • dodaj funkcjonalność znalezioną w Perlu (w sposób OO).

Teoretycznie Ruby była teoretycznie Lispem.

Nazwijmy to odtąd MatzLisp. ;-)

W dowolnym kompilatorze będziesz zarządzać identyfikatorami funkcji, zmiennych, nazwanych bloków, typów i tak dalej. Zazwyczaj przechowuje się je w kompilatorze, a zapomina się o nich w utworzonym pliku wykonywalnym, z wyjątkiem przypadków dodania informacji o debugowaniu.

W Lisp takie symbole są pierwszorzędnymi zasobami hostowanymi w różnych pakietach, co oznacza, że ​​możesz dodawać nowe symbole w czasie wykonywania, wiązać je z różnego rodzaju obiektami. Jest to przydatne podczas metaprogramowania, ponieważ możesz być pewien, że nie będziesz mieć kolizji nazw z innymi częściami kodu.

Ponadto symbole są internalizowane w czasie odczytu i można je porównać według tożsamości, co jest skutecznym sposobem na uzyskanie nowego rodzaju wartości (takich jak liczby, ale abstrakcyjne). Pomaga to w pisaniu kodu, w którym bezpośrednio używasz wartości symbolicznych, zamiast definiować własne typy wyliczeń poparte liczbami całkowitymi. Ponadto każdy symbol może przechowywać dodatkowe dane. W ten sposób na przykład Emacs / Slime może dołączać metadane z Emacsa bezpośrednio do listy właściwości symbolu.

Pojęcie symbolu jest kluczowe w Lisp. Spójrz na przykład na PAIP (Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp, Norvig), aby uzyskać szczegółowe przykłady.

rdzeń rdzeniowy
źródło
5
Dobra odpowiedź. Jednak nie zgadzam się z Matzem: nigdy nie pomyślałbym, żeby nazwać język bez makr dialektem seplenień. Funkcje metaprogramowania środowiska wykonawczego w lisp są właśnie tym, co nadaje temu językowi jego niesamowitą moc, nadrabiając jego niesamowicie uproszczoną, niewyrażającą gramatykę.
cmaster
11

Dlaczego więc twórcy Ruby musieli zastosować tę koncepcję symbolsw języku?

Cóż, nie ściśle „musieli”, postanowili. Zauważ też, że ściśle mówiąc, Symbolnie są częścią języka, są częścią podstawowej biblioteki. Oni nie mają składnię języka dosłownego poziomu, ale będą działać tak samo dobrze, jeśli trzeba było skonstruować je nazywając Symbol::new.

Pytam z perspektywy nie-rubinowego programisty próbującego to zrozumieć. Nauczyłem się wielu innych języków i w żadnym z nich nie znalazłem potrzeby określania, czy mam do czynienia z tym, co nazywa Ruby symbols.

Nie powiedziałeś, czym są te „wiele innych języków”, ale oto mały fragment języków, które mają Symboltyp danych taki jak Ruby:

Istnieją również inne języki, które zapewniają funkcje Symbols w innej formie. Na przykład w Javie funkcje Ruby Stringsą podzielone na dwa (właściwie trzy) typy: Stringi StringBuilder/ StringBuffer. Z drugiej strony, funkcje Ruby Symboltypu są składane w języku Java StringTyp: Java Strings może być internowany , ciągami tekstowymi i Stringy, które są wynikiem kompilacji oceniano wyrażenia stałe są automatycznie internowany, dynamicznie generowane Strings może być internowany przez wywołanie String.internmetodą. Internowany Stringw Javie jest dokładnie jak Symbolw Ruby, ale nie jest zaimplementowany jako osobny typ, to po prostu inny stan niż JavaStringmoże być w. (Uwaga: we wcześniejszych wersjach Ruby, String#to_symkiedyś był wywoływany, String#interna ta metoda nadal istnieje jako starszy alias).

Główne pytanie może brzmieć: czy koncepcja języka symbolsRuby istnieje jako zamiar wydajności w stosunku do siebie i innych języków,

Symbols są przede wszystkim typem danych o określonej semantyce . Ta semantyka umożliwia także implementację niektórych wydajnych operacji (np. Szybkie testowanie równości O (1)), ale to nie jest główny cel.

czy po prostu coś, co musi istnieć ze względu na sposób pisania języka?

Symbolw języku Ruby wcale nie są potrzebne, bez nich Ruby działałaby dobrze. Są to wyłącznie funkcje biblioteczne. Jest dokładnie jedno miejsce w języku powiązanym z Symbols: defwyrażenie definicji metody zwraca wartość Symboloznaczającą nazwę definiowanej metody. Jest to jednak dość niedawna zmiana, wcześniej wartość zwrotna była po prostu nieokreślona. MRI po prostu ocenił na nil, Rubinius ocenił na Rubinius::CompiledMethodobiekt i tak dalej. Możliwe byłoby również dokonanie oceny do UnboundMethod… lub po prostu String.

Czy program w Rubim byłby lżejszy i / lub szybszy niż jego, powiedzmy, odpowiednik Python lub Node? Jeśli tak, czy to z tego powodu symbols?

Nie jestem pewien, o co tu pytasz. Wydajność zależy głównie od jakości wdrożenia, a nie od języka. Ponadto Node nie jest nawet językiem, to zdeklarowana platforma we / wy dla ECMAScript. Uruchomienie równoważnego skryptu na IronPython i MRI, IronPython prawdopodobnie będzie szybszy. Uruchomienie równoważnego skryptu na CPython i JRuby + Trufla, JRuby + Trufla prawdopodobnie będzie szybsze. Nie ma to nic wspólnego z Symbols, ale z jakością implementacji: JRuby + Truffle ma agresywnie optymalizujący kompilator, a także całą maszynę optymalizacyjną o wysokiej wydajności JVM, CPython to prosty interpreter.

Skoro jednym z zamierzeń Ruby jest łatwość czytania i pisania dla ludzi, czy jej twórcy nie mogą ułatwić procesu kodowania poprzez wdrożenie tych ulepszeń w samym tłumaczu (tak jak w innych językach)?

Nie. Nie Symbolsą optymalizacjami kompilatora. Są osobnym typem danych o specyficznej semantyce. Nie są jak flonum YARV , które są prywatną wewnętrzną optymalizacją dla Floats. Sytuacja nie jest taka sama, jak w przypadku Integer, Bignumi Fixnum, który powinien być niewidoczny prywatny wewnętrzny szczegół optymalizacja, ale niestety nie jest. (Jest to ostatecznie będzie ustalona w Ruby 2.4, który usuwa Fixnumi Bignumliści i tylko Integer).

Robiąc to tak, jak robi to Java, ponieważ specjalny stan normalnych Strings oznacza, że ​​zawsze musisz uważać na to, czy twoje Stringsą w tym szczególnym stanie, i w jakich okolicznościach są one automatycznie w tym szczególnym stanie, a kiedy nie. To znacznie większe obciążenie niż po prostu posiadanie osobnego typu danych.

Czy istniałaby zależna od języka definicja symboli i powód, aby mieć je w innych językach?

Symbolto typ danych, który oznacza pojęcie nazwy lub etykiety . Symbols są obiektami wartości , niezmiennymi, zwykle natychmiastowymi (jeśli język je odróżnia), bezpaństwowcami i nie mają tożsamości. Dwie Symbolrówne są również zagwarantowane, że są identyczne, innymi słowy, dwie Symbolrówne są w rzeczywistości takie same Symbol. Oznacza to, że równość wartości i równość odniesienia są tym samym, a zatem równość jest efektywna i O (1).

Powody, dla których mają je w języku, są naprawdę takie same, niezależnie od języka. Niektóre języki polegają na nich bardziej niż inne.

Na przykład w rodzinie Lisp nie ma pojęcia „zmiennej”. Zamiast tego jesteś Symbolpowiązany z wartościami.

W językach z możliwościami odblaskowych lub introspekcji, Symbola często są wykorzystywane do określenia nazwy odbitych podmiotów API refleksji, na przykład w Ruby Object#methods, Object#singleton_methods, Object#public_methods, Object#protected_methods, i Object#public_methodszwracać Arrayz Symbols (choć równie dobrze może zwracać Arrayod Methods). Object#public_sendprzyjmuje Symbolnazwę argumentu do wysłania jako argument (chociaż akceptuje również Stringrównież, Symboljest bardziej semantycznie poprawny).

W ECMAScript Symbols to podstawowy element składowy zapewniający bezpieczeństwo ECMAScript w przyszłości. Odgrywają także dużą rolę w refleksji.

Jörg W Mittag
źródło
Atomy Erlanga zostały zabrane bezpośrednio z Prologu (Robert Virding powiedział mi, że w pewnym momencie)
Zachary K
2

Symbole są przydatne w Rubim, a zobaczysz je w całym kodzie Ruby, ponieważ każdy symbol jest ponownie używany za każdym razem, gdy się do niego odwołuje. Jest to poprawa wydajności w porównaniu z łańcuchami, ponieważ każde użycie łańcucha, który nie jest zapisany w zmiennej, tworzy nowy obiekt w pamięci. Na przykład, jeśli użyję tego samego ciągu wiele razy jako klucz skrótu:

my_hash = {"a" => 1, "b" => 2, "c" => 3}
100_000.times { |i| puts my_hash["a"] }

Ciąg „a” jest tworzony 101 000 razy w pamięci. Jeśli zamiast tego użyłem symbolu:

my_hash = {a: 1, b: 2, c: 3}
100_000.times { |i| puts my_hash[:a] }

Symbol :ajest wciąż jednym obiektem w pamięci. To sprawia, że ​​symbole są znacznie bardziej wydajne niż łańcuchy.

AKTUALIZACJA Oto punkt odniesienia (wzięty z Codecademy ), który pokazuje różnicę wydajności:

require 'benchmark'

string_AZ = Hash[("a".."z").to_a.zip((1..26).to_a)]
symbol_AZ = Hash[(:a..:z).to_a.zip((1..26).to_a)]

string_time = Benchmark.realtime do
  100_000.times { string_AZ["r"] }
end

symbol_time = Benchmark.realtime do
  100_000.times { symbol_AZ[:r] }
end

puts "String time: #{string_time} seconds."
puts "Symbol time: #{symbol_time} seconds."

Oto moje wyniki dla mojego MBP:

String time: 0.1254125550040044 seconds.
Symbol time: 0.07360960397636518 seconds.

Istnieje wyraźna różnica w używaniu ciągów vs. symboli do identyfikowania kluczy w skrócie.

Keith Mattix
źródło
Nie jestem pewien, czy tak jest. Spodziewałbym się, że implementacja Ruby wykona ten sam kod wiele razy, nie analizując go wielokrotnie dla każdej iteracji. Nawet jeśli każde wystąpienie leksykalne "a"jest rzeczywiście świeżym ciągiem znaków, myślę, że w twoim przykładzie będą dokładnie dwa "a"(i implementacja może nawet dzielić pamięć, dopóki jedno z nich nie zostanie zmutowane). Aby utworzyć miliony ciągów, prawdopodobnie będziesz musiał użyć String.new („a”). Ale nie znam się dobrze na Ruby, więc może się mylę.
coredump,
1
Podczas jednej z lekcji Codecademy generują one wzorzec dla ciągów znaków i symboli, podobnie jak mój przykład. Dodam to do odpowiedzi.
Keith Mattix,
1
Dziękujemy za dodanie testu porównawczego. Twój test pokazuje oczekiwany zysk uzyskany dzięki użyciu symboli zamiast ciągów, dzięki szybszemu testowi w tablicy mieszającej (porównanie identyczności z ciągiem), ale nie ma możliwości, aby wywnioskować, że ciągi są przydzielane przy każdej iteracji. Dodałem wersję z string_AZ[String.new("r")], aby zobaczyć, czy to robi różnicę. Dostaję 21 ms dla napisów (wersja oryginalna), 7 ms z symbolami i 50 ms ze świeżymi napisami za każdym razem. Powiedziałbym więc, że ciągi nie są przydzielane tak często w "r"wersji dosłownej .
coredump
1
Ach, więc trochę kopałem, a w Ruby 2.1 ciągi są w rzeczywistości współużytkowane. Najwyraźniej tęskniłem za tą aktualizacją; dzięki za zwrócenie na to uwagi. Wracając do pierwotnego pytania, myślę, że oba testy porównawcze pokazują użyteczność symboli vs. ciągów znaków.
Keith Mattix,