User Tools

Site Tools


inf:ruby:programming

4.

4.3. Bloki i iteratory

Bloki

  • Blok to kawałek kodu pomiędzy klamrami lub do i end,
  • blok to coś jak ciało anonimowej metody: przyjmuje parametry (wszystko pomiędzy |) i nie jest wykonywany, od razu,
  • bloki mogą pojawić się tylko po wywołaniu jakiejś metody,
  • zmienne zdefiniowane w bloku są dla niego lokalne,
  • parametry do niego przekazywane również są lokalne oraz przesłaniają zmienne zdefiniowane poza blokiem,
  • można zdefiniować lokalne zmienne bloku, podając je po średniku w liście parametrów. Dzięki temu, zmienne o tej samej nazwie poza blokiem nie zostaną nadpisane:
    best_movie = "Young and Beautiful"
     
    ["The Great Beauty", "The Wolf of Wall Street", "Il Divo"].each do |movie; best_movie|
      best_movie = movie if movie.include?('Divo')
    end
     
    puts best_movie
     
    # => Young and Beautiful

Iteratory

  • Iterator to metoda, która wywołuje blok,
  • w tej metodzie, blok wywoływany jest za pomocą polecenia yield,
  • gdy blok zostanie wykonany, Ruby kontynuuje wykonywanie kodu po uruchomionym yield,
  • do takiego bloku można przekazywać parametry i otrzymywać od nich wartości:
    def praise_her(actress)
      praise = yield actress
      puts "#{praise} very much!"
    end
     
    praise_her('Marine Vacth') { |a| "I love #{a}" }
  • jeśli wywołamy inject bez podania parametru, metoda użyje pierwszego elementu kolekcji jako wartości początkowej i rozpoczyna iterację z drugim elementem:
     > [1, 2, 3].inject() {|sum, element| sum + element}
     => 6 
     > [1, 2, 3].inject(10) {|sum, element| sum + element}
     => 16

Enumeratory

  • Enumerator to coś w stylu zewnętrznego iteratora - podczas gdy iteratory są wewnętrzne dla danej kolekcji (jest to tylko metoda, która wywołuje yield gdy wygeneruje nową wartość), enumeratory to pełnoprawne obiekty, które można przekazywać metodom,
  • enumeratory tworzymy za pomocą metody to_enum (lub enum_for),
  • metoda loop wykonuje przekazany jej blok w nieskończoność. Kiedy jednak wykorzystamy tam obiekt enumeratora, gdy jego elementy się skończą, loop zakończy swoje działanie:
     > short_enum = [1, 2, 3].to_enum
     => #<Enumerator: [1, 2, 3]:each> 
     > long_enum = ('a' .. 'z').to_enum
     => #<Enumerator: "a".."z":each> 
     > loop {puts "#{short_enum.next} - #{long_enum.next}"}
    1 - a
    2 - b
    3 - c
     => nil 
  • można utworzyć enumerator, przekazując mu blok. Kod z bloku będzie wykorzystany, gdy enumerator będzie musiał dostarczyć nową wartość programowi. Kod bloku nie jest po prostu wykonywany od góry na dół a równolegle z resztą programu. Wykonywanie zaczyna się na początku i zatrzymuje się, gdy blok zwraca wartość do programu. Gdy potrzebna jest kolejna wartość, kod wznawia się po poprzednim yield. Dzięki temu, można tworzyć nieskończone enumeratory:
    fib = Enumerator.new do |y|
      a = b = 1
      loop do
        y.yield a
        a, b = b, a + b
      end
    end
     
    p fib.take(10) # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
  • w powyższym przykładzie, iteracja jest zdefiniowana przez podany blok, w którym obiekt yielder, podany jako parametru bloku, może być wykorzystany by otrzymać wartość, wywołując metodę yield,
  • można przekonwertować blok w obiekt, przypisać go do zmiennej, przekazywać dalej i wywołać kod później,
  • jeśli ostatni parametr metody poprzedzimy znakiem &, Ruby będzie szukać bloku z każdym wywołaniem metody. Blok jest zamieniany na obiekt klasy Proc i przypisany parametrowi,
  • dobrym powodem aby przekazać blok metodzie, jest fakt, że kod w takim bloku jest obliczany ponownie.

