Book Image

Polished Ruby Programming

By : Jeremy Evans
Book Image

Polished Ruby Programming

By: Jeremy Evans

Overview of this book

Anyone striving to become an expert Ruby programmer needs to be able to write maintainable applications. Polished Ruby Programming will help you get better at designing scalable and robust Ruby programs, so that no matter how big the codebase grows, maintaining it will be a breeze. This book takes you on a journey through implementation approaches for many common programming situations, the trade-offs inherent in each approach, and why you may choose to use different approaches in different situations. You'll start by refreshing Ruby fundamentals, such as correctly using core classes, class and method design, variable usage, error handling, and code formatting. Then you'll move on to higher-level programming principles, such as library design, use of metaprogramming and domain-specific languages, and refactoring. Finally, you'll learn principles specific to web application development, such as how to choose a database and web framework, and how to use advanced security features. By the end of this Ruby programming book, you’ll be a well rounded web developer with a deep understanding of Ruby. While most code examples and principles discussed in the book apply to all Ruby versions, some examples and principles are specific to Ruby 3.0, the latest release at the time of publication.
Table of Contents (23 chapters)
1
Section 1: Fundamental Ruby Programming Principles
8
Section 2: Ruby Library Programming Principles
17
Section 3: Ruby Web Programming Principles

Different numeric types for different needs

Ruby has multiple core numeric types, such as integers, floats, rationals, and BigDecimal, with integers being the simplest type. As a general principle when programming, it's best if you keep your design as simple as possible, and only add complexity when necessary. Applying the principle to Ruby, if you need to choose a numeric type, you should generally use an integer unless you need to deal with fractional numbers.

Note that while this chapter is supposed to discuss core classes, BigDecimal is not a core class, though it is commonly used. BigDecimal is in the standard library, and you need to add require 'bigdecimal' to your code before you can use it.

Integers are the simplest numeric types, but they are surprisingly powerful in Ruby compared to many other programming languages. One example of this is executing a block of code a certain number of times. In many other languages, this is either done with the equivalent of a for loop or using a range, but in Ruby, it is as simple as calling Integer#times:

10.times do
  # executed 10 times
end

One thing that trips up many new Ruby programmers is how division works when both the receiver and the argument are integers. Ruby is similar to C in how integer division is handled, returning only the quotient and dropping any remainder:

5 / 10
# => 0
7 / 3
# => 2

Any time you are considering using division in your code and both arguments could be integers, be aware of this issue and consider whether you would like to use integer division. If not, you should convert the numerator or denominator to a different numeric type so that the division operation will include the remainder:

5 / 10r # or Rational(5, 10) or 5 / 10.to_r
# => (1/2)
7.0 / 3
# => 2.3333333333333335

In cases where your numeric type needs to include a fractional component, you have three main choices, floats, rationals, or BigDecimal, each with its own trade-offs. Floats are fastest but not exact in many cases, as shown in the earlier example. Rationals are exact but not as fast. BigDecimal is exact in most cases, and most useful when dealing with a fixed precision, such as two digits after the decimal point, but is generally the slowest.

Floats are the fastest and most common fractional numeric type, and they are the type Ruby uses for literal values such as 1.2. In most cases, it is fine to use a float, but you should make sure you understand that they are not an exact type. Repeated calculations on float values result in observable issues:

f = 1.1
v = 0.0
1000.times do
  v += f
end
v
# => 1100.0000000000086

Where did the .0000000000086 come from? This is the error in the calculation that accumulates because each Float#+ calculation is inexact. Note that this issue does not affect all floats:

f = 1.109375
v = 0.0
1000.times do
  v += f
end
v
# => 1109.375

This is slightly counter-intuitive to many programmers, because 1.1 looks like a much simpler number than 1.109375. The reason for this is due to the implementation of floats and the fact that computers operate in binary and not in decimal, and 0.109375 can be stored exactly in binary (it is 7/64ths of 1), but 1.1 cannot be stored exactly in binary.

Rationals are slower than floats, but since they are exact numbers, you don't need to worry about calculations introducing errors. Here's the first example using the r suffix to the number so that Ruby parses the number as a rational:

f = 1.1r
v = 0.0r
1000.times do
  v += f
end
v
# => (1100/1)

Here, we get 1100 exactly as a rational, showing there is no error. Let's use the same approach with the second example:

f = 1.109375r
v = 0.0r
1000.times do
  v += f
end
v
# => (8875/8)
v.to_f
# => 1109.375

As shown in the previous example, rationals are stored as an integer numerator and denominator, and inspecting the output reflects that. This can make debugging with them a little cumbersome, as you often need to convert them to floats for human-friendly decimal output.

While rationals are slower than floats, they are not orders of magnitude slower. They are about 2-6 times slower depending on what calculations you are doing. So, do not avoid the use of rationals on a performance basis unless you have profiled them and determined they are a bottleneck (you'll learn about that in Chapter 14, Optimizing Your Library).

A good general principle is to use a rational whenever you need to do calculations with non-integer values and you need exact answers. For cases where exactness isn't important, or you are only doing comparisons between numbers and not calculations that result in an accumulated error, it is probably better to use floats.

BigDecimal is similar to rationals in that it is an exact type in most cases, but it is not exact when dealing with divisions that result in a repeating decimal:

v = BigDecimal(1)/3
v * 3
# => 0.999999999999999999e0

However, other than divisions involving repeating decimals and exponentiation, BigDecimal values are exact. Let's take the first example, but make both arguments BigDecimal instances:

f = BigDecimal(1.1, 2)
v = BigDecimal(0)
1000.times do
  v += f
end
v
# => 0.11e4
v.to_s('F')
# => "1100.0"

So, as you can see, no error is introduced when using repeated addition on BigDecimal, similar to rationals. You can also see that inspecting the output is less helpful since BigDecimal uses a scientific notation. BigDecimal does have the advantage that it can produce human-friendly decimal string output directly without converting the object to a float first.

If we try the same approach with the second example, we can see that it also produces exact results:

f = BigDecimal(1.109375, 7)
v = BigDecimal(0)
1000.times do
  v += f
end
v
# => 0.1109375e4
v.to_s('F')
# => "1109.375"

As both examples show, one issue with using a BigDecimal that is created from floats or rationals is that you need to manually specify the initial precision. It is more common to initialize BigDecimal values from integers or strings, to avoid the need to manually specify the precision.

BigDecimal is significantly slower than floats and rationals for calculations. Due to the trade-offs inherent in BigDecimal, a good general principle is to use BigDecimal only when dealing with other systems that support similar types, such as fixed precision numeric types in many databases, or when dealing with other fixed precision areas such as monetary calculations. For most other cases, it's generally better to use a rational or float.

Of the numeric types, most integer and float values are immediate objects, which is one of the reasons why they are faster than other types. However, large integer and float values are too large to be immediate objects (which must fit in 8 bytes if using a 64-bit CPU). Rationals and BigDecimal are never immediate objects, which is one reason why they are slower.

In this section, you learned about Ruby's many numeric types and how best to use each. In the next section, you'll learn how symbols are very different from strings, and when to use each.