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 =~ /\.rbw?$/
result += IO.read( path ).scan( /\.[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