Ruby Metaprogramming Is Even Cooler Than It Sounds

Często słyszy się, że metaprogramowanie jest czymś czego używają tylko ninja Rubiego, i że po prostu nie jest dla zwykłych śmiertelników. Ale prawda jest taka, że metaprogramowanie wcale nie jest czymś strasznym. Ten wpis na blogu ma na celu podważenie tego typu myślenia i przybliżenie metaprogramowania przeciętnemu programiście Rubiego, tak aby mógł on również czerpać z niego korzyści.

Metaprogramowanie Rubiego: Kod Piszący Kod

Należy zauważyć, że metaprogramowanie może oznaczać wiele i często może być bardzo nadużywane i popadać w skrajności, jeśli chodzi o użycie, więc postaram się wrzucić kilka przykładów ze świata rzeczywistego, które każdy może wykorzystać w codziennym programowaniu.

Metaprogramowanie

Metaprogramowanie jest techniką, dzięki której można napisać kod, który sam pisze kod dynamicznie w czasie wykonywania. Oznacza to, że możesz definiować metody i klasy podczas działania. Szalone, prawda? W skrócie, używając metaprogramowania możesz ponownie otwierać i modyfikować klasy, wyłapywać metody, które nie istnieją i tworzyć je w locie, tworzyć kod, który jest DRY poprzez unikanie powtórzeń i wiele więcej.

Podstawy

Zanim zanurkujemy w poważne metaprogramowanie musimy poznać podstawy. A najlepszym sposobem na to są przykłady. Zacznijmy od jednego i zrozummy metaprogramowanie w Ruby krok po kroku. Zapewne domyślasz się co robi ten kod:

class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend

Zdefiniowaliśmy klasę z dwiema metodami. Pierwsza metoda w tej klasie jest metodą klasową, a druga jest metodą instancji. To są podstawowe rzeczy w Rubim, ale za tym kodem dzieje się o wiele więcej, co musimy zrozumieć zanim przejdziemy dalej. Warto zaznaczyć, że klasa Developer sama w sobie jest obiektem. W Rubim wszystko jest obiektem, łącznie z klasami. Ponieważ Developer jest instancją, jest on instancją klasy Class. Here is how the Ruby object model looks like:

p Developer.class # Classp Class.superclass # Modulep Module.superclass # Objectp Object.superclass # BasicObject

One important thing to understand here is the meaning of self. The frontend method is a regular method that is available on instances of class Developer, but why is backend method a class method? Every piece of code executed in Ruby is executed against a particular self. When the Ruby interpreter executes any code it always keeps track of the value self for any given line. self is always referring to some object but that object can change based on the code executed. For example, inside a class definition, the self refers to the class itself which is an instance of class Class.

class Developer p self end# Developer

Inside instance methods, self refers to an instance of the class.

class Developer def frontend self endend p Developer.new.frontend# #<Developer:0x2c8a148>

Wewnątrz metod klasowych, self odnosi się do samej klasy w pewien sposób (który zostanie omówiony bardziej szczegółowo w dalszej części artykułu):

class Developer def self.backend self endendp Developer.backend# Developer

To jest w porządku, ale czym w końcu jest metoda klasowa? Zanim odpowiemy na to pytanie, musimy wspomnieć o istnieniu czegoś, co nazywamy metaklasą, znaną również jako klasa singleton i klasa eigen. Metoda klasowa frontend, którą zdefiniowaliśmy wcześniej, to nic innego jak metoda instancji zdefiniowana w metaklasie dla obiektu Developer! Metaklasa jest klasą, którą Ruby tworzy i wstawia do hierarchii dziedziczenia, aby przechowywać metody klasy, nie przeszkadzając w ten sposób instancjom, które są tworzone z klasy.

Metaklasy

Każdy obiekt w Rubim ma swoją własną metaklasę. Jest ona w pewien sposób niewidoczna dla dewelopera, ale istnieje i można jej używać w bardzo prosty sposób. Ponieważ nasza klasa Developer jest w zasadzie obiektem, posiada ona swoją własną metaklasę. Jako przykład stwórzmy obiekt klasy String i manipulujmy jego metaklasą:

example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT

To co tutaj zrobiliśmy to dodaliśmy metodę singleton something do obiektu. Różnica pomiędzy metodami klasowymi a metodami singletonowymi polega na tym, że metody klasowe są dostępne dla wszystkich instancji obiektu klasy, podczas gdy metody singletonowe są dostępne tylko dla tej pojedynczej instancji. Metody klasowe są szeroko stosowane, podczas gdy metody singletonowe nie tak bardzo, ale oba typy metod są dodawane do metaklasy tego obiektu.

Poprzedni przykład może być przepisany w ten sposób:

example = "I'm a string object"class << example def something self.upcase endend

Składnia jest inna, ale skutecznie robi to samo. Wróćmy teraz do poprzedniego przykładu, w którym utworzyliśmy Developer klasę i poznajmy kilka innych składni do zdefiniowania metody klasy:

class Developer def self.backend "I am backend developer" endend

To jest podstawowa definicja, której prawie każdy używa.

def Developer.backend "I am backend developer"end

This is the same thing, we are defining the backend class method for Developer. We didn’t use self but defining a method like this effectively makes it a class method.

class Developer class << self def backend "I am backend developer" end endend

Again, we are defining a class method, but using syntax similar to one we used to define a singleton method for a String object. You may notice that we used self here which refers to a Developer object itself. First we opened Developer class, making self equal to the Developer class. Next, we do class << self, making self equal to Developer’s metaclass. Then we define a method backend on Developer’s metaclass.

class << Developer def backend "I am backend developer" endend

By defining a block like this, we are setting self to Developer’s metaclass for the duration of the block. As a result, the backend method is added to Developer’s metaclass, rather than the class itself.

Let’s see how this metaclass behaves in the inheritance tree:

As you saw in previous examples, there’s no real proof that metaclass even exists. But we can use a little hack that can show us the existence of this invisible class:

class Object def metaclass_example class << self self end endend

If we define an instance method in Object class (yes, we can reopen any class anytime, that’s yet another beauty of metaprogramming), we will have a self referring to the Object object inside it. Następnie możemy użyć składni class << self, aby zmienić bieżące self, aby wskazywało na metaklasę bieżącego obiektu. Ponieważ bieżący obiekt jest Object klasą samą w sobie, będzie to metaklasa instancji. Metoda zwraca self, która w tym momencie sama jest metaklasą. Tak więc wywołując metodę instance na dowolnym obiekcie możemy uzyskać metaklasę tego obiektu. Zdefiniujmy ponownie naszą klasę Developer i zacznijmy trochę eksplorować:

A dla crescendo zobaczmy dowód, że frontend jest metodą instancji klasy, a backend jest metodą instancji metaklasy:

Although, aby uzyskać metaklasę, nie musisz faktycznie ponownie otwierać Object i dodać ten hack. Możesz użyć singleton_class, który zapewnia Ruby. To jest to samo co metaclass_example, który dodaliśmy, ale dzięki temu hackowi możesz zobaczyć jak Ruby działa pod maską:

p developer.class.singleton_class.instance_methods false# 

Definiowanie metod przy użyciu „class_eval” i „instance_eval”

Jest jeszcze jeden sposób na stworzenie metody klasy, a jest nim użycie instance_eval:

Ten fragment kodu interpreter Ruby ocenia w kontekście instancji, którą w tym przypadku jest obiekt Developer. Kiedy definiujesz metodę na obiekcie tworzysz albo metodę klasową albo metodę singletonową. W tym przypadku jest to metoda klasowa – dokładnie rzecz ujmując, metody klasowe są metodami singletonowymi, ale singletonowymi klasy, podczas gdy pozostałe są metodami singletonowymi obiektu.

Z drugiej strony, class_eval ocenia kod w kontekście klasy, a nie instancji. To praktycznie otwiera klasę na nowo. Oto jak class_eval może być użyty do stworzenia metody instancji:

Podsumowując, kiedy wywołujesz metodę class_eval, zmieniasz self na odwołanie do oryginalnej klasy, a kiedy wywołujesz instance_evalself zmienia się na odwołanie do oryginalnej metaklasy klasy.

Definiowanie brakujących metod w locie

Jeszcze jednym elementem układanki metaprogramowania jest method_missing. Kiedy wywołujesz metodę na obiekcie, Ruby najpierw przechodzi do klasy i przegląda jej metody instancji. Jeśli nie znajdzie tam metody, kontynuuje poszukiwania w górę łańcucha przodków. Jeśli Ruby nadal nie znajdzie metody, wywoła inną metodę o nazwie method_missing, która jest metodą instancji Kernel, którą dziedziczy każdy obiekt. Ponieważ jesteśmy pewni, że Ruby będzie w końcu wywoływał tę metodę dla brakujących metod, możemy to wykorzystać do zaimplementowania kilku sztuczek.

define_method jest metodą zdefiniowaną w klasie Module, którą możesz wykorzystać do dynamicznego tworzenia metod. Aby użyć define_method, wywołujemy ją podając nazwę nowej metody oraz blok, w którym parametry bloku stają się parametrami nowej metody. Jaka jest różnica między użyciem def do stworzenia metody a define_method? Nie ma dużej różnicy, z wyjątkiem tego, że możesz użyć define_method w połączeniu z method_missing, aby napisać kod DRY. Dokładnie mówiąc, możesz użyć define_method zamiast def do manipulowania zakresami podczas definiowania klasy, ale to już zupełnie inna historia. Przyjrzyjmy się prostemu przykładowi:

To pokazuje jak define_method został użyty do stworzenia metody instancji bez użycia def. Jednak możemy z nimi zrobić o wiele więcej. Spójrzmy na ten wycinek kodu:

class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" endenddeveloper = Developer.newdeveloper.coding_frontend# "writing frontend"developer.coding_backend# "writing backend"

Ten kod nie jest DRY, ale używając define_method możemy sprawić, że będzie DRY:

To jest znacznie lepsze, ale wciąż nie idealne. Dlaczego? Jeśli chcemy dodać nową metodę coding_debug na przykład, musimy umieścić to "debug" w tablicy. Ale używając method_missing możemy to naprawić:

Ten fragment kodu jest trochę skomplikowany, więc rozłóżmy go na czynniki pierwsze. Wywołanie metody, która nie istnieje, spowoduje odpalenie method_missing. Tutaj chcemy utworzyć nową metodę tylko wtedy, gdy nazwa metody zaczyna się od "coding_". W przeciwnym razie po prostu wywołujemy super, aby wykonać pracę polegającą na zgłoszeniu metody, której faktycznie brakuje. A my po prostu używamy define_method do stworzenia tej nowej metody. To jest to! Za pomocą tego kawałka kodu możemy stworzyć dosłownie tysiące nowych metod zaczynających się od "coding_", i to właśnie ten fakt sprawia, że nasz kod jest DRY. Ponieważ define_method jest prywatny dla Module, musimy użyć send aby go wywołać.

Podsumowanie

To tylko wierzchołek góry lodowej. Aby stać się Ruby Jedi, to jest punkt wyjścia. Po opanowaniu tych elementów metaprogramowania i zrozumieniu jego istoty, możesz przejść do czegoś bardziej złożonego, na przykład do stworzenia własnego języka dziedzinowego (Domain-specific Language – DSL). DSL jest tematem samym w sobie, ale te podstawowe pojęcia są warunkiem wstępnym do zrozumienia zaawansowanych tematów. Niektóre z najczęściej używanych klejnotów w Railsach zostały zbudowane w ten sposób i prawdopodobnie używałeś ich DSL nawet o tym nie wiedząc, takie jak RSpec i ActiveRecord.

Mając nadzieję, że ten artykuł przybliży cię o krok do zrozumienia metaprogramowania, a może nawet do zbudowania własnego DSL, którego będziesz mógł używać do bardziej efektywnego kodowania.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.