Ruby/Избранное с RubyNews

В далеком 2003 году на просторах Рунета был замечательный сайт (ныне заброшенный). Он занимался тем, что освещал события, связанные с языком программирования Ruby. Помимо новостей вида "появилась новая библиотека" он публиковал интересные статьи и рассуждения. Их хорошо бы внести в наш викиучебник.

Иерархия классов в Ruby

править

Иерархии представлены в виде UML диаграмм, которые представлены в форматах JPG и PDF. Все диаграммы созданы при помощи программы UMLet. Представлена ссылка по которой можно скачать все в одном архиве.

Адрес проекта: http://www.insula.cz/dali/material/rubycl/.

Интуитивная мощь конструкции case

править

Увидел заметку на RubyGarden про конструкцию case...when...else...end и решил ею поделиться с вами. Мало кто знает, что конструкция case может принимать на вход любое количество аргументов.

  • Начнем с малого. Продемонстрируем как case может обходиться вообще без аргументов:
a, b = 5, 6

case 
    when a==b #!!!! 
        puts "a эквивалентно b" 
    else 
        puts "нет совпадения" 
end
Конструкция case обходится без аргументов только тогда, когда выражение после when трактуется как логическое выражение. По сути, пример выше эквивалентен следующему примеру:
a, b = 5, 6

if a==b 
    puts "a эквивалентно b" 
else 
    puts "нет совпадения" 
end
  • Чтобы конструкция case могла правильно воспринимать больше одного аргумента необходимо, чтобы количество выражений проверок when было точно таким же, то есть, если нужно, чтобы case обрабатывала два аргумента, то каждая проверка when также должна содержать два выражения. Обратите внимание, что аргументы case и when взяты в квадратные скобки. Если их не использовать, то интерпретатор будет выдавать сообщение о синтаксической ошибке. Рассмотрим это на примере:
a, b = 1,1
case [a,b] #!
    when [1,1] #!
        puts "a=1, b=1" 
    when [2,2]
        puts "a=2, b=2"
    when [3,3]
        puts "a=3, b=3" 
    else
        puts "нет совпадений" 
end

Думаю, что вам было интересно, т.к. меня эта возможность case очень даже впечатлила.

Особенность с локальными переменными внутри блока

править

Локальная переменная блока после выхода из своей области видимости, продолжает хранить прежнее значение, но только если она была определена до блока. Для начала рассмотрим работающий пример:

loop {
    a = 5 
    break 
}
puts a      #-> Ошибка. Не могу найти метод или переменную a.

При попытке вывести значение переменной a, интерпретатор сообщает о том, что переменная a не определена. Предварим этот пример небольшим блоком if, который заранее никогда не выполняется:

if false 
    a = nil 
end 

loop{
    a = 5 
    break 
}
puts a      #-> 5

И получаем нестандартную ситуацию в которой интерпретатор выводит значение a, которое по его мнению равно 5. Неверующим рекомендую проверить...

Это связано с тем, что Ruby определяет видимость переменных по синтаксическому дереву, а не по выполняемым операциям.

Особенность eval

править

Когда один из моих студентов занимался разработкой калькулятора на Ruby, то он столкнулся с одной интересной особенностью метода eval. В случае синтаксической ошибки в строке, переданой eval в качестве параметра, программа вылетала с ошибкой SyntaxError. Ниже идет пример, который демонстрирует эту ситуацию:

begin 
    eval('2+2+') 
rescue 
    p "error" 
end

Для контроля за ситуацией надо явно указать ошибку, которую вы хотите контролировать (в нашем случае SyntaxError).

begin 
   eval('2+2+') 
rescue SyntaxError 
   p "error" 
end

Теперь все работает! Осталось только заметить, что если строка приходит из внешнего источника, то желательно ее предварительно проверить на соответствие вашим ожиданиям. Иначе, вы будете кормить хакеров... :-) Как пояснил Yuri Leikind, этот эффект связан с тем, что rescue без параметра отлавливает ошибки выполнения программы (Runtime Error), а приведенный выше пример вызывает исключение (Exception). В общем, проблема все равно решается так, как я описал выше. :-)

Переписываем программы на новый лад

править

Набрел я на типовые задания для программистов в МГИУ. И на решение этих задач "тупым сишным способом". Тут же зачесались руки... захотелось исправить данный недостаток.

