Ruby 2.1: refinements, wymagane argumenty nazwane i mądrzejszy GC
Ruby 2.1.0 ukazało się w Boże Narodzenie, jak nakazuje tradycja. Nagłówek to generacyjny garbage collector, ale to zmiany w języku — wymagane argumenty nazwane, refinements, Exception#cause — odmienią sposób, w jaki piszemy kod.
Ruby 2.1.0 wylądowało w Boże Narodzenie, podtrzymując uroczy zwyczaj projektu, by dostarczać wydanie pod choinkę. To wyważona aktualizacja, nie rewolucja — ale takie ujęcie jej nie docenia. Dwie funkcje języka zmieniają codzienny kod, jedna zmiana w runtime po cichu czyni większość aplikacji Rails szybszymi za nic, a garść mniejszych dodatków usuwa drobne uciążliwości, z którymi żyłeś latami. Oto czego naprawdę użyjemy i dlaczego.
Generacyjny garbage collector (RGenGC)
Nagłówkiem dla produkcji jest nowy generacyjny GC. Wykorzystuje on starą i gruntownie sprawdzoną obserwację: większość obiektów umiera młodo. Żądanie webowe alokuje tysiące krótko żyjących obiektów — łańcuchy, hashe, pośrednie tablice — które niemal natychmiast stają się śmieciem, podczas gdy znacznie mniejszy zbiór (załadowane klasy, długowieczne cache, pula połączeń) przeżywa całe życie procesu.
Przed 2.1 kolektor mark-and-sweep w Ruby przechodził każdy żywy obiekt przy każdym zbieraniu, płacąc raz za razem za ponowne badanie długowiecznych obiektów, których i tak nigdy nie zwolniono. RGenGC dzieli stertę na pokolenie młode i stare oraz uruchamia tanie mniejsze GC, które skanują tylko młode obiekty, awansując ocalałych do starego pokolenia i potem w większości zostawiając ich w spokoju. Kosztowne duże GC, które skanują wszystko, wciąż się zdarzają, tyle że znacznie rzadziej.
Naprawdę imponującą inżynierią jest to, że jest to wstecznie zgodne na poziomie rozszerzeń C. Kolektor generacyjny normalnie potrzebuje „write barrier” — powiadomienia, ilekroć stary obiekt zaczyna wskazywać na młody — a istniejące rozszerzenia C nic o barierach nie wiedzą. Rozwiązanie zespołu traktuje obiekty bez bariery jako „shady” i obchodzi się z nimi zachowawczo, więc rozszerzenia, które się nie zgłoszą, pozostają poprawne (tracą tylko część przyspieszenia). Efekt: aktualizujesz się, a typowe żądanie Rails — będące strumieniem alokacji młodych, skazanych obiektów — widzi realną poprawę opóźnień bez żadnych zmian w kodzie.
Jeśli chcesz zobaczyć to w działaniu, GC.stat ujawnia liczniki (liczby mniejszych
vs większych GC, sloty sterty, liczbę starych obiektów), a zmienne środowiskowe
RUBY_GC_HEAP_* pozwalają dostroić początkowy rozmiar i wzrost sterty dla
długodziałającego serwera. Większość zespołów nie będzie musiała ich dotykać —
domyślne zachowanie jest wygraną.
Wymagane argumenty nazwane
Ruby 2.0 wprowadziło argumenty nazwane, ale każdy potrzebował wartości domyślnej —
nie było sposobu, by zadeklarować obowiązkowy. 2.1 domyka lukę: argument nazwany
bez wartości domyślnej jest teraz wymagany, a jego pominięcie podnosi ArgumentError.
# Ruby 2.0 — default był kłamstwem; nil nie był tak naprawdę poprawną kwotą
def transfer(from:, to:, amount: nil)
raise ArgumentError, "amount required" if amount.nil?
# ...
end
# Ruby 2.1 — mów dokładnie to, co masz na myśli
def transfer(from:, to:, amount:)
# wszystkie trzy są obowiązkowe ORAZ nazwane w miejscu wywołania
end
transfer(from: alice, to: bob, amount: 50)
transfer(from: alice, to: bob) # => ArgumentError: missing keyword: amount
To większa sprawa, niż z początku wygląda. Argumenty pozycyjne to podatek od
czytelności w miejscu wywołania: charge(account, 50, true, false) nic ci nie mówi
o tym, co znaczą true i false, i nie da się ich przestawić ani pominąć. Stara
obejściówka — hash opcji z ręcznym fetch/walidacją — to boilerplate, który każda
baza kodu wynajdywała na nowo. Wymagane argumenty nazwane dają samodokumentujące się
wywołania oraz wymuszoną obecność oraz swobodną kolejność, bez żadnego kodu
walidacji. Dla każdej metody z więcej niż dwoma parametrami albo z dowolną flagą
boolean to nowy styl domyślny i już sięgamy po niego wszędzie.
Refinements: monkey-patching z ograniczonym zasięgiem rażenia
Refinements wychodzą ze stanu „eksperymentalnego” z jaśniejszym, bezpieczniejszym
projektem. Rozwiązują problem ciemnej strony otwartości Ruby — monkey-patchingu.
Otwórz ponownie String, by dodać metodę, a zmieniłeś String dla całego
procesu: twojego kodu, każdego gemu, frameworka, na zawsze. Tak dwie biblioteki
kończą, każda definiując String#truncate inaczej i po cichu psując się nawzajem, a
wygrywa ta załadowana później.
Refinement to monkey-patch z przełącznikiem. Definiujesz zmianę wewnątrz modułu,
gdzie leży uśpiona, dopóki plik (albo, nowość w 2.1, moduł) jawnie jej nie aktywuje
przez using — i co kluczowe, aktywacja jest zasięgowana leksykalnie:
module Squishing
refine String do
def squish
strip.gsub(/\s+/, " ")
end
end
end
# tylko kod, który się zgłosi, widzi #squish:
using Squishing
" hello world ".squish # => "hello world"
Bez using String#squish nie istnieje. Żaden inny plik, gem ani komponent
frameworka nie jest dotknięty — zmiana jest zawężona do zasięgu leksykalnego, który
o nią poprosił. To długo obiecywana odpowiedź na „chcę dodać metodę do klasy
rdzennej bez zatruwania globalnej przestrzeni nazw”. Ergonomia w 2.1 wciąż ma ostre
krawędzie — reguły zasięgu leksykalnego są subtelne, nie można robić refine wewnątrz
bloku, a wyszukiwanie metod przy aktywnych refinements zaskakiwało ludzi — więc to
nie hurtowe zastępstwo dla Module#prepend czy kompozycji. Ale kierunek jest
dokładnie właściwy: wygoda monkey-patchingu z opanowanym zasięgiem rażenia, i to
narzędzie, po które sięgasz, gdy inaczej otworzyłbyś ponownie klasę rdzenną.
Exception#cause: łańcuch jest zapisywany za ciebie
Cichy faworyt. Gdy łapiesz jeden błąd i podnosisz inny, Ruby 2.1 automatycznie
zapisuje oryginalny jako cause nowego wyjątku — bez ręcznego owijania:
begin
parse_config
rescue SyntaxError
raise ConfigError, "plik konfiguracji jest nieprawidłowy" # oryginalny SyntaxError zachowany
end
ConfigError#cause zwraca leżący u podstaw SyntaxError, a dobrze zachowujący się
wypisywacz backtrace przejdzie łańcuch. Koniec z połykaniem prawdziwej porażki, gdy
podnosisz przyjaźniejszą — ścieżka z powrotem do przyczyny źródłowej jest zachowana
za ciebie.
Mniejsze przyjemności
Garść zmian dopełnia obraz, każda usuwając drobną codzienną uciążliwość:
defzwraca nazwę metody jako symbol. To czyni dekoratory metod czystymi:private def helper; endpo prostu działa, koniec zprivate :helperw osobnej linii po definicji.Array#to_hiEnumerable#to_h. Budowanie hasha z par nie potrzebuje już gimnastykiHash[...]:[[:a, 1], [:b, 2]].to_halboitems.map { |i| [i.id, i] }.to_h.String#scrubzastępuje nieprawidłowe bajty UTF-8 znakiem zastępczym — czysta poprawka na błędyinvalid byte sequence, które trapią aplikacje obsługujące niechlujny tekst z zewnątrz.- Zamrażanie literałów łańcuchowych jest zoptymalizowane.
"text".freezejest teraz traktowane specjalnie i zwraca cache’owaną zamrożoną instancję zamiast alokować, co zapowiada nadchodzący świat frozen-string-literal i warto tego używać dla kluczy hashy i stałych na gorących ścieżkach już dziś. - Literały Rational i Complex zyskują przyrostki:
42rto Rational,2ito Complex — przydatne dla każdego, kto robi dokładną arytmetykę.
Czy aktualizować?
Tak, i bez wielkich ceremonii. Usprawnienie GC to rodzaj darmowej wygranej, którą
bierzesz od razu: zaktualizuj na stagingu, popatrz, jak wykresy pamięci i opóźnień
się uspokajają, nie rób nic więcej. Funkcje języka są czysto addytywne, więc
istniejący kod dalej działa — wymagane argumenty nazwane adoptujesz, dotykając
metod, po refinements sięgasz, gdy następnym razem skusisz się na monkey-patch klasy
rdzennej, a Exception#cause oraz wygody to_h/scrub dostajesz za darmo.
Myślą przewodnią 2.1 jest dojrzałość, nie nowość. Ruby jest strojony pod duże, długo działające aplikacje, które ludzie naprawdę uruchamiają na produkcji: szybszy collector dla serwera, jaśniejsze kontrakty argumentów dla bazy kodu, zdyscyplinowana alternatywa dla własnej najgroźniejszej funkcji i lepsze pochodzenie błędów na sesję debugowania o trzeciej nad ranem. Nic z tego nie jest efekciarskie i wszystko to rzecz, za którą po cichu jesteś wdzięczny pół roku później. Dobry prezent gwiazdkowy.