W tej części tutoriala nauczymy się korzystać z wejścia i wyjścia na poziomie daleko wykraczającym poza programy "Hello World!"...

Kernel

Moduł Kernel dostarcza nam podstawowe metody związane z obsługą wejścia i wyjścia. Oto i one:

  • putc - wyświetla znak o kodzie ASCII podanym za argument; zwrócmy uwagę, że dodanie do kodu wielokrotności liczby 256 wyświetli taki sam znak

    (97..99).each do |c|
      putc c
    end
    
    (354..356).each do |c|
      putc c
    end
    

    Co na wyjściu da nam:

    abcbcd
  • puts - wyświetla tekst podany za argument, dodając na koniec znak końca linii; jeśli argumentem jest tablica, wyświetla każdy element, oddzielając je separatorem linii; jeżeli którykolwiek z elementów zawiera już separatory, metoda usuwa jeden z nich

    puts "Hello World!"
    puts ["To\n", "tylko\n\n", "test"];

    Wyjście:

    Hello World!
    To
    tylko
    
    test
    
  • print - wypisuje obiekt (lub obiekty) na standardowe wyjście; odziela każdy z nich łańcuchem określonym w zmiennej magicznej $, (która domyślnie ma wartość nil); dodaje na koniec Stringa wynikowego zawartość $\ (domyślnie nil)

    print "a", 1, [2, '4'], "b", "\n"
    
    $, = ", "
    $\ = "\n"
    print "a", 1, [2, '4'], "b"

    Wyjście:

    a124b
    a, 1, 2, 4, b

    Zwróćmy uwagę, że po ustawieniu zmiennej $\ nie musimy dorzucać znaku \n na koniec sekwencji obiektów.

  • printf - działa tak samo, jak standardowa funkcja języka C o takiej samej nazwie
  • open - otwiera plik (jeśli parametr nie rozpoczyna się znakiem |) lub potok podprocesu.

    Poniższy program na dwa sposoby realizuje odczyt pierwszej linii pliku:

    # 1
    f = open "C:\\plik.txt"
    puts f.gets
    
    # 2
    open "C:\\plik.txt" do |f|
      puts f.gets
    end

    Rozpoczynając parametr metody open znakiem potoku, otwieramy podproces z możliwością śledzenia jego wyjścia oraz zapisu danych na jego wejście. Napiszemy teraz program, który wywołuje systemowe polecenie dir (lub jakiekolwiek inne) i wyświetli wynik jego działania na ekranie.

    open("|dir") do |f|
      while ln = f.gets do puts ln end
    end

    Wyniku działania, ze względów bezpieczeństwa, nie podam :-).

Operujemy na plikach

Można to robić w sposób klasyczny lub blokowy:

# 1
f = File.open "C:\\plik.txt"
while ln = f.gets do puts ln end
f.close

# 2
File.open "C:\\plik.txt" do |f|
  while ln = f.gets do puts ln end
end

Przewaga tego drugiego jest dokładnie widoczna, gdy zwracany jest wyjątek. Pierwszy sposób może wtedy ominąć metodę zamykającą plik. Stosując podejście blokowe, możemy kodować (i spać) spokojnie.

Zapis do pliku realizujemy za pomocą metod puts, print itp. obiektu klasy File, które działają tak samo, jak ich imiennicy z modułu Kernel.

Używamy iteratorów

Iteratory to potężne narzędzie (pisałem o nich w poprzedniej części tutoriala), zaimplementowano takowe również do operacji wejścia i wyjścia.

Stwórzmy przykładowy plik o następującej zawartości:

Pierwsza linia
Druga linia
A to jest trzecia

Napiszemy teraz program, który, z wykorzystaniem iteratorów, odczyta kolejno wszystkie znaki (oddzielając każdy minusem) i każdą linię pliku, informując o odczytanym wierszu.

File.open "C:\\plik.txt" do |f|
  f.each_byte { |c| putc c; print "-" }
end

puts # Przejście do nowego wiersza

File.open "C:\\plik.txt" do |f|
  f.each_line { |ln| puts "Napotkałem na wiersz: #{ln}" }
end

Output:

P-i-e-r-w-s-z-a- -l-i-n-i-a--
-D-r-u-g-a- -l-i-n-i-a--
-A- -t-o- -j-e-s-t- -t-r-z-e-c-i-a-
Napotkałem na wiersz: Pierwsza linia
Napotkałem na wiersz: Druga linia
Napotkałem na wiersz: A to jest trzecia

Nic nie stoi na przeszkodzie, byśmy sami wybrali sobie separator linii, gdy domyślny (\n) nam nie odpowiada.

File.open "C:\\plik.txt" do |f|
  f.each_line(" ") { |ln| puts ln }