Задание 1: Определите значение максимального элемента массива, содержащего целые числа.

Устаревшее решение:

a = [1, 3, 4,67,-3] 
max = a[0] 
i = 1 
while i < a.size 
    if a[i]>max 
        max = a[i] 
    end 
    i = i+1 
end 
puts max

Современное решение:

p [1, 3, 4,67,-3].max

Задание 2: Ввести с клавиатуры размерность массива - целое положительное число, после чего заполнить все его элементы действительными числами, введенными с клавиатуры.

Устаревшее решение:

n = -1 
while n < 1 
    print "Введите размерность массива: " 
    n = gets.to_i 
end 
a = Array.new(n) 
i = 0 
while i < n 
    print "Введите #{i}-й элемент массива: " 
    a[i] = gets.to_f 
    i = i+1 
end 
p a

Современное решение:

$stdout.sync = true # для корректного запуска из SciTE 
p Array.new( ( print "Введите размерность массива: " ; gets.to_i ) ){ |i| 
    print "Введите #{i}-й элемент массива: " ; gets.to_f 
}

Задание 3: Ввести с клавиатуры массив целых чисел и определить номер минимального элемента массива (отсчет начинается с нуля).

Устаревшее решение:

n = -1 
while n < 1 
    print "Input n: " 
    n = gets.to_i 
end 
a = Array.new(n) 
i = 0 
while i < n 
    print "Input #{i}-number: " 
    a[i] = gets.to_f 
    i = i+1 
end 
numberMin = 0 
min = a[0] 
for i in 1 .. n-1 
    if a[i] < min 
        numberMin = i 
        min = a[i] 
    end 
end 
puts "Number of minimum is #{numberMin}"

Современное решение:

$stdout.sync = true # для корректного запуска из SciTE 
a = Array.new( ( print "Input n: " ; gets.to_i ) ){ |i| 
    print "Input #{i}-number: " ; gets.to_f 
} 
puts "Number of minimum is #{ a.index( a.min ) }"

Генерация пароля или новый взгляд на метод rand

править

Рассмотрим классическую задачу генерации пароля. Алгоритм решения прост до безобразия: формируем словарь символов из которых будет состоять пароль и затем случайным образом выбираем символы из этого словаря. Результат формируется в виде строки и выводится на экран. Для решения данной задачи "в одну строчку" мы будем использовать возможность инициализации массива через блок, которая появилась в Ruby начиная с версии 1.8. Итак, сразу оговоримся, что наш пароль будет состоять из латинских букв верхнего и нижнего регистра, а также из арабских цифр. Решения данной задачи тогда будет выглядеть так:

chars = ['0'..'9','a'..'z','A'..'Z'].map{ |r| r.to_a }.flatten 
puts Array.new(8){ chars[ rand( chars.size ) ] }.join

Теперь на примере решения данной задачи, хотелось бы подемонстрировать применение новых методов генерации псевдослучайной последовательности. Создадим мы эти методы для класса Integer (целые числа), String (строки), Array (массивы) и Range (диапазоны). Начнем с самого простого - с целых чисел:

class Integer 
    def rand 
        Kernel.rand( self ) 
    end 
end

Как видно из описания метода, он генерит случайное число от 0 до self, исключая self. Теперь применим вновь созданный метод к решению нашей задачи:

chars = ['0'..'9','a'..'z','A'..'Z'].map{ |r| r.to_a }.flatten 
puts Array.new(8){ chars[ chars.size.rand ] }.join

Теперь рассмотрим метод rand для массивов. Дальше мы его будем использовать для описания методов других классов. Итак, описание метода rand для класса Array:

class Array 
    def rand 
        self[ size.rand ] 
    end 
end

Как видно из описания метода, он возвращает произвольный элемент массива. Теперь перепишем решение задачи генераци пароля через вышеописанный метод:

chars = ['0'..'9','a'..'z','A'..'Z'].map{ |r| r.to_a }.flatten 
puts Array.new(8){ chars.rand }.join

По-моему получилось неплохо. Давайте смотреть дальше. Теперь у нас на очереди метод rand для класса String:

class String 
    def rand 
        self.split("").rand 
    end 
end

