#ruby #matrix-multiplication
#ruby #матрица-умножение
Вопрос:
Не могли бы вы мне помочь?
Я решаю упражнение на Ruby, и результат должен быть таким:
it 'multiplication table de 1 a 10' do
expect(ArrayUtils.tabuada(10)).to eq [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
[4, 8, 12, 16, 20, 24, 28, 32, 36, 40],
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
[6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
[7, 14, 21, 28, 35, 42, 49, 56, 63, 70],
[8, 16, 24, 32, 40, 48, 56, 64, 72, 80],
[9, 18, 27, 36, 45, 54, 63, 72, 81, 90],
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
]
end
it 'multiplication table de 1 a 3' do
expect(ArrayUtils.tabuada(3)).to eq [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
]
end
Мой код:
def self.tabuada(n)
(1..n).each do |element|
(1..n-1).each { |item| print "#{element * item}, " }
puts element * n
end
end
tabuada(3)
Это результат:
1, 2, 3
2, 4, 6
3, 6, 9
Есть предложения?
Комментарии:
1. В чем ваш вопрос здесь? Вам дана спецификация, и вам нужно реализовать метод?
Ответ №1:
В вашем коде есть две проблемы:
- Тест ожидает, что вы вернете вложенный массив, а не напечатаете результат
- Внутренний цикл всегда заканчивается a
10
, он вообще не зависит отn
Я бы начал с чего-то вроде этого:
def self.tabuada(n)
(1..n).map do |n|
(1..10).map do |i|
n * i
end
end
end
Или:
def self.tabuada(n)
(1..n).map { |element| Array.new(10) { |i| (i 1) * element } }
end
Ответ №2:
Хорошие сообщения об ошибках очень важны для хорошей платформы тестирования.
Сообщения об ошибках должны привести вас к решению.
Похоже, вы используете RSpec, который действительно имеет хорошие сообщения об ошибках. Итак, давайте просто посмотрим на сообщение об ошибке, которое мы получаем:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: 1..10
(compared using ==)
Итак, сообщение об ошибке сообщает нам, что RSpec ожидал найти массив, но вместо этого ваш метод вернул диапазон. Это интересно, поэтому давайте посмотрим, что возвращает ваш метод.
Ну, в отсутствие явного return
ключевого слова тело определения метода вычисляется до значения последнего выражения, которое было вычислено внутри тела определения метода. В вашем случае внутри метода есть только одно выражение, поэтому возвращается значение этого выражения:
(1..n).each do |element|
(1..n-1).each { |item| print "#{element * item}, " }
puts element * n
end
Какое возвращаемое значение (1..n).each
? Мы можем просто посмотреть документацию, и мы видим, что Range#each
возвращает self
, то есть объект, к которому он был вызван. Таким образом, возвращаемым значением вашего метода всегда будет просто диапазон 1..n
, а не массив массивов, который ожидает тест.
[Sidenote: на самом деле я не смотрел, что Range#each
возвращает. Каждая реализация each
для каждой коллекции всегда возвращает self
, что является частью контракта each
. Это одна из вещей, которые вы просто узнаете со временем, когда программируете на Ruby.]
Давайте сделаем самую простую вещь, которую мы можем сделать, чтобы изменить сообщение. Это все, что мы хотим сделать. Мы не хотим все исправлять и не хотим делать огромный шаг. Мы просто хотим внести небольшое изменение, которое изменит сообщение об ошибке.
Если мы посмотрим на доступные нам методы, мы найдем метод Enumerable#map
, который преобразует элементы и возвращает массив преобразованных элементов. На самом деле это звучит неплохо, поскольку преобразование некоторых чисел в таблицу умноженных чисел — это в значительной степени именно то, что мы хотим сделать.
Итак, мы просто меняем значение each
на map
. Ничего больше:
(1..n).map do |element|
(1..n-1).each { |item| print "#{element * item}, " }
puts element * n
end
And then we see what happens:
got: [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
That is already better. We are expecting an array, and we are getting an array. Just the wrong array, but we already have the correct type! Before, we were getting a range, now we are getting an array.
Why is this array filled with nil
s, though? We want numbers! If we look at the documentation of map
again, we see that the value of the block is used to fill the array. So, what is the value of the block?
Just as above, the value of the whole block is the value of the last expression evaluated inside the block. The last expression inside the block is
puts element * n
And if we look at the documentation of Kernel#puts
, we can see that it does, indeed, return nil
! But we want a number instead. Well, we already have a number: element * n
is a number! So, instead of printing the number (which the problem description never asked for in the first place) and returning nil
, let’s just return the number. In other words, just remove the puts
:
(1..n).map do |element|
(1..n-1).each { |item| print "#{element * item}, " }
element * n
end
And this is the result:
got: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
Again, one step closer. We want an array, we have an array. We want numbers, we have numbers. What we actually want, though is not an array of numbers, but an array-of-arrays-of-numbers. If you look closely, you can see that we have here is actually the last array of the array-of-arrays-of-numbers we are expecting (or to put it another way, it is the last row of the table).
Hey, but we already know something that can produce arrays: map
can do that! Turning the outer each
into a map gave us an array. Naturally, nesting a map
within a map
will give us nested arrays. So, let’s just do that:
(1..n).map do |element|
(1..n-1).map { |item| print "#{element * item}, " }
element * n
end
Hmm … actually, that didn’t change anything. It makes sense, really: the value of the block is the value of the last expression, and the last expression is element * n
. The map
before it does return an array, but we’re not doing anything with that array, we are just throwing it away.
So, let’s just as an experiment remove the element * n
and see what happens:
got: [[nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, ... nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil]]
One step forward, one step back, I guess. We wanted an array-of-arrays-of-numbers, originally we had an array-of-numbers and were missing the «nesting» part, now we have a nested array-of-arrays, but we lost the numbers.
But we already know the cause, since we figured it out once before: Kernel#print
returns nil
, so let’s just delete it:
(1..n).map do |element|
(1..n-1).map { |item| "#{element * item}, " }
end
And this is the result:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: [["1, ", "2, ", "3, ", "4, ", "5, ", "6, ", "7, ", "8, ", "9, "], ["2, ", "4, ", "6, ", "8, ", "10, "..., "63, ", "72, ", "81, "], ["10, ", "20, ", "30, ", "40, ", "50, ", "60, ", "70, ", "80, ", "90, "]]
Hey, that’s actually pretty close! We want an array-of-arrays-of-numbers, and we have an array-of-arrays-of-strings-with-numbers-in-them. Let’s see what happens when we remove the string and simply keep the number:
(1..n).map do |element|
(1..n-1).map { |item| element * item }
end
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2, 4, 6, 8, 10, 12, 14, 16, 18], [3, 6, 9, 12, 15, 18, 21, 24, 27], [4... 32, 40, 48, 56, 64, 72], [9, 18, 27, 36, 45, 54, 63, 72, 81], [10, 20, 30, 40, 50, 60, 70, 80, 90]]
Круто! Мы считаем только один слишком низкий. Это потому, что у нас изначально был особый случай для последнего числа, и мы просто удалили этот особый случай. Как мы теперь видим, на самом деле нет причин для особого случая, поэтому мы можем просто включить последнее число в наш обычный поток кода:
(1..n).map do |element|
(1..n).map { |item| element * item }
end
Поздравляем! Первый из двух тестов пройден! Теперь нам нужно беспокоиться только о втором тесте, который выдает эту ошибку:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]]
got: [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
О, я вижу, что здесь происходит. В первом тесте таблица умножения просто оказалась квадратной, поэтому мы внедрили квадратную таблицу умножения. Но на самом деле, строки всегда должны иметь ширину в 10 столбцов, независимо от n
, тогда как мы просто предположили, что они были n
широкими, потому что в первом тесте n
так и было 10
.
Но это легко исправить:
(1..n).map do |element|
(1..10).map { |item| element * item }
end
И теперь все тесты пройдены!
Вы заметите, что теперь мы почти не получаем выходных данных. Это стандартная философия программирования: если все идет хорошо, ничего не печатайте. Печатайте что-то только тогда, когда что-то не так. Если вы печатаете слишком много, когда все идет хорошо, будет трудно определить, когда что-то идет не так. Кроме того, вы снижаете чувствительность своих пользователей, и они просто начинают игнорировать то, что напечатано, потому что это им не интересно.
Вот почему RSpec по умолчанию выводит только одну зеленую точку для каждого пройденного теста и однострочную сводку в конце и печатает большие сообщения только тогда, когда что-то идет не так:
..
Finished in 0.00701 seconds (files took 0.15231 seconds to load)
2 examples, 0 failures
Конечно, есть много других способов написать это. Я хотел показать вам, как вы могли бы исправить то, что вы уже написали, не делая ничего, кроме слепого следования превосходным сообщениям об ошибках RSpec и просто выполняя крошечные шаги, чтобы изменить сообщение об ошибке на что-то другое. Вы даже не пытаетесь исправить ошибку, вы только пытаетесь изменить ошибку на что-то, что ближе к вашей цели.
На самом деле нам не нужно было много думать. Ошибки в значительной степени подсказывали нам, что делать. Это признак хороших сообщений об ошибках и хороших тестов.
Вы даже можете сделать это, когда у вас вообще нет кода! Существует методология, называемая «Разработка, основанная на тестировании» (TDD), которая основана на идее, что вы пишете тесты перед тем, как написать первую строку кода, а затем разрабатываете свой код, просто «прослушивая ошибки».
В этом случае это может выглядеть примерно так. Мы начинаем с пустого файла и получаем эту ошибку:
NameError:
uninitialized constant ArrayUtils
Помните, мы только пытаемся сделать «простейшую возможную вещь, чтобы изменить сообщение об ошибке». В сообщении об ошибке говорится, что ArrayUtils
константа не инициализирована, поэтому мы просто инициализируем ее, не более того:
ArrayUtils = nil
NoMethodError:
undefined method `tabuada' for nil:NilClass
Хорошо, теперь он сообщает нам, что NilClass
у него нет вызываемого метода tabuada
. Все, что мы делаем, это самую глупую вещь, чтобы изменить сообщение об ошибке: мы добавляем этот метод в NilClass
:
class NilClass
def tabuada
end
end
Это глупо? Да, конечно, это так! Но мы не пытаемся быть умными здесь. Мы, на самом деле, очень стараемся быть не умными. Отлаживать код сложно, сложнее, чем писать код. Это означает, что если вы делаете свой код настолько умным, насколько можете, вы по определению недостаточно умны, чтобы его отлаживать! Итак, давайте пока остановимся на этом.
ArgumentError:
wrong number of arguments (given 1, expected 0)
Хорошо, так что давайте просто дадим ему один:
def tabuada(_)
end
Теперь мы больше не получаем ошибок от Ruby, а вместо этого тестируем сбои от RSpec. Это уже хороший шаг:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: nil
Итак, мы возвращаем неправильную вещь. Мы можем легко исправить это, вернув то, что просит нас тест:
def tabuada(_)
[
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
[4, 8, 12, 16, 20, 24, 28, 32, 36, 40],
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
[6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
[7, 14, 21, 28, 35, 42, 49, 56, 63, 70],
[8, 16, 24, 32, 40, 48, 56, 64, 72, 80],
[9, 18, 27, 36, 45, 54, 63, 72, 81, 90],
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
]
end
This passes the first test, obviously, but not the second. Now, we could add a conditional expression like this:
def tabuada(_)
if _ == 10
[
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
[4, 8, 12, 16, 20, 24, 28, 32, 36, 40],
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
[6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
[7, 14, 21, 28, 35, 42, 49, 56, 63, 70],
[8, 16, 24, 32, 40, 48, 56, 64, 72, 80],
[9, 18, 27, 36, 45, 54, 63, 72, 81, 90],
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
]
else
[
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
]
end
end
However, it should be clear that this is not the spirit of the problem. If there were more tests, we would have to add a new clause for every number. In particular, this should be working for infinitely many numbers.
So, we’ll have rethink our approach. Let’s go back. All we need is an array, so let’s start with an array:
def tabuada(_)
[]
end
As a first step, our array needs to have as many rows as the argument. If you want to create an array with a certain number of elements, you can use Array::new
:
def tabuada(_)
Array.new(_)
end
got: [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
The length is good. But the contents are not. The contents should also be an array:
def tabuada(_)
Array.new(_) { Array.new(10) }
end
got: [[nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, ni...l, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]]
That’s great, the result array already has the shape we want, now it’s just missing the content. Array::new
yields the index to the block, so we can use that to create the content:
def tabuada(_)
Array.new(_) { |a| Array.new(10) { |b| a * b }}
end
got: [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 2, 4, 6, 8, 10, 12, 14, 16, 18],...35, 42, 49, 56, 63], [0, 8, 16, 24, 32, 40, 48, 56, 64, 72], [0, 9, 18, 27, 36, 45, 54, 63, 72, 81]]
We are very close now! As I said above, Array::new
yields the index, but Ruby array indices range from 0 to size-1, not 1 to size, so all that is left to do is add 1 to the index, or alternatively, use the successor:
def tabuada(_)
Array.new(_) { |a| Array.new(10) { |b| a.succ * b.succ }}
end
And now, all of our tests pass!
Now that we have passing tests comes a very important step: Refactoring. Refactoring means:
- While the tests are passing,
- in small, well-defined, reversible steps,
- change the structure of the code without changing its behavior
- to make it look like you had the whole design figured out from the beginning.
Our first refactoring is going to be the Rename Parameter Refactoring. When we introduced the parameter, we were ignoring it, so we gave it the standard name for an ignored parameter: _
. But that is a terrible name, so we will rename it to n
:
def tabuada(n)
Array.new(n) { |a| Array.new(10) { |b| a.succ * b.succ }}
end
И мы снова запускаем наши тесты, чтобы убедиться, что мы ничего не нарушили.
Нашим следующим рефакторингом будет рефакторинг метода перемещения. Мы очень глупо последовали за сообщением об ошибке, в котором говорилось, что не удалось найти метод tabuada
on nil
, поэтому мы сделали простейшую возможную вещь и добавили tabuada
to nil
. Но это не имеет особого смысла.
Вместо этого мы хотим добавить ее в ArrayUtils
, которая на тот момент просто была nil
.
Это означает, что сначала нам нужно перейти ArrayUtils
на что-то другое. Большинство программистов на Ruby, вероятно, использовали бы a module
, но для чего-то, что не будет смешиваться и использоваться только как контейнер для одноэлементных методов, я лично предпочитаю empty BasicObject
. Итак, давайте сделаем это, а затем переместим метод к нему:
ArrayUtils = BasicObject.new
def ArrayUtils.tabuada(n)
Array.new(n) {|a| Array.new(10) {|b| a.succ * b.succ } }
end
Мы можем немного сократить это до чего-то вроде этого:
def (ArrayUtils = BasicObject.new).tabuada(n)
Array.new(n) {|a| Array.new(10) {|b| a.succ * b.succ } }
end
Обратите внимание, что в обоих случаях, в первом примере, где мы начали с вашего сбоящего кода и исправили его, и во втором примере, где мы начали с пустого файла, мы всегда работали небольшими простыми шагами:
- Прочитайте сообщение об ошибке.
- Поймите ошибку.
- Внесите в код самое простое, наименьшее и глупое изменение, чтобы изменить только один аспект сообщения об ошибке.
- Прочитайте новое сообщение об ошибке.
- Поймите ошибку.
- Убедитесь, что новая ошибка является шагом вперед или, по крайней мере, в сторону.
- Если нет, отмените изменение и попробуйте что-то другое.
- В противном случае перейдите к # 3 и повторяйте, пока тесты не пройдут.
Только после прохождения тестов: реорганизуйте, чтобы все выглядело так, как будто мы знали, куда идем с самого начала.
Тесты гарантируют, что мы всегда движемся вперед. Небольшие шаги гарантируют, что мы всегда понимаем, что делаем, и когда что-то идет не так, мы знаем, что проблема может быть только в крошечном фрагменте кода, который мы изменили. Они также гарантируют, что, когда нам нужно вернуться, мы теряем всего пару секунд работы.