end
Pierwsza 
linia
Druga 
linia
A 
to 
jest 
trzecia

Jak widać w tym przypadku, za separator linii obraliśmy sobie spację.

Aby URL plikiem się stał

To żaden problem. Wystarczy dodać jedną linię kodu, aby można było traktować adresy internetowe jak zwykłe pliki. Poniższy program wyświetli kod HTML wyszukiwarki Google.

require 'open-uri'

open("http://google.com") do |f|
  puts f.readlines
end

Prawda, że piękne?

Komentarze: Skocz na dół, na górę

  1. Taaak, ten blog znowu żyje! :-)

  2. Bo i ja żyję – sesja się skończyła, a ja odespałem swoje. ;]

  3. Takie małe pytanie: czy w rubym iteratory można obsługiwać tak bardziej „niskopoziomowo”? W pythonie można zrobić:

    for linia in file(’/etc/passwd’): print linia

    ale można też:

    iterator = file(’/etc/passwd’).iter()
    try: while True: print iterator.next()
    except StopIteration: pass

  4. oj, wcięło białe znaki… ale chyba widać o co chodzi

  5. Czy ja wiem, czy takie podejście można nazwać niskopoziomowym? Grunt, że się da:

    IO.readlines('plik').each { |ln| puts ln }
    

    Można też zastosować for, wtedy kod wygląda tak:

    for ln in IO.readlines('plik') do 
      puts ln 
    end
    

    Oczywiście można powiedzieć, że to nie to samo, co zaprezentowany przez Ciebie file, ale zawsze można napisać alias do IO.readlines. Tyle że chyba nie ma sensu klonować innego języka, skoro natywnie w Rubim mamy równie zwięzłe, ale inne sposoby osiągnięcia takiego czytania z pliku. ;)

  6. Chodzi mi o samą ideę iteratorów. Nie staram się klonować pythona w rubym. Wiem tylko, że w pythonie mogę zrobić coś takiego:

    from itertools import cycle # cycle - iterator, który 
    def proces1(): # fibonacci
        a,b=0,1
        while True:
            print a
            a,b = b,a+b
            yield None
    def proces2():
        a=2
        while True:
            print a
            a*=2
            yield None
    def proces3():
        while True:
            print 'Trzeci proces'
            yield None
    # każdy z procesów jest iteratorem, który będzie zwracał za każdym razem None
    procesy = [proces1(), proces2(), proces3()]
    for aktywny in cycle(procesy):
        aktywny.next()
    

    I to wypisze na przemian linijki z różnych iteratorów. To coś w stylu procesów współbieżnych, tyle że same się zrzekają co jakiś czas władzy nad procesorem (za pomocą yielda)... Ale każdy proces ma własne zmienne lokalne itd… Całkiem przydatne na przykład w aplikacjach webowych, gdzie jednym takim procesem reprezentujesz sesję dla jednego użytkownika, trzymasz w nim stan sesji… a jak użytkownik kliknie, proces jest przywracany do życia przez odpowiednią procedurę sterującą tak, jakby nic się nie stało między wysłaniem jednego html do użytkownika, a przyjściem wypełnionego formularza.

  7. Aha, i do tego właśnie przydaje się niskopoziomowa obsługa. Ciekawy jestem po prostu, jak by coś takiego w rubym wyglądało.

  8. Paradoksalnie for to jeszcze wyższy „poziom” ponieważ używa on protokołu each. Nie używają go natomiast while i until:

    f = open(„foo”)
    s = nil;puts s while s = f.gets

    Można też posłużyć się enumeratorami (podobnie jak pythonowe generator expressions nie materializują list)

    require ‘enumerator’
    enum = open(„foo”).to_enum
    enum.each{|e|puts e}

    W 1.9 są już mocno zintegrowane i powyższe sprowadzi się do:

    enum = open(„foo”).each
    enum.each{|e|puts e}

    Choć w tym przypadku są oczywiście wydumane bo IO#each też nie materializuje.

  9. (Komentarz zmodyfikowany 06.07.2007 o 19:42)

    W Rubim oczywiście istnieją zaawansowane możliwości tworzenia i manipulowania iteratorami oraz blokami. Niżej wklejam na szybko napisany kod, który na przemian, cyklicznie wywołuje bloki kodu, które po każdym wywołaniu potrafią kontynuować swoje działanie (zachowują wartości swoich zmiennych).

    class Cycle
      def initialize(enum)
        @collection = enum.to_a
        @len = @collection.length
        @current = 0
      end
    
      def current
        @collection[@current]
      end
    
      def next
        @current += 1
        if @current >= @len
          @current = 0
        end
        @collection[@current]
      end
    
      def each
        loop do
          yield self.current
          self.next
        end
      end
    end
    
    def Cycle(arr)
      Cycle.new(arr.to_a)
    end
    
    def proc1
      a, b = 1, 1
      proc do
        puts "proc1: #{a}"
        a, b = b, a + b
      end
    end
    
    def proc2
      a = 2
      proc do
        puts "proc2: #{a}"
        a *= 2
      end
    end
    
    def proc3
      proc do
        puts "proc3"
      end
    end
    
    for active_proc in Cycle([proc1, proc2, proc3]) do
      active_proc.call
    end
    

    Nie znalazłem w dokumentacji Rubiego metod, które pozwalają na tworzenie cyklów z jakiejś kolekcji, więc napisałem na szybko swoją implementację. Zdaję sobie sprawę, że pod wieloma względami jest ułomna i nieelastyczna, ale tu chodzi o szybki przykład pokazujący, że taka możliwość istnieje także w Rubim. ;-)

    PS. Czuję się sprowokowany i biorę się za pisanie tutoriala o blokach, procach itp. :P

  10. Te proce wyglądają trochę magicznie, napisz o tym ;-)

    Widzę tu funkcję Cycle i klasę Cycle, nie gryzą się? Domyślam się, że ‘initialize’ to domyślna nazwa konstruktora?

  11. Napiszę. Jutro na pewno tutorial wyjdzie na świat. :)

    Co do Cycle – jest to zwykła klasa. Metoda (nie funkcja! :) ) Cycle to tak naprawdę kolejna metoda modułu Kernel i jej definicja mogłaby wyglądać tak:

    module Kernel
      def Cycle
        #...
      end
    end
    

    co byłoby synonimem do tego, co napisałem w poprzednim commencie (metodę można także wywołać poprzez Kernel.Cycle…). Dzięki temu nic się ze sobą nie gryzie.

    I tak, initialize to nazwa konstruktora. :)

    A co do proców – dają sporo możliwości. W tym przypadku pozwalają skonwertować blok kodu na obiekt, by potem przechować go np. w tablicy, a później – wywołać.

  12. Dobrze… to chyba wiem o co chodzi z tym proc. Tylko takie pytanie żeby sprawdzić:

    def proces():
        print 'Za'; yield None
        print 'każdym'; yield None
        print 'razem'; yield None
        for i in xrange(10):
            print i; yield None
        print 'coś'; yield None
        print 'innego'
    

    na ruby? Bo te proce to tylko generują kawałek kodu, a zmienne ‘lokalne’ to po prostu zmienne z domknięcia?

  13. Ciekawe zastosowanie tego pythonowskiego yield None. Nie słyszałem o podobnej konstrukcji w Rubim, chyba nie da się (bez obchodzenia naokoło) napisać czegoś podobnego. Ale… czy to źle? To chyba raczej ciekawostka, niż niezbędnik. A ciekawsze zastosowania typu „porprocesy” z własnymi zmiennymi można zaimplementować także w Rubim, tyle, że w nieco inny sposób.

    Co do samych proców, to konwertują one blok na obiekt. Blok (domknięcie) charakteryzuje się tym, że zapamiętuje kontekst, w jakim był wywołany. Stąd jego zdolność do „pamiętania” zmiennych.

  14. Podawałem już przykład: serwisy webowe. Kiedyś do każdego połączenia serwery odpalały osobne procesy, które były bardzo proste. Teraz kombinuje się na wszystkie sposoby żeby tego nie robić, bo to mało efektywne. Takie mechanizmy jak coroutines (http://en.wikipedia.org/wiki/Coroutine) pozwalają wrócić do takich metod, bez utraty efektywności. Co więcej, zapis jest prosty, więc programista jest w stanie zrozumieć na raz większy kawałek kodu. Inny przykład: programy GUI bez tych denerwujących modalnych okienek. Gdyby Gajim był pisany z wykorzystaniem coroutines (choćby tak prostych jak w cpythonie, bo gdzie im tam do możliwości takiego LISPa czy choćby stackless-pythona!), miałby pewnie ze dwa razy mniej kodu i cztery razy mniej błędów.

  15. Wiem, że sprawa już może nico nieświeża, ale być może ktoś jeszcze zajrzy do tych komentarzy.

    To o co pytał Liorithiel w Rubym jak najbardziej istnieje i są to kontynuacje – klasa Continuation

Dodaj komentarz na temat

Zanim skomentujesz...

W komentarzach działają znaczniki Textile.
Zastrzegam sobie prawo do edycji Twojego komentarza tylko i wyłącznie w celach estetycznych (naprawienie źle wstawionego kodu, itp). Nie zmieniam ich treści, ortografii, interpunkcji. Jeśli odczuwasz potrzebę edycji swojego komentarza, skontaktuj się ze mną, a zdziałamy co trzeba.