5. Dziedziczenie, moduły i mixiny

  • Dziedziczenie pozwala tworzyć klasę, która jest szczególną wersją innej klasy,
  • wszystkie metody instancji są dziedziczone przez podklasę,
  • często wykorzystuje się dziedziczenie aby dodać specyficzne dla danej aplikacji zachowanie do istniejącej już biblioteki lub frameworka,
  • klasa w Rubym ma tylko jedną bezpośrednią nadklasę, może jednak zawierać w sobie wiele mixinów (tj. częściowych definicji klas),
  • moduły to sposób na grupowanie razem metod, klas i stałych,
  • moduły zapewniają przestrzeń nazw, zapobiegając kolizjom,
  • moduł nie może mieć instancji, ponieważ nie jest klasą. Można jednak dołączać moduły do klas. Wszystkie metody instancji będą dostępne w tej klasie - zostają w nią wmieszane (mixin),
  • gdy moduł jest dołączany do klasy, staje się jej nadklasą,
  • polecenie include tworzy referencję do modułu. Jeśli ten moduł jest w innym pliku, musi zostać wcześniej załadowany (poprzez require lub load). Jeśli moduł jest zawarty w wielu klasach i jest zmieniony podczas działania programu, wszystkie klasy będą miały uaktualnione metody z tego modułu,
  • prawdziwa siła mixinów pojawia się, gdy metody z modułu współdziałają z metodami obiektu,
  • moduł dołączany do klasy może w niej tworzyć zmienne instancji i może używać attr_reader i jemu podobnych aby zdefiniować akcesory dla tych zmiennych,
  • może to doprowadzić do kolizji ze zmiennymi klasy bądź innych mixinów,
  • w większości wypadków, moduły dołączane do klas nie korzystają ze zmiennych instancji klasy bezpośrednio, ale za pomocą akcesorów, jeśli jednak jest konieczne aby mixin posiadał swój własny stan, trzeba się upewnić, że zmienne mają unikalne nazwy,
  • trzeba jednak pamiętać, że jeśli mixin wymaga takich zabiegów, to chyba powinien być pełnoprawną klasą,
  • szukając metody, Ruby najpierw sprawdza w bezpośredniej klasie obiektu, następnie w mixinach dołączonych do tej klasy, a w końcu w nadklasie i jej mixinach. Jeśli klasa ma wiele mixinów, dołączony ostatnio jest najpierw przeszukiwany,
  • dziedziczenie to bardzo silne połączenie dwóch komponentów - każda zmiana nadklasy to duże ryzyko problemów w podklasie. Jest to poważne ograniczenie zwinności programu i dlatego odchodzi się od dziedziczenia (is a) w stronę kompozycji (has a, uses a).

6. Typy danych

Liczby

  • Liczby całkowite mogą być o dowolnej długości, do wyczerpania pamięci komputera,
  • w pewnych przedziałach, przechowywane są jako obiekty klasy Fixnum (-2^30…2^30 - 1 lub -2^62…2^62-1)
  • poza tym przedziałem, liczby przechowywane są jako Bignum. Proces przechodzenia pomiędzy przedziałami jest transparentny
  • zapisuje się je wykorzystując:
    • opcjonalny znak (-),
    • opcjonalny wskaźnik systemu pozycyjnego (0 - ósemkowy, 0d - dziesiętny, 0x - szesnastkowy, 0b - binarny),
    • lista cyfr,
  • liczba z częścią po przecinku jest obiektem Float,
  • Ruby obsługuje liczby rzeczywiste i złożone:
    Rational(3, 4) * Rational(2, 3)    # => (1/2)
    Complex("1+2i") * Complex("3+4i")  # => (-5+10i)
  • jeśli na dwóch liczbach różnych klas wykonywana jest jakaś operacja, wynikiem będzie obiekt bardziej ogólnej klasy (Fixnum dodany do Floata zwróci Float, Float dodany do liczby złożonej, zwróci liczbę złożoną).

Łańcuchy znaków

  • W łańcuchach zawartych pomiędzy pojedynczym cudzysłowem, dwa kolejne backslashe są zastępowane jednym, a pojedynczy cudzysłów po backslashu staje się pojedynczym cudzysłowem,

24. Metaprogramowanie

  • Metaprogramowanie to pisanie kodu, który generuje kod,
  • metaprogramując, nie jesteśmy ograniczeni do abstrakcji wbudowanych w dany język programowania; tworzymy własny poziom abstrakcji, bliższy domenie problemu, który staramy się rozwiązać,
  • metaprogramowanie umożliwia uproszczenie kodu,

