Skip to content
← Wszystkie wpisy
4 min czytania Dawid Skłodowski

Ruby 2.3: zamrożone literały łańcuchowe i operator bezpiecznej nawigacji

Ruby 2.3.0 ląduje w Boże Narodzenie z dwiema zmianami, które będziesz pisać codziennie — pragmą frozen-string-literal i operatorem bezpiecznej nawigacji &. — plus dig, did_you_mean i więcej. Praktyczny przegląd.

Ruby 2.3.0 przybyło w Boże Narodzenie, jak to ma w zwyczaju, i to wydanie pełne jest małych funkcji, które faktycznie będziesz pisać. Tam, gdzie ostatnie parę wydań było głównie o garbage collectorze pracującym ciężej za kulisami, 2.3 zmienia kod, który piszesz — nowy operator, nowa pragma na poziomie pliku, parę naprawdę przydatnych metod i przyjaźniejsze doświadczenie, gdy coś idzie nie tak. Oto praktyczny przegląd.

Operator bezpiecznej nawigacji: &.

To nagłówek, zapożyczony z C# i Groovy, gdzie od dawna zarabia na swoje. „Samotny operator” &. woła metodę tylko, jeśli odbiorca nie jest nil, zwracając nil zamiast wybuchać:

# przedtem — obronny taniec
name = user && user.profile && user.profile.name

# Ruby 2.3
name = user&.profile&.name

Jeśli user jest nil, całe wyrażenie zwiera się do nil zamiast podnosić NoMethodError: undefined method 'profile' for nil. To zabija jeden z najczęstszych, brzydkich wzorców w Ruby — łańcuch strażników && albo wywołania try(:method), po które sięgają programiści Rails. Czyta się czysto i mówi dokładnie to, co znaczy: „zawołaj to, jeśli jest na czym zawołać”.

Słowo ostrożności, bo łatwo go nadużyć: &. to narzędzie do przypadków, gdzie nil jest prawowitą, oczekiwaną wartością (opcjonalna asocjacja, konfiguracja, której może nie być). To nie sposób na zatuszowanie nil-i, które nigdy nie powinny wystąpić — jeśli wartość będąca nil wskazuje błąd, chcesz, by NoMethodError go ujawnił, a nie &. połknął go w cichy nil, który zawodzi myląco trzy warstwy dalej. Używaj go do obsługi oczekiwanej nieobecności, nie do ukrywania nieoczekiwanej.

Zamrożone literały łańcuchowe: pragma i przyszłość

Drugą dużą zmianą jest pragma frozen_string_literal — magiczny komentarz na górze pliku, który czyni każdy literał łańcuchowy w nim zamrożonym (niezmiennym):

# frozen_string_literal: true

GREETING = "hello"   # ten łańcuch jest zamrożony — nie da się go zmutować
GREETING << " world" # => RuntimeError: can't modify frozen String

Są dwa powody, dla których to ma znaczenie. Pierwszy to wydajność: w Ruby każdy literał łańcuchowy normalnie alokuje zupełnie nowy obiekt za każdym razem, gdy jest ewaluowany, więc literał w gorącej pętli tworzy śmieci przy każdym przejściu. Zamrożony literał może być pojedynczym cache’owanym, współdzielonym, niezmiennym obiektem — mniej alokacji, mniejsza presja GC. Dla literałów używanych jako klucze hashy i stałe na gorących ścieżkach to realna oszczędność. Drugi to poprawność: niezmiennych łańcuchów nie da się przypadkowo zmutować przez jakiś odległy kod trzymający referencję, co usuwa kategorię błędów typu spooky-action-at-a-distance.

W 2.3 jest to opt-in per plik przez pragmę i to wyraźnie kierunek podróży — długoterminowym planem jest, by zamrożone literały łańcuchowe stały się domyślne w przyszłym Ruby. Przyjęcie pragmy teraz zabezpiecza twoje pliki na przyszłość i zarabia korzyść wydajnościową już dziś. Jedna korekta: gdzie naprawdę potrzebujesz mutowalnego łańcucha, zaalokuj go jawnie przez String.new albo +"..." (unarny plus zwraca mutowalną kopię).

dig: bezpieczne sięganie w zagnieżdżone struktury

Hash#dig i Array#dig są małe, ale będziesz ich używać nieustannie. Przechodzą zagnieżdżoną strukturę sekwencją kluczy, zwracając nil w chwili, gdy brakuje któregokolwiek poziomu, zamiast podnosić błąd:

data = { user: { address: { city: "Kraków" } } }

data.dig(:user, :address, :city)   # => "Kraków"
data.dig(:user, :company, :name)   # => nil  (bez błędu na brakującym :company)

# vs stare, kruche:
data[:user][:company][:name]       # => NoMethodError na nil[:name]

To idea bezpiecznej nawigacji zastosowana do struktur danych i jest darem niebios dla głęboko zagnieżdżonych hashy, które dostajesz z parsowania odpowiedzi JSON API, gdzie któregokolwiek poziomu może brakować. Jedno wywołanie zastępuje stertę sprawdzeń && albo łańcuchów fetch.

did_you_mean: literówki dostają podpowiedź

Wygrana w jakości życia dla wszystkich: gem did_you_mean jest teraz dołączony i włączony domyślnie, więc błędnie napisana nazwa metody lub zmiennej wraca z sugestią zamiast gołego błędu:

NoMethodError: undefined method `lenght' for "hello":String
Did you mean?  length

To drobiazg oszczędzający realny czas, zwłaszcza dla nowicjuszy, i nic cię nie kosztuje.

Mniejsze dodatki

Jeszcze kilka wartych wiedzy:

  • Enumerable#grep_v to odwrotność grep — wybierz wszystko, co nie pasuje: lines.grep_v(/^#/) odrzuca linie komentarzy.
  • Comparable#clamp ogranicza wartość do zakresu: score.clamp(0, 100) zamiast zagnieżdżonych wywołań min/max.
  • Hash#to_proc pozwala hashowi działać jako blok wyszukiwania: ids.map(&lookup_hash) mapuje każde id przez hash.
  • Narzędzia deprecjacji „zamrożonych łańcuchów” (--enable-frozen-string-literal i tryb debug) pomagają znaleźć kod, który by się zepsuł pod przyszłym zachowaniem frozen-by-default.

Czy aktualizować?

Tak — i nietypowo, to wydanie, którego wartość pokazuje się w twoim edytorze, nie tylko w metrykach. Operator bezpiecznej nawigacji i dig posprzątają kod obsługi nil, który piszesz codziennie; pragma frozen_string_literal zarabia korzyść wydajnościową i przygotowuje cię na przyszły domyślny wybór; a did_you_mean po cichu czyni każdy komunikat błędu bardziej pomocnym. Nic z tego nie jest ryzykowne do przyjęcia — &., dig i przyjemności są czysto addytywne, a pragma zamrożonych łańcuchów jest opt-in per plik, więc możesz wdrażać ją stopniowo.

Wzorzec ostatnich wydań Ruby trwa: runtime wciąż robi się szczuplejszy, a język wciąż dostaje małe, ostre usprawnienia ergonomii, które czynią codzienny kod czystszym. 2.3 to szczególnie miła ich porcja — rodzaj wydania, gdzie miesiąc po aktualizacji user&.profile&.name i data.dig(...) już wydają się, jakby zawsze tam były.