Ruby 3.4 i długa droga do zamrożonych literałów łańcuchowych
Ruby 3.4 robi ostrożny krok ku zmianie tworzonej od dekady: uczynieniu literałów łańcuchowych zamrożonymi domyślnie. Mechanizm przejścia — „schłodzone” łańcuchy, które ostrzegają zamiast łamać — to mała lekcja mistrzowska ewolucji języka bez łamania świata. Spojrzenie na to, dlaczego zamrożone łańcuchy i jak Ruby dochodzi tam łagodnie.
Są święta, więc jest nowe Ruby — 3.4 — a zmianą, na której chcę się skupić, nie jest najefektowniejsza w wydaniu, ale ta z najdłuższą historią i najbardziej pouczającym podejściem: powolny, ostrożny marsz ku zamrożonym literałom łańcuchowym domyślnie. To nadchodzi od niemal dekady, a 3.4 robi świadomy kolejny krok ku temu bez łamania kogokolwiek. Co czyni to wartym wpisu, to nie tylko funkcja; to jak Ruby czyni tę zmianę — mała lekcja mistrzowska ewolucji żywego języka z milionami linii istniejącego kodu, bez wyciągania dywanu. Omówmy, dlaczego zamrożone literały łańcuchowe są w ogóle chciane, historię i sprytny mechanizm przejścia, który wprowadza 3.4.
Po co zamrażać literały łańcuchowe?
Najpierw motywacja, bo korzyść nie jest oczywista, dopóki nie pomyślisz, co literał łańcuchowy faktycznie
robi. W Ruby łańcuchy są mutowalne domyślnie — "hello" tworzy obiekt łańcucha, który możesz zmienić w
miejscu. I za każdym razem, gdy linia z literałem łańcuchowym się uruchamia, Ruby tworzy nowy obiekt
łańcucha, bo ten obiekt mógłby być zmutowany i każda ewaluacja potrzebuje własnego. Rozważ metodę, która
buduje łańcuch z "prefix-" w środku, wołaną tysiące razy: to tysiące identycznych obiektów łańcucha
zaalokowanych, użytych krótko i wyrzuconych, by garbage collector je posprzątał.
Jeśli literał łańcuchowy jest zamrożony — niemutowalny — Ruby nie potrzebuje świeżego obiektu za każdym
razem. Ten sam zamrożony "prefix-" może być bezpiecznie użyty ponownie przez każdą ewaluację, bo nikt nie
może go zmienić. To kupuje dwie rzeczy:
- Mniej pamięci i mniej presji GC. Użycie ponownie jednego zamrożonego obiektu zamiast alokowania nowego na każdej ewaluacji redukuje alokacje, co znaczy mniej pracy dla garbage collectora — realna, mierzalna korzyść wydajności w gorących ścieżkach kodu.
- Niemutowalność jako poprawność. Zamrożone łańcuchy nie mogą być zmutowane przypadkiem. Cała kategoria błędu — gdzie jakaś metoda nieoczekiwanie modyfikuje łańcuch, który przekazałeś, a zmiana fala się gdzieś, gdzie się nie spodziewałeś — po prostu nie może się zdarzyć z zamrożonymi łańcuchami. Niemutowalność czyni kod łatwiejszym do rozumowania.
To dokładnie powody, dla których niemutowalność jest ceniona ogólnie, zastosowane do pojedynczo najczęstszego literału w kodzie Ruby. Uczynienie literałów łańcuchowych zamrożonymi domyślnie dałoby każdemu programowi Ruby te korzyści za darmo. Haczyk — i powód, dla którego zajęło to dekadę — jest taki, że to łamiąca zmiana: kod polegający na mutowaniu literałów łańcuchowych by się złamał.
Dekada magicznego komentarza
Pierwszym ruchem Ruby ku temu, lata temu, był magiczny komentarz: umieść # frozen_string_literal: true
na górze pliku, a wszystkie literały łańcuchowe w tym pliku stają się zamrożone. To było opt-in, per plik, i
pozwalało ostrożnym i dbającym o wydajność uzyskać korzyści i ujawnić dowolny kod, który mutował literały
(który teraz by podniósł błąd), plik po pliku, na własnym harmonogramie. Wiele gemów i aplikacji to przyjęło,
i stało się to częstym widokiem na górze plików Ruby i standardową rekomendacją rubocopa.
Ale magiczny komentarz zawsze był pomyślany jako kamień milowy, nie cel. Celem zawsze było odwrócenie domyślnego, by literały łańcuchowe były zamrożone bez potrzeby komentarza — dając każdemu programowi Ruby korzyść, nie tylko plikom, które się zgłosiły. Problem jest oczywisty: odwrócenie tego domyślnego wprost złamałoby każdy kawałek kodu, gdziekolwiek, który mutuje literał łańcuchowy bez zgłoszenia się do nowego zachowania. Nie możesz tego zrobić językowi z ogromnym zasobem istniejącego kodu Ruby. Więc pytaniem przez lata było nie czy, ale jak uczynić przejście łagodnie. To jest to, co adresuje 3.4.
Sprytny kawałek: „schłodzone” łańcuchy
Mechanizm przejścia Ruby 3.4 jest naprawdę elegancki i zasługuje na reflektor. Zamiast odwrócić literały łańcuchowe z mutowalnych prosto na zamrożone — co zamieniłoby każdą mutację literału w natychmiastowy błąd — 3.4 wprowadza stan pośredni, nieformalnie zwany „schłodzonymi” łańcuchami. Literał łańcuchowy w pliku bez magicznego komentarza jest teraz schłodzony, co znaczy:
- Zachowuje się jak zamrożony w sensie, że jest gotowy, by być — dostajesz semantykę niemutowalności odtąd.
- Ale jeśli twój kod go mutuje, zamiast podnieść błąd (jak zrobiłby naprawdę zamrożony łańcuch), emituje ostrzeżenie o przestarzałości — i tylko gdy zgłosiłeś się, by widzieć te ostrzeżenia.
To jest mistrzowskie posunięcie. Schłodzony łańcuch to „zamrożony, ale ostrzeże cię zamiast łamać, jeśli go zmutujesz”. To daje ekosystemowi ścieżkę przejścia bez twardego złamania: istniejący kod, który mutuje literały, wciąż działa, ale teraz mówi ci, że robi coś, co ostatecznie będzie błędem. Możesz uruchomić swój zestaw testów z włączonymi ostrzeżeniami, zobaczyć dokładnie, gdzie twój kod mutuje literały łańcuchowe, naprawić te miejsca we własnym tempie i być gotowym na dzień, gdy domyślne naprawdę odwróci się na zamrożone. Niczyj kod nie łamie się dziś; każdy dostaje jasne, wykonalne ostrzeżenie o tym, co naprawić przed jutrem.
Dlaczego to model ewolucji języka
Chcę zatrzymać się na podejściu, bo to prawdziwa lekcja i uogólnia się daleko poza łańcuchy. Język z ogromną zainstalowaną bazą staje przed prawdziwym dylematem, kiedykolwiek chce poprawić domyślne: poprawa jest warta posiadania, ale jej odwrócenie łamie istniejący kod, a łamanie istniejącego kodu przy skali niszczy zaufanie, które czyni ludzi chętnymi do aktualizacji. Tanie opcje są obie złe — nigdy nie zmieniaj domyślnego (i zaniechaj korzyści na zawsze) albo zmień je gwałtownie (i złam świat). Mechanizm schłodzonych łańcuchów to droga, poprawna trzecia opcja: wprowadź stan pośredni, który dostarcza nową semantykę, jednocześnie trzymając stare zachowanie działające z ostrzeżeniem, by migracja mogła dziać się stopniowo, widocznie i na własnym harmonogramie każdego zespołu.
To ta sama filozofia co dobry cykl przestarzałości w dowolnym systemie — nie usuwaj starej rzeczy w dniu, gdy dodajesz nową; ostrzeż, daj ludziom okno, potem usuń. Ruby stosujący to tak ostrożnie do czegoś tak fundamentalnego jak literały łańcuchowe to cicha demonstracja tego, jak dojrzały język pozostaje zarazem ulepszalny i godny zaufania. Funkcja jest miła; metoda to lekcja mistrzowska.
Reszta 3.4
Świąteczne wydanie niesie więcej, oczywiście. Ruby 3.4 czyni it dostępnym jako domyślne odniesienie do
pierwszego parametru bloku (mała ergonomiczna miłość dla krótkich bloków), promuje parser Prism, o którym
pisałem w zeszłym roku, ku byciu domyślnym, i kontynuuje stały strumień usprawnień YJIT i biblioteki
standardowej, które charakteryzują dojrzałe wydania Ruby. Nic z tego nie wymaga przepisania — co, jak
zauważyłem w zeszłym roku, jest dokładnie tym, czego chcesz od dojrzałej platformy: rzeczy, które po cichu
stają się lepsze, nie prosząc cię o nauczenie się czegokolwiek od nowa.
Werdykt
Najbardziej pouczającą zmianą Ruby 3.4 jest ostrożny kolejny krok ku zamrożonym literałom łańcuchowym
domyślnie — zmianie tworzonej niemal od dekady. Motywacja jest solidna: zamrożone literały pozwalają Ruby
użyć ponownie jednego niemutowalnego obiektu zamiast alokować świeży mutowalny łańcuch na każdej ewaluacji,
tnąc pamięć i presję GC, i usuwają całą klasę błędów przypadkowej mutacji. Przeszkodą zawsze było to, że
odwrócenie domyślnego to łamiąca zmiana dla języka z ogromnym zasobem istniejącego kodu, a odpowiedź Ruby to
naprawdę elegancka część: po dekadzie opt-in magicznego komentarza # frozen_string_literal jako kamienia
milowego, 3.4 wprowadza „schłodzone” łańcuchy — literały niosące semantykę zamrożenia, ale ostrzegające
zamiast łamać przy mutacji, i tylko gdy zgłosisz się do ostrzeżeń. Ten stan pośredni pozwala każdemu
zespołowi znaleźć i naprawić swoje miejsca mutacji literałów stopniowo, na własnym harmonogramie, bez łamania
niczyjego kodu dziś. Funkcja ma znaczenie, ale metoda ma większe: to model tego, jak dojrzały język ewoluuje
fundamentalne domyślne bez zdradzania zaufania każdego, kto na nim polega — ostrzeż, daj okno, potem zmień.
Uruchom swój zestaw z włączonymi ostrzeżeniami, napraw, co oznaczą, a będziesz gotowy, gdy domyślne wreszcie
zamarznie. To, a nie jakakolwiek pojedyncza funkcja, to najbardziej rubowa rzecz w Ruby 3.4.