24.1. Obiekty i klasy

  • W Rubym istnieje koncepcja obecnego obiektu. Obecny obiekt jest zreferencjonowany przez wbudowaną, niemodyfikowalną zmienną self,
  • gdy odczytujemy zmienne instancji, Ruby szuka ich w obiekcie zreferencjonowanym przez self,
  • każda metoda jest wywoływana na jakimś obiekcie (na przykład movie.length wywołuje metodę length na obiekcie movie, który nazywamy odbiorcą wywołania). Jeśli wywołujemy metodę bez podania odbiorcy (puts string), Ruby jako odbiorcę ustawia obecny obiekt, zreferencjonowany przez self i tam szuka metody. W obu przypadkach, proces ten jest zbliżony, ponieważ podanie odbiorcy spowoduje ustawienie self na obiekt odbiorcy (w naszym przypadku obiekt zreferencjonowany przez movie) na czas wywołania metody,
  • self jest zmieniana również podczas definiowania klasy. Jest to spowodowane tym, że w Rubym klasy to również wykonywalny kod, a jeżeli jesteśmy w stanie wykonać jakiś kod, musi on mieć obiekt.
    class Vcard
      puts "self = #{self}, self.class = #{self.class}"
    end
     
    # => self = Vcard, self.class = Class

    self jest ustawiona na klasę Vcard, która również jest obiektem. Zmienne instancji, zdefiniowane w środku definicji klasy będą dostępne metodom klasy, ponieważ self będzie takie samo gdy były one definiowane i kiedy metody były uruchamiane.

    class Ticket
      @price = 100
     
      def self.first_price
        @price
      end
    end
     
    Ticket.first_price
     
    # => 100

24.2 Singletony

  • Ruby umożliwia definiowanie metod konkretnemu obiektowi.
    class Ticket; end
     
    ticket_1 = Ticket.new
    ticket_2 = Ticket.new
     
    def ticket_1.price
      puts "Too much!"
    end
     
    ticket_1.price
    ticket_2.price
     
    # => Too much!
    # => singleton_methods.rb:11:in `<main>': undefined method `price' for #<Ticket:0x91292ac> (NoMethodError)
  • gdy zdefiniowaliśmy metodę singletonową dla ticket_1, Ruby utworzył nową anonimową klasę i zdefiniował w niej metodę price. Ta anonimowa klasa, nazywana klasą singletonową lub eigenclass jest ustawiana jako superklasa klasy Ticket, ale tylko dla obiektu ticket_1,
  • w praktyce, coś takiego jak metody klasy nie istnieją. Zamiast tego, definiujemy metody singletonowe na obiektach klasy Class i wywołujemy je na tym obiekcie,
  • drugi sposób na definiowanie metody singletonowej wygląda następująco:
    class << ticket_1
      def info
        puts "I am a #{self}"
      end
    end
  • w środku powyższej definicji, self jest ustawiane do klasy singletonowej. Ponieważ definicje klas zwracają wartość ostatnio wykonanego polecenia, możemy to wykorzystać do dostania się do obiektu singletona:
    class Ticket; end
     
    ticket_1 = Ticket.new
     
    def ticket_1.price
      puts "Too much!"
    end
     
    singleton = class << ticket_1
      def info
        puts "Ticket from Knurow to Zernica"
      end
      self
    end
     
    puts "Singleton: #{singleton}"
    puts "Singleton methods: #{singleton.instance_methods - Ticket.new.methods}"
     
    # Singleton: #<Class:#<Ticket:0x8bcded4>>
    # Singleton methods: [:price, :info]
  • Ruby uniemożliwia wykorzystanie takiego obiektu poza kontekstem ich pierwotnego obiektu. Nie można utworzyć nowej instancji obiektu singletonowego.

24.3. Dziedziczność i widoczność

  • W definicji klasy, można zmienić widoczność metody dziedziczonej z superklasy. Ruby wstawia niewidoczną metodę w klasie, która wywołuje tą z superklasy za pomocą metody super i ustawia jej widoczność na tą, którą wybraliśmy; super może wywoływać metody superklasy bez względu na ich widoczność.

24.4. Moduły i Mixiny

