Od czasu do czasu zdarza mi się usłyszeć od innych w trakcje programowania To naprawdę działa w Ruby? albo Nie wiedziałam/nie wiedziałem, że to tak działa. W końcu zrozumiałam, że to co dla mnie jest czymś normalnym, inne osoby niekoniecznie znają. Dziś chciałabym się podzielić kilkoma takimi smaczkami z języka Ruby, o których nie wszyscy wiedzą. Mam nadzieję, że Ci się spodobają.
1. Domyślna wartość dla metody dig
i zagnieżdżone obiekty typu Hash/Array
Domyślnie, gdy użyjemy metody dig
z kluczem, którego nie ma w obiekcie Hash, jako wynik dostaniemy nil
. Ale możemy zmienić to zachowanie metody dig
.
Zachowanie domyślne:
hash = { foo: { bar: [:a, :b, :c] } }
hash.dig(:hello)
# => nil
Nasze nowe zachowanie:
hash = { foo: { bar: [:a, :b, :c] } }
hash.default_proc = -> (hash, _key) { hash }
hash.dig(:hello, :world)
# => {:foo=>{:bar=>[:a, :b, :c]}}
hash.dig(:hello, :world, :foo, :bar, 2)
# => :c
Możemy tu zauważyć jeszcze jedną interesującą rzecz. Kiedy używamy metody dig
na bardziej złożonym obiekcie typu Hash niż { foo: 1, bar: 2}
, możemy bez problemu poruszać się po kolejnych zagnieżdżeniach (nawet tych typu Array) by wybrać interesującą nas wartość.
hash = { foo: { bar: [:a, :b, :c] } }
hash.dig(:foo, :bar, 2)
# => :c
2. Szybki sposób na debugowanie z metodą tap
Moim zdaniem najciekawszym użyciem metody tap
w Ruby, jest możliwość stworzenia szybkiego debuggera.
class Object
def debug
tap { |object| p object }
end
end
"foo".upcase.debug.reverse
# "FOO"
# => "OOF"
Teraz dzięki rozszerzeniu klasy Object
, mamy możliwość wglądu w środek łańcucha wywołań metod.
3. Różnica między concat
a +=
dla klasy Array
Zazwyczaj metod concat
i +=
używamy naprzemiennie w naszym kodzie. Lub czasem ktoś woli jedną metodę bardziej niż drugą. Różnice pomiędzy nimi nie są wtedy dla nas istotne lub zauważalne. Ale jest jedna różnica, o której warto pamiętać.
Zazwyczaj interesuje nas wyniki końcowy tych dwóch operacji, który jest ten sam.
array = [1, 2, 3]
array.concat([4, 5, 6])
# => [1, 2, 3, 4, 5, 6]
array = [1, 2, 3]
array += [4, 5, 6]
# => [1, 2, 3, 4, 5, 6]
Ale co jeżeli użyjemy tych metod w połączeniu z metodą tap
?
array = [1, 2, 3]
array.tap { |a| a.concat([4, 5, 6]) }
# => [1, 2, 3, 4, 5, 6]
Metoda concat
zachowuje się tak jakbyśmy tego oczekiwali. Nie możemy tego samego powiedzieć o metodzie +=
.
array = [1, 2, 3]
array.tap { |a| a += [4, 5, 6] }
# => [1, 2, 3]
Co właściwie się tu stało? By dobrze to zrozumieć zacznijmy od zapoznania się z definicją metody tap
z dokumentacji Rubiego:
tap
- Yieldsself
to theblock
, and then returnsself
. The primary purpose of this method is to “tap into” a method chain, in order to perform operations on intermediate results within the chain.
Najważniejsza informacja z tej definicja to ta mówiąca o zwracaniu z metody tap
obiektu self
: and then returns self
. Zobaczmy w takim razie, co dzieje się z array
w przypadku obu metod.
array = []
array.object_id
# => 260
array += [4, 5, 6]
array.object_id
# => 280
W przypadku metody +=
otrzymujemy nowy obiekt, jednak metoda tap
zwraca nam obiekt, na którym została wywołana. Czyli obiekt początkowy. Porównajmy to z metodą concat
.
array = []
array.object_id
# => 300
array.concat([4, 5, 6])
array.object_id
# => 300
Jak widać metoda concat
zwraca ten sam obiekt, który był na początku. Podsumowując metoda concat
dołącza do już istniejącego obiektu dodatkowe elementy. Natomiast metoda +=
tworzy nowy obiekt zawierający wszystkie elementy (te początkowe i te dodane późnej).
Na koniec chciałabym dodać, że jeżeli chcemy otrzymać ten sam efekt jaki otrzymujemy przy użyciu concat
i tap
dla +=
możemy użyć innej metody a dokładnie metody then
. Sprawdzając jej definicję w dokumentacji otrzymujemy:
then
- Yieldsself
to theblock
and returns the result of theblock
.
array = [1, 2, 3]
array.then { |a| a += [4, 5, 6] }
# => [1, 2, 3, 4, 5, 6]
4. Metoda split
z dwoma argumentami
Często używamy metody split
by rozdzielić jakiś tekst, tak jak tutaj:
"ruby:python:java".split(':')
# => ["ruby", "python", "java"]
ale poza tym możemy powiedzieć metodzie split
na ile dokładnie kawałków chcemy podzielić początkowy tekst:
"ruby:python:java".split(':', 1)
# => ["ruby:python:java"]
"ruby:python:java".split(':', 2)
# => ["ruby", "python:java"]
"ruby:python:java".split(':', 3)
# => ["ruby", "python", "java"]
5. Konkatenacja łańcuchów znaków
W języku Ruby istnieje wiele możliwości by połączyć dwa łańcuchy znaków. Niektóre z nich zamieściłam poniżej:
first_name = 'Agnieszka'
last_name = 'Małaszkiewicz'
name = first_name + ' ' + last_name
name
# => "Agnieszka Małaszkiewicz"
name = first_name << ' ' << last_name
name
# => "Agnieszka Małaszkiewicz"
name = "#{first_name} #{last_name}"
name
# => "Agnieszka Małaszkiewicz"
name = [first_name, last_name].join(' ')
name
# => "Agnieszka Małaszkiewicz"
Istnieje jeszcze jeden dość ciekawy sposób na łączenie łańcuchów znaków:
name = "Agnieszka" " " "Małaszkiewicz"
name
# => "Agnieszka Małaszkiewicz"
lub trochę prościej by załapać ideę:
name = "Agnieszka " "Małaszkiewicz"
name
# => "Agnieszka Małaszkiewicz"
Na początku wygląda to dość dziwnie. I tu może pojawić się pytanie: Czy to naprawdę działa w Ruby? Jednak jeżeli zastanowimy się przez chwilę to na pewno zdarzało Ci się podzielić łańcuch znaków pomiędzy dwie linie. Ja czasem używam tego w testach:
it 'calls DeliverCheckInInstructionsForProperty service for properties ' \
'with check-in instructions delivery enabled' do
# ...
end
Teraz ważne pytanie: Dlaczego to działa? Z tego, co udało mi się ustalić, to funkcjonalność ta jest związana z kodem źródłowym języka Ruby dla parse.y
. Bazując na odpowiedzi z Stack Overflow mamy:
A Ruby string is either a
tCHAR
(e.g.?q
), astring1
(e.g."q"
,'q'
, or%q{q}
), or a recursive definition of the concatenation ofstring1
andstring
itself, which results in string expressions like"foo" "bar"
,'foo' "bar"
or?f "oo" 'bar'
being concatenated.
6. Tworzenie obiektu Hash z domyślnymi wartościami
Na temat obiektu Hash
napisałam osobny artykuł Triki dla obiektu Hash w Ruby, gdzie podaję więcej szczegółów. Tutaj chciałabym się skupić na dwóch zastosowaniach domyślnej wartości.
Po pierwsze, możemy ustawić jedną i tą samą wartość dla wszystkich kluczy. To zastosowanie przydaje się gdy chcemy coś zliczać.
hash = Hash.new(0)
hash[:foo]
# => 0
Teraz używając +=
możemy sumować.
hash[:bar] += 1
hash[:bar]
# => 1
Po drugie, możemy zadeklarować domyślną wartość inną dla każdego klucza. Jak na przykład:
hash = Hash.new { |hash, i| i }
hash[1]
# => 1
hash[35]
# => 35
7. Użycie obiektu proc
w case
proc
to jedna z klas w języku Ruby, która pomaga nam w programowaniu funkcyjnym. Jedno z ciekawych zastosowań obiektu proc
, to użycie go w warunku when
dla case
. Możemy stworzyć proc
i wstawić go bezpośrednio w when
. A oto przykład:
payload_1 = {
event_type: 'ConversationAdded',
body: 'Test'
}
payload_2 = {
event_type: 'MessageAdded',
body: 'Test'
}
payload_3 = {
body: 'Test'
}
def payload_object(payload)
message_type_event = proc { |event_type| event_type.include?('Message') }
case payload[:event_type]
when nil then 'Message'
when message_type_event then 'ConversationMessage'
else
'None'
end
end
payload_object(payload_1)
# => "None"
payload_object(payload_2)
# => "ConversationMessage"
payload_object(payload_3)
# => "Message"
8. Wywołanie metody na różne sposoby
Ruby to naprawdę fantastyczny język. Daje nam duże możliwości. Przykładem tego może być możliwość wywołania metody na wiele różnych sposobów. Jeżeli interesuje Cię ile jest takich możliwości, to zachęcam Cię do zapoznania się z artykułem Grzegorza Witka 12 ways to call a method in Ruby. Ja chciałabym podzielić się jeszcze dwoma dodatkowymi sposobami na wywołanie metody. Zastosuje do tego ten sam przykład, który w swoim artykule umieścił Grzegorz.
class User
def initialize(name)
@name = name
end
def hello
puts "Hello, #{@name}!"
end
def method_missing(_)
hello
end
end
Pierwszy sposób odkryłam oglądając prezentacje z RailsConf 2022 Ruby Archaeology by Nick Schwaderer:
user = User.new('Agnieszka')
user::hello
# Hello, Agnieszka!
# => nil
Drugi sposób jest związany z programowaniem funkcyjnym. Możemy zamienić metodę na obiekt typu proc
i wywołać na nim metodę ===
:
user = User.new('Agnieszka')
:hello.to_proc === user
# Hello, Agnieszka!
# => nil
Jeżeli chcesz dowiedzieć się więcej na temat obiektu proc
i metody ===
, to zachęcam Cię do zapoznania się z moim artykułem na temat programowania funkcyjnego.
Potrzebujesz pomocy?
Jeśli szukasz doświadczonej programistki Ruby z ponad dziesięcioletnim stażem, śmiało skontaktuj się ze mną.
Mam doświadczenie w różnych domenach, a szczególną wagę przykładam do szybkiej reakcji na opinie użytkowników i pracy zespołowej. Pomogę Ci stworzyć świetny produkt.