Przedziały logiczne (przerzutniki) w Rubim

Posted by Jacek Galanciak on

Przeglądając książki i zasoby sieci natrafiłem na bardzo ciekawą konstrukcję w Rubim:

if wyr1 .. wyr2
while wyr1 .. wyr2

Normalne wyrażenia logiczne mają charakter kombinacyjny - ich wartość zależy jedynie od danych wejściowych. Wyrażenie jest wykonywane, zwraca wartość i jest zapominane. Inaczej jest z przedziałami logicznymi - te działają podobnie jak przerzutniki. Wykazują działanie bistabilne - nie tylko wykonują i zwracają wyrażenia, ale także pamiętają swój poprzedni stan.

Szczypta kodu

Wypróbujmy poniższy kod:

for i in 1..10 do
  if (i == 3)..(i == 7)
    print 1
  else
    print 0
  end
end

Na wyjściu otrzymamy:

0011111000

Jak to działa?

Ogólny schemat działania wyrażenia przedstawia poniższy graf (podziękowania dla Riddle'a):

Ilustracja działania orzedziału logicznego

Innymi słowy:

  • wyróżniamy 2 wewnętrzne stany: ustawiony i nieustawiony
  • rozpoczynamy stanem nieustawionym i sprawdzamy wyr1
  • jeśli jest false, nie zmieniamy stanu wewnętrznego, przerzutnik zwraca false
  • jeśli jest true, przeskakujemy do wyr2, przerzutnik zwraca true
  • jeśli jest false, nie zmieniamy stanu wewnętrznego, przerzutnik zwraca true
  • jeśli jest true, przeskakujemy do wyr1, przerzutnik zwraca true
  • i tak dalej…

Można powiedzieć, że nasz automat zaczął zwracać jedynki, gdy natrafił na spełnienie pierwszego warunku (i == 3), a skończył zwracać po tym, jak tylko drugi warunek (i == 7) zostanie spełniony. Program ponownie czeka na spełnienie warunku pierwszego. Biedaczek ;-).

Coś bardziej praktycznego

Użyjmy przerzutnika w pętli. Napiszemy prosty program, który czyta plik tekstowy i wypisuje bloki zawarte pomiędzy słowami BEGIN i END (wraz z nimi - dla uproszczenia):

file = File.open("test.txt")
while a = file.gets
  puts a if a.chomp == "BEGIN" .. a.chomp == "END"
end

Dla pliku o zawartości

BEGIN
  Ta informacja
  pojawi się
  na wyjściu
END

  Ale ta
  już nie

BEGIN
  A ta już tak
END

program zwróci:

BEGIN
    Ta informacja
    pojawi się
    na wyjściu
END
BEGIN
    A ta już tak
END

Prześledźmy to dokładniej

Napiszmy kolejny prosty skrypcik, ale tym razem skupimy się na samych wyrażeniach i ich wartościach:

# Sprawdza podzielność przez 5
def check5(i)
  puts "check5 zwraca " + (i%5 == 0).to_s
  i%5 == 0
end

# Sprawdza podzielność przez 4
def check4(i)
  puts "check4 zwraca " + (i%4 == 0).to_s
  i%4 == 0
end

for i in 9..22 do
  puts "BEGIN: " + i.to_s
  if check5(i)..check4(i)
    puts "END: true"
  else
    puts "END: false"
  end
  puts
end

Program pokazuje, co zwraca każde z wyrażeń, co zwraca cały przerzutnik, oraz czy dane wyrażenie jest w ogóle wykonywane.

Czym możemy być zaskoczeni?

Zanim zaczniemy poważniej używać przedziałów logicznych, musimy pamiętać o dwóch sprawach, by na przyszłość uniknąć męczących błędów.

  • jeśli wyr1 jest fałszywe, wyr2 nie będzie w ogóle wykonywane; i vice versa
  • w jednym cyklu nasz automat może dwukrotnie zmienić stan - widzimy to dla liczby 20 - pierwszy warunek jest spełniony, przeskakuje więc na drugi stan, którego warunek również jest spełniony; automat wraca więc do swojego pierwszego stanu

Podsumowanie

Może i operator przerzutnika nie należy do najprostszych, ale może mieć potężne zastosowania o miażdżącej zwięzłości. Warto jednak śledzić newsy dotyczące rozwoju Rubiego - część społeczności domaga się usunięcia tej konstrukcji, która dla nich jest perlowym dziwactwem. Ciekawe jak się sprawa dalej potoczy…

PS. Zna ktoś inne ciekawe zastosowania? :-)