Как видно, данный метод возвращает произвольный символ строки. Для простоты реализации он реализован через использование метода Array#rand. Перепишем решение задачи генерации пароля через вровь написанный нами метод:

chars = ['0'..'9','a'..'z','A'..'Z'].map{ |r| r.to_a }.flatten.join 
puts Array.new(8){ chars.rand }.join

Итак, остался последний класс, который еще пока не имеет метода rand. Для класса Range мы будем использовать тот же поход, что и для String, а именно сведением класса Range к массиву, а затем уже выборкой случайного элемента из этого диапазона. Смотрим реализацию:

class Range 
    def rand 
        to_a.rand 
    end 
end

Переписываем решение исходной задачи через использование метода Range#rand (последний раз за сегодня):

chars = ['0'..'9','a'..'z','A'..'Z'].map{ |r| r.to_a }.flatten 
puts Array.new(8){ chars[ (0...chars.size).rand ] }.join

Надеюсь, что я не сильно загрузил неискушенного читателя. Если загрузил, то включите музыку и разгрузитесь. :-)

Подпрограммы: функции, методы и другие...

править

Многие из студентов уже встречались с такими словами и даже пытались самостоятельно понять, что они значат. Но вопрос все равно возникает, и хотелось бы его разобрать по кускам, чтобы он больше не возникал. Начнем с того, что подпрограммы, функции и методы - это все одно и тоже. Отличия лишь в незначительных деталях. Все это кусок программы, которые программист решил объявить всего лишь один раз, а потом вызывать столько сколько душе угодно. Например, программисту заказали программу подсчета суммы факториалов (помните 1*2*:*N) двух чисел. Вот как он написал бы программу, если бы у него не было возможности составлять подпрограммы:

n1, n2 = 4, 5 
p1, p2 = 1, 1 
n1.times{ |i| p1 *= i + 1 } 
n2.times{ |i| p2 *= i + 1 } 
puts "#{n1}! + #{n2}! = #{p1 + p2}"

Он тут же смекнул, что писать кусок программы подсчета факториала числа он написал дважды (чисел, то две штуки) и решил написать подпрограмму, которая этот факториал числа вычислит. Вот как будет выглядеть более продвинутая программа:

def fact( int ) 
    pr = 1 
    int.times{ |i| pr *= i + 1 } 
    pr 
end 
n1, n2 = 4, 5 
puts "#{n1}! + #{n2}! = #{fact(n1) + fact(n2)}"

А если нужно было бы складывать три числа? Выигрыш в простоте очевиден. А теперь давайте разберемся, что же такое мы написал смекалистый программист. Разберем сначала верхний кусок. Он называется ОПРЕДЕЛЕНИЕМ ПОДПРОГРАММЫ или ОПРЕДЕЛЕНИЕМ МЕТОДА (от англ. define methods). Верхняя строчка состоит из ключевого слова def, названия метода (в нашем случае fact) и списка аргументов, то есть входных данных (в нашем случае int). Если в списке аргументов указано всего одно имя переменной, то и передавать в метод (при вызове) надо тоже один аргумент (обратите внимание на вызов метода fact, fact(n1). Его не обязательно вызывать с переменной в качестве аргумента. Достаточно указать какое либо число, например fact(4) ). Помимо того, взгляните на end в конце тела метода. ЛЮБОЙ МЕТОД должен заканчиваться end. Типичный шаблон для создания метода выглядит примерно так:

def имя_метода( имя_первого_аргумента, имя_второго_аргумента, и т.д.) 
    кусок_программы_который_мы_хотим_поместить_в_подпрограмму 
end

Вызывать наш метод надо примерно так:

имя_метода( значение_первого_аргумента, значение_второго_аргумента, и т.д.)

