Pré-visualização50 páginas

# => Complex(3.0, 4.25) b + 1.5*Complex::I # => Complex(1.5, 5.75) a - b # => Complex(-0.5, -0.25) a * b # => Complex(-15.5, 10.25) b.conjugate # => Complex(1.5, -4.25) Math::sin(b) # => Complex(34.9720129257216, 2.47902583958724) Discussion You can use two floating-point numbers to keep track of the real and complex parts of a complex number, but that makes it complicated to do mathematical operations such as multiplication. If you were to write functions to do these operations, you\u2019d have more or less reimplemented the Complex class. Complex simply keeps two instances of Numeric, and implements the basic math operations on them, keeping them together as a complex number. It also implements the complex-specific mathe- matical operation Complex#conjugate. Complex numbers have many uses in scientific applications, but probably their coolest application is in drawing certain kinds of fractals. Here\u2019s a class that uses complex numbers to calculate and draw a character-based representation of the Mandelbrot set, scaled to whatever size your screen can handle. class Mandelbrot # Set up the Mandelbrot generator with the basic parameters for # deciding whether or not a point is in the set. 68 | Chapter 2: Numbers def initialize(bailout=10, iterations=100) @bailout, @iterations = bailout, iterations end A point (x,y) on the complex plane is in the Mandelbrot set unless a certain iterative calculation tends to infinity. We can\u2019t calculate \u201ctends towards infinity\u201d exactly, but we can iterate the calculation a certain number of times waiting for the result to exceed some \u201cbail-out\u201d value. If the result ever exceeds the bail-out value, Mandelbrot assumes the calculation goes all the way to infinity, which takes it out of the Mandelbrot set. Otherwise, the itera- tion will run through without exceeding the bail-out value. If that happens, Mandelbrot makes the opposite assumption: the calculation for that point will never go to infinity, which puts it in the Mandelbrot set. The default values for bailout and iterations are precise enough for small, chunky ASCII renderings. If you want to make big posters of the Mandelbrot set, you should increase these numbers. Next, let\u2019s define a method that uses bailout and iterations to guess whether a specific point on the complex plane belongs to the Mandelbrot set. The variable x is a position on the real axis of the complex plane, and y is a position on the imaginary axis. # Performs the Mandelbrot operation @iterations times. If the # result exceeds @bailout, assume this point goes to infinity and # is not in the set. Otherwise, assume it is in the set. def mandelbrot(x, y) c = Complex(x, y) z = 0 @iterations.times do |i| z = z**2 + c # This is the Mandelbrot operation. return false if z > @bailout end return true end The most interesting part of the Mandelbrot set lives between \u20132 and 1 on the real axis of the complex plane, and between \u20131 and 1 on the complex axis. The final method in Mandelbrot produces an ASCII map of that portion of the complex plane. It maps each point on an ASCII grid to a point on or near the Mandelbrot set. If Mandelbrot estimates that point to be in the Mandelbrot set, it puts an asterisk in that part of the grid. Otherwise, it puts a space there. The larger the grid, the more points are sampled and the more precise the map. def render(x_size=80, y_size=24, inside_set="*", outside_set=" ") 0.upto(y_size) do |y| 0.upto(x_size) do |x| scaled_x = -2 + (3 * x / x_size.to_f) scaled_y = 1 + (-2 * y / y_size.to_f) print mandelbrot(scaled_x, scaled_y) ? inside_set : outside_set end puts 2.13 Simulating a Subclass of Fixnum | 69 end end end Even at very small scales, the distinctive shape of the Mandelbrot set is visible. Mandelbrot.new.render(25, 10) # ** # **** # ******** # *** ********* # ******************* # *** ********* # ******** # **** # ** See Also \u2022 The scaling equation, used to map the complex plane onto the terminal screen, is similar to the equations used to scale data in Recipe 12.5, \u201cAdding Graphical Context with Sparklines,\u201d and Recipe 12.14, \u201cRepresenting Data as MIDI Music\u201d 2.13 Simulating a Subclass of Fixnum Problem You want to create a class that acts like a subclass of Fixnum, Float, or one of Ruby\u2019s other built-in numeric classes. This wondrous class can be used in arithmetic along with real Integer or Float objects, and it will usually act like one of those objects, but it will have a different representation or implement extra functionality. Solution Let\u2019s take a concrete example and consider the possibilities. Suppose you wanted to create a class that acts just like Integer, except its string representation is a hexadeci- mal string beginning with \u201c0x\u201d. Where a Fixnum\u2019s string representation might be \u201c208\u201d, this class would represent 208 as \u201c0xc8\u201d. You could modify Integer#to_s to output a hexadecimal string. This would proba- bly drive you insane because it would change the behavior for all Integer objects. From that point on, nearly all the numbers you use would have hexadecimal string representations. You probably want hexadecimal string representations only for a few of your numbers. This is a job for a subclass, but you can\u2019t usefully subclass Fixnum (the Discussion explains why this is so). The only alternative is delegation. You need to create a class that contains an instance of Fixnum, and almost always delegates method calls to that instance. The only method calls it doesn\u2019t delegate should be the ones that it wants to override. 70 | Chapter 2: Numbers The simplest way to do this is to create a custom delegator class with the delegate library. A class created with DelegateClass accepts another object in its constructor, and delegates all methods to the corresponding methods of that object. require 'delegate' class HexNumber < DelegateClass(Fixnum) # The string representations of this class are hexadecimal numbers. def to_s sign = self < 0 ? "-" : "" hex = abs.to_s(16) "#{sign}0x#{hex}" end def inspect to_s end end HexNumber.new(10) # => 0xa HexNumber.new(-10) # => -0xa HexNumber.new(1000000) # => 0xf4240 HexNumber.new(1024 ** 10) # => 0x10000000000000000000000000 HexNumber.new(10).succ # => 11 HexNumber.new(10) * 2 # => 20 Discussion Some object-oriented languages won\u2019t let you subclass the \u201cbasic\u201d data types like integers. Other languages implement those data types as classes, so you can subclass them, no questions asked. Ruby implements numbers as classes (Integer, with its concrete subclasses Fixnum and Bignum), and you can subclass those classes. If you try, though, you\u2019ll quickly discover that your subclasses are useless: they don\u2019t have constructors. Ruby jealously guards the creation of new Integer objects. This way it ensures that, for instance, there can be only one Fixnum instance for a given number: 100.object_id # => 201 (10 * 10).object_id # => 201 Fixnum.new(100) # NoMethodError: undefined method `new' for Fixnum:Class You can have more than one Bignum object for a given number, but you can only cre- ate them by exceeding the bounds of Fixnum. There\u2019s no Bignum constructor, either. The same is true for Float. (10 ** 20).object_id # => -606073730 ((10 ** 19) * 10).object_id # => -606079360 Bignum.new(10