include

  • include jest tak zaimplementowane, że wstawiany moduł jest ustawiany jako klasa nadrzędna klasy, do której moduł wstawiamy,
  • z tego powodu, moduł musi przechowywać informacje poprzedniej superklasie obiektu, do którego jest wstawiany. Ponadto, moduł może być wstawiany do różnych obiektów o różnych superklasach - jeden obiekt modułu nie wystarczy. Aby sobie z tym poradzić, Ruby tworzy nowy obiekt klasy, ustawia go jako superklasę klasy, do której moduł jest wstawiany. Superklasą nowo utworzonego obiektu zostaje superklasa klasy, do której moduł jest wstawiany. Na końcu referencjonuje metody modułu w nowej klasie, że wywołując metodę na nowym obiekcie, jest ona wyszukiwana w module (FIXME)
  • zmiany w module uwidaczniają się w klasie nawet po jego wstawieniu,
  • wstawiając moduł, do którego wstawione są inne moduły, uzyskamy dostęp do metod z wszystkich modułów,
  • Ruby wstawi moduł tylko raz w łańcuchu dziedziczenia.

extend

  • jeśli chcemy dodać moduł tylko do konkretnego obiektu, korzystamy z metody Object#extend,
  • jeśli wykorzystamy metodę w definicji klasy, metody modułu stają się metodami klasy. Dzieje się tak, ponieważ wywołanie extend to ekwiwalent self.extend, metody dodawane są zatem do self, co w definicji klasy jest tą klasą.

24.5. Metaprogramowanie - makra z poziomu klasy

  • Hierarchia klas singletonowych jest równoległa do hierarchii normalnej klasy. Wynikiem tego jest to, że metody klasy w klasie nadrzędnej są również dostępne w klasie podrzędnej:
    class Logger
      def self.add_logging
        def log(message)
          STDERR.puts Time.now.strftime("%H:%M:%S:") + "#{self} (#{message})"
        end
      end
    end
     
    class Example < Logger
      add_logging
    end
     
    ex = Example.new
    ex.log("Haii")
  • metoda define_method, przyjmując nazwę metody i blok, definiuje metodę o podanej nazwie i ciele metody z bloku. Każdy argument w definicji bloku staje się parametrem nowej metody.
    class Logger
      def self.add_logging(id_string)
        define_method(:log) do |msg|
          now = Time.now.strftime("%H:%M:%S")
          STDERR.puts "#{now}-#{id_string}: #{self} (#{msg})"
        end
      end
    end
     
    class Song < Logger
      add_logging "Song"
    end
     
    class Album < Logger
      add_logging "Album"
    end
     
    Song.new.log("tralala")
  • sprawy nieco się komplikują, gdy chcemy dodać zarówno metody klasy jak i obiektu do nowo definiowanej klasy. Jedną z technik wykorzystywanych w Railsach jest metoda included, która jest uruchamiana za każdym razem, gdy dołącza się moduł do klasy.
    module GeneralLogger
     
      def log(msg)
        puts Time.now.strftime("%H:%M: ") + msg
      end
     
      module ClassMethods
        def attr_logger(name)
          attr_reader name
          define_method("#{name}=") do |val|
            log "Assigning #{val.inspect} to #{name}"
            instance_variable_set("@#{name}", val)
          end
        end
      end
     
      def self.included(receiver)
        receiver.extend ClassMethods
      end
    end
     
    class Example
      include GeneralLogger
     
      attr_logger :value
    end
     
    ex = Example.new
    ex.log("Hello!")
    ex.value = 123
    puts "Value is #{ex.value}"
    ex.value = "Snooker"
    puts "Value is #{ex.value}"

25. Refleksja, ObjectSpace i rozproszony Ruby

25.3. Dynamiczne wywoływanie metod

  • Metoda Object#send umożliwia wywołanie metody na obiekcie.
    [1, 2, 3, 4].send(:size)
    # => 4
     
    "Olivia Wilde".send("sub", "livia", "scar")
    # => "Oscar Wilde" 
  • Obiekt klasy Method umożliwia to samo. Zachowuje się jak Proc, to znaczy że zachowuje kontekst, w którym uruchamia.
    olivia = "Olivia Wilde".method("sub")
    # => #<Method: String#sub> 
     
    olivia.call("livia", "scar")
    # => "Oscar Wilde"  = "Olivia Wilde".method("sub")
inf/ruby/programming.txt · Last modified: 2021/02/16 09:56 (external edit)