Теперь обратите внимание на то, что переменная pr в теле метода (на которую мы заменили переменные p1 и p2) расположена в последней строчке подпрограммы (т.е. непосредственно перед end). Вроде как она там прохлаждается и совсем не нужна, но это не так. Таким образом, мы говорим, что переменная pr является РЕЗУЛЬТАТОМ РАБОТЫ ПОДПРОГРАММЫ. И как раз результат перемножения хранится в этой переменной, т.е. данная переменная содержит факториал числа int (т.е. числа, которое передано в качестве первого аргумента). Отсюда и получается, что наша подпрограмма ВОЗВРАЩАЕТ факториал числа. Теперь ПРАВИЛО: программный КОД, который ПОВТОРЯЕТСЯ больше двух раз, ДОЛЖЕН быть ВЫНЕСЕН В ПОДПРОГРАММУ. А сейчас немного о различиях между подпрограммой, функцией и методом. Дело в том, что ПОДПРОГРАММА - это ОБЩЕЕ НАЗВАНИЕ методов, функций и процедур, т.е. объединяющее понятие. Если программист говорит подпрограмма, то он может иметь в виду как метод, так и процедуру. Теперь про различия между методами, функциями и процедурами. Процедуры - это устаревшая конструкция, от которой многие языки программирования отказались. На данный момент она используется только в языке Pascal. СЕЙЧАС в основном ОСТАЛИСЬ только МЕТОДЫ, и ФУНКЦИИ. От процедур их отличает то, что ОНИ ВОЗВРАЩАЮТ РЕЗУЛЬТАТ. Иными словами, ПРОЦЕДУРА - это метод или функция, которые НЕ ВОЗВРАЩАЮТ РЕЗУЛЬТАТА. Теперь пор различия между методами и функциями. МЕТОДЫ есть только в ОБЪЕКТНО-ОРИЕНТИРОВАННЫХ ЯЗЫКАХ. ФУНКЦИИ во всех ОСТАЛЬНЫХ. Отсюда можно заключить, что мы написали с вами подпрограмму, которая является методом, т.к. Ruby - ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ ЯЗЫК. PS. Большие буквы -- это влияние самоучителя Драгункина... :-)

Тенденции в ООП

править

Просматривая исходники некоторых стандартных библиотек Ruby наткнулся на интересную тенденцию в описании классов. Мое наблюдение касается объявление методов класса. Напомню, что такое метод класса... Метод класса - это метод, который вызывается не относительно объекта, а относительно самого класса. Пример:

puts Dir.getwd # getwd -- метод класса 
Dir.new("testdir").each{|x| puts "Got #{x}" } # each -- метод объекта, а new -- метод класса.

В первом случае происходит вызов метода класса, а во втором - метода объекта (метод класса new создает объект, от которого вызывается метод each). Перед вызовом метода класса всегда идет название класса (в нашем случае Dir). В чем же их отличие при объявлении? Ниже представлен пример объявления класса метода и класса объекта:

class Dir 
    def Dir.getwd 
        # тело метода 
    end 
    def each 
        # тело метода 
    end 
end

Теперь про тенденцию... Если методов класса много, то в стандартных библиотеках просто создают еще один блок, в который помещают методы класса. Пример:

class Dir 
    class << self
        def getwd 
            # тело метода 
        end 
    end 
    def each 
        # тело метода 
    end 
end

Вот такое интересное наблюдение. Думаю, что эта методика закреплена в каком либо регламентирующем документе и в дальнейшем будет стандартом.

100 популярнейших методов в Ruby

править

Какие методы в Ruby самые популярные? Естественно, что ответ на подобный вопрос зависит от опытности программиста и его собственного стиля. А ведь хочется узнать ответ "в общем"... Для чего мне это? Ну как же, детей надо обучать методам первой необходимости (остальные сами выучат). Им же лень учить все! Поэтому при проектировании учебных пособий следует учитывать частоту использования тех или иных методов. Метод [] (он же .at) учитывать не будем, ибо ясно, что он один из популярнейших. Чтобы найти наши 100 излюбленных методов Руби мы напишем простенькую программу (которая имеет право глючить) и натравим ее на каталог ruby (для узости можно натравить только на каталог ruby/lib). Методы будем искать и в строках и в коментариях. Для тех, кто хочет чистоты эксперимента, может удалять из обработки строки и коментарии. Я их оставил умышленно. Итак, код программы:

