Текст книги "Программирование на языке Ruby"
Автор книги: Хэл Фултон
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 16 (всего у книги 56 страниц) [доступный отрывок для чтения: 20 страниц]
5.11. Комплексные числа
Стандартная библиотека complex
предназначена для работы с комплексными числами в Ruby. Большая ее часть не требует пояснений.
Для создания комплексного числа применяется следующая несколько необычная нотация:
z = Complex(3,5) # 3+5i
Необычно в ней то, что имя метода совпадает с именем класса. В данном случае наличие скобок указывает на то, что это вызов метода, а не ссылка на константу. Вообще говоря, имена методов не похожи на константы, и я не рекомендую начинать имена методов с прописной буквы, разве что в подобных специальных случаях. (Отметим, что имеются также методы Integer
и Float
; вообще, имена, начинающиеся с прописной буквы, зарезервированы для методов, которые выполняют преобразование данных и аналогичные действия.)
Метод im
преобразует вещественное число в мнимое (по существу, умножая его на i). Поэтому представлять комплексные числа можно и с помощью более привычной нотации:
а = 3.im # 3i
b = 5 – 2.im # 5-2i
Если вас больше интересуют полярные координаты, то можно обратиться к методу polar
:
2 – Complex.polar(5,Math::PI/2.0) # Радиус, угол.
В классе Complex
имеется также константа I
, которая представляет число i – квадратный корень из минус единицы:
z1 = Complex(3,5)
z2 = 3 + 5*Complex::I # z2 == z1
После загрузки библиотеки complex
некоторые стандартные математические функции изменяют свое поведение. Тригонометрические функции – sin
, sinh
, tan
и tanh
(а также некоторые другие, например, ехр
и log
) начинают принимать еще и комплексные аргументы. Некоторые функции, например sqrt
, даже возвращают комплексные числа в качестве результата.
x = Math.sqrt(Complex(3,5)) # Приближенно Complex(2.1013, 1.1897)
y = Math.sqrt(-1) # Complex(0,1)
Дополнительную информацию ищите в любой полной документации, в частности на сайте ruby-doc.org.
5.12. Библиотека mathn
В программах, выполняющих большой объем математических вычислений, очень пригодится замечательная библиотека mathn
, которую написал Кейдзу Исидзука (Keiju Ishitsuka). В ней есть целый ряд удобных методов и классов; кроме того, она унифицирует все классы Ruby для работы с числами так, что они начинают хорошо работать совместно.
Простейший способ воспользоваться этой библиотекой – включить ее с помощью директивы require
и забыть. Поскольку она сама включает библиотеки complex
, rational
и matrix
(в таком порядке), то вы можете этого не делать.
В общем случае библиотека mathn
пытается вернуть «разумные» результаты вычислений. Например, при извлечении квадратного корня из Rational
будет возвращен новый объект Rational
, если это возможно; в противном случае Float
. В таблице 5.1 приведены некоторые последствия загрузки этой библиотеки.
Таблица 5.1. Результаты вычислений в случае отсутствия и наличия библиотеки mathn
Math.sqrt(Rational(9,16)) | 0.75 | Rational(3,4) |
1/2 | 0 | Rational(1,2) |
Matrix.identity(3)/3 | Matrix[[0,0,0], [0,0,0],[0,0,0]] | Matrix[[1/3,0,0], [0,1/3,0],[0,0,1/3]] |
Math.sqrt(64/25) | 1.4142… | Rational(8,5) |
Rational(1,10).inspect | Rational(1,10) | 1/10 |
Библиотека mathn
добавляет методы **
и power2
в класс Rational
. Она изменяет поведение метода Math.sqrt
и добавляет метод Math.rsqrt
, умеющий работать с рациональными числами.
Дополнительная информация приводится в разделах 5.13 и 5.14.
5.13. Разложение на простые множители, вычисление НОД и НОК
В библиотеке mathn
определены также некоторые новые методы в классе Integer
. Так, метод gcd2
служит для нахождения наибольшего общего делителя (НОД) объекта, от имени которого он вызван, и другого числа.
n = 36.gcd2(120) # 12 k = 237.gcd2(79) # 79
Метод prime_division
выполняет разложение на простые множители. Результат возвращается в виде массива массивов, в котором каждый вложенный массив содержит простое число и показатель степени, с которым оно входит в произведение.
factors = 126.prime_division # [[2,1], [3,2], [7,1]]
# To есть 2**1 * 3**2 * 7**1
Имеется также метод класса Integer.from_prime_division
, который восстанавливает исходное число из его сомножителей. Это именно метод класса, потому что выступает в роли «конструктора» целого числа.
factors = [[2,1],[3,1],[7,1]]
num = Integer.from_prime_division(factors) # 42
Ниже показано, как разложение на простые множители можно использовать для отыскания наименьшего общего кратного (НОК) двух чисел:
require 'mathn'
class Integer
def lcm(other)
pf1 = self.prime_division.flatten
pf2 = other.prime_division.flatten
h1 = Hash[*pf1]
h2 = Hash[*pf2]
hash = h2.merge(h1) {|key,old,new| [old,new].max }
Integer.from_prime_division(hash.to_a)
end
end
p 15.1cm(150) # 150
p 2.1cm(3) # 6
p 4.1cm(12) # 12
p 200.1cm(30) # 600
5.14. Простые числа
В библиотеке mathn
есть класс для порождения простых чисел. Итератор each
возвращает последовательные простые числа в бесконечном цикле. Метод succ
порождает следующее простое число. Вот, например, два способа получить первые 100 простых чисел:
require 'mathn'
list = []
gen = Prime.new
gen.each do |prime|
list << prime
break if list.size == 100
end
# или:
list = []
gen = Prime.new
100.times { list << gen.succ }
В следующем фрагменте проверяется, является ли данное число простым. Отметим, что если число велико, а машина медленная, то на выполнение может уйти заметное время:
require 'mathn'
class Integer
def prime?
max = Math.sqrt(self).ceil
max -= 1 if max % 2 == 0
pgen = Prime.new
pgen.each do |factor|
return false if self % factor == 0
return true if factor > max
end
end
end
31.prime? # true
237.prime? # false
1500450271.prime? # true
5.15. Явные и неявные преобразования чисел
Программисты, только начинающие изучать Ruby, часто удивляются, зачем нужны два метода to_i
и to_int
(и аналогичные им to_f
и to_flt
). В общем случае метод с коротким именем применяется для явных преобразований, а метод с длинным именем – для неявных.
Что это означает? Во-первых, в большинстве классов определены явные конверторы, но нет неявных. Насколько мне известно, методы to_int
и to_flt
не определены ни в одном из системных классов.
Во-вторых, в своих собственных классах вы, скорее всего, будете определять неявные конверторы, но не станете вызывать их вручную (если только не заняты написанием «клиентского» кода или библиотеки, которая пытается не конфликтовать с внешним миром).
Следующий пример, конечно, надуманный. В нем определен класс MyClass
, который возвращает константы из методов to_i
и to_int
. Такое поведение лишено смысла, зато иллюстрирует идею:
class MyClass
def to_i
3
end
def to_int
5
end
end
Желая явно преобразовать объект класса MyClass
в целое число, мы вызовем метод to_i
:
m = MyClass.new x = m.to_i # 3
Но при передаче объекта MyClass
какой-нибудь функции, ожидающей целое число, будет неявно вызван метод to_int
. Предположим, к примеру, что мы хотим создать массив с известным начальным числом элементов. Метод Array.new
может принять целое, но что если вместо этого ему будет передан объект MyClass
?
m = MyClass.new
a = Array.new(m) # [nil,nil,nil,nil,nil]
Как видите, метод new
оказался достаточно «умным», чтобы вызвать to_int
и затем создать массив из пяти элементов.
Дополнительную информацию о поведении в другом контексте (строковом) вы найдете в разделе 2.16. См. также раздел 5.16.
5.16. Приведение числовых значений
Приведение можно считать еще одним видом неявного преобразования. Если некоторому методу (например, +
) передается аргумент, которого он не понимает, он пытается привести объект, от имени которого вызван, и аргумент к совместимым типам, а затем сложить их. Принцип использования метода coerce в вашем собственном классе понятен из следующего примера:
class MyNumberSystem
def +(other)
if other.kind_of?(MyNumberSystem)
result = some_calculation_between_self_and_other
MyNumberSystem.new(result)
else
n1, n2 = other.coerce(self)
n1 + n2
end
end
end
Метод coerce
возвращает массив из двух элементов, содержащий аргумент и вызывающий объект, приведенные к совместимым типам.
В данном примере мы полагаемся на то, что приведение выполнит тип аргумента. Но если мы хотим быть законопослушными гражданами, то должны реализовать приведение в своем классе, сделав его пригодным для работы с другими типами чисел. Для этого нужно знать, с какими типами мы в состоянии работать непосредственно, и при необходимости выполнять приведение к одному из этих типов. Если мы не можем сделать это самостоятельно, то должны обратиться за помощью к родительскому классу.
def coerce(other)
if other.kind_of?(Float)
return other, self.to_f
elsif other.kind_of?(Integer)
return other, self.to_i
else
super
end
end
Разумеется, это будет работать только, если наш объект реализует методы to_i
и to_f
.
Метод coerce
можно применить для реализации автоматического преобразования строк в числа, как в языке Perl:
class String
def coerce(n)
if self['.']
[n, Float(self)]
else
[n, Integer(self)]
end
end
end
x = 1 + "23" # 24
y = 23 * "1.23" # 28.29
Мы не настаиваем на таком решении. Но рекомендуем реализовывать coerce
при создании любого класса для работы с числовыми данными.
5.17. Поразрядные операции над числами
Иногда требуется работать с двоичным представлением объекта Fixnum
. На прикладном уровне такая необходимость возникает нечасто, но все-таки возникает.
Ruby обладает всеми средствами для таких операций. Для удобства числовые константы можно записывать в двоичном, восьмеричном или шестнадцатеричном виде. Поразрядным операциям И, ИЛИ, ИСКЛЮЧАЮЩЕЕ ИЛИ и НЕ соответствуют операторы &
, |
, ^
и ~
.
x = 0377 # Восьмеричное (десятичное 255)
y = 0b00100110 # Двоичное (десятичное 38)
z = 0xBEEF # Шестнадцатеричное (десятичное 48879)
а = x | z # 48895 (поразрядное ИЛИ)
b = x & z # 239 (поразрядное И)
с = x ^ z # 48656 (поразрядное ИСКЛЮЧАЮЩЕЕ ИЛИ)
d = ~ y # -39 (отрицание или дополнение до 1)
Метод экземпляра size
позволяет узнать размер слова для той машины, на которой исполняется программа.
size # Для конкретной машины возвращает 4.
Имеются операторы сдвига влево и вправо (<<
и >>
соответственно). Это логические операторы сдвига, они не затрагивают знаковый бит (хотя оператор >>
распространяет его).
x = 8
y = -8
а = x >> 2 # 2
b = y >> 2 # -2
с = x << 2 # 32
d = y << 2 # -32
Конечно, если сдвиг настолько велик, что дает нулевое значение, то знаковый бит теряется, поскольку -0 и 0 – одно и то же.
Квадратные скобки позволяют трактовать числа как битовые массивы. Бит с номером 0 всегда является младшим, вне зависимости от порядка битов в конкретной машинной архитектуре.
x = 5 # То же, что 0b0101
а = x[0] # 1
b = x[1] # 0
с = x[2] # 1
d = x[3] # 0
# И так далее # 0
Присваивать новые значения отдельным битам с помощью такой нотации невозможно (поскольку Fixnum
хранится как непосредственное значение, а не как ссылка на объект). Но можно имитировать это действие путем сдвига 1 влево на нужное число позиций с последующим выполнением операции ИЛИ или И.
# Выполнить присваивание x[3] = 1 нельзя,
# но можно поступить так:
x |= (1<<3)
# Выполнить присваивание x[4] = 0 нельзя,
# но можно поступить так:
x &= ~(1<<4)
5.18. Преобразование системы счисления
Ясно, что любое целое число можно представить в любой системе счисления, поскольку хранятся эти числа в двоичном виде. Мы знаем, что Ruby умеет работать c целыми константами, записанными в любой из четырех наиболее популярных систем. Следовательно, разговор о преобразовании системы счисления может вестись только применительно к числам, записанным в виде строк.
Вопрос о преобразовании строки в целое рассмотрен в разделе 2.24. Для преобразования числа в строку проще всего воспользоваться методом to_s
, которому можно еще передать основание системы счисления. По умолчанию оно равно 10, но в принципе может быть любым вплоть до 36 (когда задействованы все буквы латинского алфавита).
237.to_s(2) # "11101101"
237.to_s(5) # "1422"
237.to_s(8) # "355"
237.to_s # "237"
237.to_s(16) # "ed"
237.to_s(30) # "7r"
Другой способ – обратиться к методу %
класса String
:
hex = "%x" % 1234 # "4d2"
oct = "%о" % 1234 # "2322"
bin = "%b" % 1234 # "10011010010"
Метод sprintf
тоже годится:
str = sprintf(str,"Nietzsche is %xn",57005)
# str теперь равно: "Nietzsche is deadn"
Если нужно сразу же вывести преобразованное в строку значение, то подойдет и метод printf
.
5.19. Извлечение кубических корней, корней четвертой степени и т.д.
В Ruby встроена функция извлечения квадратного корня (Math.sqrt
), поскольку именно она применяется чаще всего. А если надо извлечь корень более высокой степени? Если вы еще не забыли математику, то эта задача не вызовет затруднений.
Можно, например, воспользоваться логарифмами. Напомним, что е в степени x – обратная функция к натуральному логарифму x и что умножение чисел эквивалентно сложению их логарифмов.
x = 531441
cuberoot = Math.exp(Math.log(x)/3.0) # 81.0
fourthroot = Math.exp(Math.log(x)/4.0) # 27.0
Но можно просто использовать дробные показатели степени (оператор возведения в степень принимает в качестве аргумента произвольное целое число или число с плавающей точкой).
include Math
y = 4096
cuberoot = y**(1.0/3.0) # 16.0
fourthroot = y**(1.0/4.0) # 8.0
fourthroot = sqrt(sqrt(y)) # 8.0 (то же самое)
twelfthroot = y**(1.0/12.0) # 2.0
Отметим, что во всех примерах мы пользовались при делении числами с плавающей точкой (чтобы избежать отбрасывания дробной части).
5.20. Определение порядка байтов
Интересно, что производители компьютеров никак не могут договориться, в каком порядке лучше хранить двоичные байты. Следует ли размещать старший бит по большему или по меньшему адресу? При передаче сообщения по проводам нужно сначала посылать старший или младший бит?
Хотите верьте, хотите нет, но решение не произвольно. Существуют убедительные аргументы в пользу обеих точек зрения (обсуждать их здесь мы не будем).
Вот уже больше двадцати лет, как для описания противоположных позиций применяются термины «остроконечный» (little-endian) и «тупоконечный» (big-endian). Кажется, впервые их употребил Дэнни Коэн (Danny Cohen); см. его классическую статью "On Holy Wars and a Plea for Peace" (IEEE Computer, October 1981). Взяты они из романа Джонатана Свифта «Путешествия Гулливера».
Обычно нам безразличен порядок байтов в конкретной машинной архитектуре. Но как быть, если все-таки его нужно знать?
Можно воспользоваться показанным ниже методом. Он возвращает одну из строк LITTLE
, BIG
или OTHER
. Решение основано на том факте, что директива l
выполняет упаковку в машинном формате, а директива N
распаковывает в сетевом порядке байтов (по определению тупоконечном).
def endianness
num = 0x12345678
little = "78563412"
big = "12345678"
native = [num].pack('1')
netunpack = native.unpack('N')[0]
str = "%8x" % netunpack
case str
when little
"LITTLE"
when big
"BIG"
else
"OTHER"
end
end
puts endianness # В данном случае печатается "LITTLE"
Этот прием может оказаться удобным, если, например, вы работаете с двоичными данными (скажем, отсканированным изображением), импортированными из другой системы.
5.21. Численное вычисление определенного интеграла
Я очень хорошо владею дифференциальным и интегральным исчислением…
У.С.Джильберт, «Пираты Пензанса», акт 1
Для приближенного вычисления определенного интеграла имеется проверенная временем техника. Любой студент, изучавший математический анализ, вспомнит, что она называется суммой Римана.
Приведенный ниже метод integrate
принимает начальное и конечное значения зависимой переменной, а также приращение. Четвертый параметр (который на самом деле параметром не является) – это блок. В блоке должно вычисляться значение функции от переданной в него зависимой переменной (здесь слово «переменная» употребляется в математическом, а не программистском смысле). Необязательно отдельно определять функцию, которая вызывается в блоке, но для ясности мы это сделаем.
def integrate(x0, x1, dx=(x1-x0)/1000.0)
x = x0
sum = 0
loop do
y = yield(x)
sum += dx * y
x += dx
break if x > x1
end
sum
end
def f(x)
x**2
end
z = integrate(0.0,5.0) {|x| f(x) }
puts z, "n" # 41.7291875
Здесь мы опираемся на тот факт, что блок возвращает значение, которое может быть получено с помощью yield
. Кроме того, сделаны некоторые допущения. Во-первых, мы предполагаем, что x0
меньше x1
(в противном случае получится бесконечный цикл). Читатель сам легко устранит подобные огрехи. Во-вторых, мы считаем, что функцию можно вычислить в любой точке заданной области. Если это не так, мы получим хаотическое поведение. (Впрочем, подобные функции все равно, как правило, не интегрируемы – по крайней мере, на указанном интервале. В качестве примера возьмите функцию f(x)=x/(x-3)
в точке x=3
.)
Призвав на помощь полузабытые знания об интегральном исчислении, мы могли бы вычислить, что в данном случае результат равен примерно 41.666
(5 в кубе, поделенное на 3). Почему же ответ не так точен, как хотелось бы? Из-за выбранного размера приращения; чем меньше величина dx
, тем точнее результат (ценой увеличения времени вычисления).
Напоследок отметим, что подобная методика более полезна для действительно сложных функций, а не таких простых, как f(x) = x**2
.
5.22. Тригонометрия в градусах, радианах и градах
При измерении дуг математической, а заодно и «естественной» единицей измерения является радиан. По определению, угол в один радиан соответствует длине дуги, равной радиусу окружности. Немного поразмыслив, легко понять, что угол 2π радиан соответствует всей окружности.
Дуговой градус, которым мы пользуемся в повседневной жизни, – пережиток древневавилонской системы счисления по основанию 60: в ней окружность делится на 360 градусов. Менее известна псевдометрическая единица измерения град, определенная так, что прямой угол составляет 100 град (а вся окружность – 400 град).
При вычислении тригонометрических функций в языках программирования по умолчанию чаще всего используются радианы, и Ruby в этом отношении не исключение. Но мы покажем, как производить вычисления и в градусах, и в градах для тех читателей, которые по образованию не инженеры, а по происхождению не древние вавилоняне.
Поскольку число любых угловых единиц в окружности – константа, можно легко переходить от одних единиц к другим. Мы определим соответствующие константы и будем пользоваться ими в коде. Для удобства поместим их в модуль Math.
module Math
RAD2DEG = 360.0/(2.0*PI) # Радианы в градусы.
RAD2GRAD = 400.0/(2.0*РI) # Радианы в грады.
end
Теперь можно определить и новые тригонометрические функции. Поскольку мы всегда преобразуем в радианы, то будем делить на определенные выше коэффициенты. Можно было бы поместить определения функций в тот же модуль Math, но мы этого делать не стали.
def sin_d(theta)
Math.sin(theta/Math::RAD2DEG)
end
def sin_g(theta)
Math.sin(theta/Math::RAD2GRAD)
end
Функции cos
и tan
можно было бы определить аналогично.
С функцией atan2
дело обстоит несколько сложнее. Она принимает два аргумента (длины противолежащей и прилежащей сторон прямоугольного треугольника). Поэтому мы преобразуем результат, а не аргумент:
def atan2_d(y,x)
Math.atan2(у,x)/Math::RAD2DEG
end
def atan2_g(y,x)
Math.atan2(y, x)/Math::RAD2GRAD
end
5.23. Неэлементарная тригонометрия
В ранних версиях Ruby не было функций arcsin
и arccos
. Равно как и гиперболических функций sinh
, cosh
и tanh
. Их определения были приведены в первом издании этой книги, но сейчас они являются стандартной частью модуля Math
.
5.24. Вычисление логарифмов по произвольному основанию
Чаще всего мы пользуемся натуральными логарифмами (по основанию е, часто натуральный логарифм обозначается как ln), иногда также десятичными (по основанию 10). Эти функции реализованы в методах Math.log
и Math.log10
соответственно.
В информатике, а в особенности в таких ее областях, как кодирование и теория информации, обычно применяются логарифмы по основанию 2. Например, так вычисляется минимальное число битов, необходимых для представления числа. Определим функцию с именем log2
:
def log2(x)
Math.log(x)/Math.log(2)
end
Ясно, что обратной к ней является функция 2**x
(как обращением ln x
служит Math::Е**x
или Math.exp(x)
).
Эта идея обобщается на любое основание. В том маловероятном случае, если вам понадобится логарифм по основанию 7, можно поступить так:
def log7(x)
Math.log(x)/Math.log(7)
end
На практике знаменатель нужно вычислить один раз и сохранить в виде константы.