require 'find' 
result = [] 
Find.find('c:/ruby/'){ |path| 
    if test(?f,path) && path =~ /&#92;.rbw?$/ 
        result += IO.read( path ).scan( /&#92;.[a-z][_w!?]+/ ) 
    end 
} 
puts (result - ['.com','.jp','.org','.rb','.rbw','.amazon']).inject( Hash.new(0) ){ |result,elem| 
    result[ elem ] = result[ elem ].succ result }.sort_by{ |array| array[1] 
}.reverse[0...100].map{ |array| array.reverse.join(' : ') }

Как видно из кода программы, она предельно проста и хватает даже доменные зоны в качестве методов (а чего?! метод .com очень даже ничего). Результат ее работы примерно такой (цифра слева — это частота использования):

11866 : .new 
 2075 : .each 
 1589 : .create 
 1409 : .kind_of? 
 1178 : .pack 
 1140 : .size 
 1047 : .to_s 
 1046 : .join 
  914 : .name 
  832 : .nil? 
  817 : .freeze 
  711 : .push 
  692 : .to_i 
  620 : .id 
  615 : .empty? 
  583 : .delete 
  571 : .length 
  569 : .class 
  563 : .collect 
  555 : .shift
  519 : .path 
  513 : .call 
  433 : .add 
  423 : .data 
  419 : .bind 
  415 : .inspect 
  413 : .split 
  370 : .value 
  349 : .text 
  329 : .include?
  324 : .manager
  316 : .index
  315 : .connect
  312 : .open
  305 : .is_a?
  303 : .dup 
  282 : .insert
  267 : .gsub
  266 : .print
  260 : .concat
  256 : .close
  255 : .puts 
  248 : .destroy 
  231 : .read 
  231 : .start
  231 : .pop
  220 : .set
  212 : .parse
  207 : .ac
  204 : .parent
  203 : .match
  200 : .kyutech
  200 : .last
  193 : .current
  189 : .root
  179 : .update
  177 : .ruby
  177 : .respond_to?
  174 : .downcase
  171 : .grid
  171 : .properties
  169 : .key?
  169 : .gsub!
  165 : .to_f
  162 : .type
  160 : .write
  159 : .message
  156 : .width
  154 : .to_a
  151 : .find
  149 : .invoke
  145 : .require
  144 : .critical
  140 : .nodeType
  138 : .mainloop
  136 : .configure
  133 : .unpack
  129 : .has_key?
  129 : .clear
  128 : .map
  127 : .exist?
  122 : .chr
  121 : .html
  120 : .strip
  118 : .now
  117 : .namespace
  115 : .handle
  114 : .first
  114 : .method
  112 : .sub 
  112 : .unshift
  112 : .sort
  112 : .sub!
  109 : .scan
  109 : .run
  107 : .body
  107 : .appendItem
  105 : .taint
  103 : .height
  103 : .id2obj

Все это конечно бред, зато прикольно и есть над чем подумать! :-)

Условие может объединяться не только при помощи ||

править

Правил я программку для mikrit'a и применил достаточно интересный подход для составных условий вида type == const1 || type == const2. Итак, чтобы было понятно, продемонстрирую пример кода:

CONST1, CONST2 = 45, 37 
var = gets.to_i 
puts( if CONST1 == var || CONST2 == var then "yes" else "no" end )

В данном примере наглядно видно дублирование кода. Попытаемся от него избавиться:

CONST1, CONST2 = 45, 37
var = gets.to_i
puts( if [CONST1,CONST2].include?( var ) then "yes" else "no" end )

Данный способ позволяет не только убрать дублирование кода, но и (в случае необходимости) добавить еще одно подобное условие.

Константы при программировании окошек

править

Во время программирования окошек всегда приходится создавать массу констант. Чтобы потом на эти константы вешать обработчики событий. И вот какая меня посетила идея по этому поводу. Чаще всего первые строчки типичной оконной программы выглядят примерно так:

ID_FRAME = 1
ID_DIALOG_1 = 2
ID_DIALOG_2 = 3
ID_DIALOG_3 = 4

Иногда можно увидеть, как разработчик выравнивает объявления констант в одну строчку:

ID_FRAME, ID_DIALOG_1, ID_DIALOG_2, ID_DIALOG_3 = 1,2,3,4

А теперь продолжим мысль и заменим правую часть более короткой записью:

ID_FRAME, ID_DIALOG_1, ID_DIALOG_2, ID_DIALOG_3 = (1..4).to_a

Теперь используем оператор * вместо метода .to_a, что позволит запись сделать более изящной, но и более непонятной:

ID_FRAME, ID_DIALOG_1, ID_DIALOG_2, ID_DIALOG_3 = *1..4