Home > Net >  Learning Ruby. Need to return an array of length n, with a given number x and then squares of the pr
Learning Ruby. Need to return an array of length n, with a given number x and then squares of the pr

Time:03-23

This is the problem

Complete the function that returns an array of length n, starting with the given number x and the squares of the previous number. If n is negative or zero, return an empty array/list.

Examples
2, 5  -->  [2, 4, 16, 256, 65536]
3, 3  -->  [3, 9, 81]

Seems easy enough for what Ive been learning.

Ive completed the code for the most part as it completes the examples:

def squares(x, n)
array = [x]

i = 1
  while i < n
  array << x *= x

    i  = 1

  end

  return array
end
PASS Test.assert_equals(squares(2,5),[2,4,16,256,65536]);  
PASS Test.assert_equals(squares(3,3),[3,9,81]);  
PASS Test.assert_equals(squares(5,3),[5,25,625]);  
PASS Test.assert_equals(squares(10,4),[10,100,10000,100000000]);

The trouble Im having is, the problem also calls for n to be a positive integer, and if not, return an empty array:

If n is negative or zero, return an empty array/list.

Im having difficulty figuring out how to correctly go about this. Ive tried several different ways, with no success. This is an example of one of my attempts where I thought I was on the right track:

def squares(x, n)
array = [x]
arr = []

i = 1
  if n < 0
    return arr
  elsif i < n
    array << x *= x
  end

    i  = 1
 end
end

CodePudding user response:

One way is to create an enumerator and then take the required number of elements:

def squares(x, m)
  Enumerator.produce(x) { |n| n*n }.take(m)
end
squares(2, 5)
  #=> [2, 4, 16, 256, 65536]
squares(3, 5)
  #=> [3, 9, 81, 6561, 43046721]

See Enumerator::produce.

CodePudding user response:

Just adding the following line at the top of your method will solve it:

return [] if n <= 0

Though there are many other ways to do this. For example:

def squares(x, n)
  a = x
  (1..n).map { |e| a.tap { a *= a } }
end

This relies on the fact that Ruby ranges of form m..n produce no elements if n <= m.

CodePudding user response:

First off, I would like to say that the question is stupid. Not your question, I am talking about the problem statement:

Complete the function that returns an array of length n, starting with the given number x and the squares of the previous number. If n is negative or zero, return an empty array/list.

There are several things wrong with it.

  1. It requires you to return an Array. In programming, you should always strive to make your return values as generic as possible, so that your subroutine can be re-used in as many different contexts as possible. If your subroutine returns an Array and n is very large, then the Array will use a lot of memory. However, if someone just wants to iterate over all of those values, for example, there is no need for an Array. It would make much more sense to return what is commonly called a stream or what Ruby calls an Enumerator. A client can easily convert an Enumerator to an Array if they really need an Array.
  2. The same applies to the requirement to return a pre-determined number of elements n. It would make much more sense to return an infinite number of elements and let the client pick the number they need. For example, what if they don't know the number of elements? What if they want to sum those numbers until they reach a certain threshold? With the requirement from the problem statement, they would have to guess how many numbers they need in order to reach that threshold, whereas if you give them an infinite stream, they can just iterate the stream until they have found their answer.
  3. This is one of my pet peeves: the problem statement talks about functions, but Ruby doesn't have functions. It has methods. Those are two different things.
  4. It also treats arrays and lists as if they were the same thing. They are not.
  5. [I'll put this one in parentheses, because it can be argued either way. I think it would make more sense to raise an Exception, more precisely an ArgumentError for a negative n instead of returning an empty result. The empty result makes sense for n == 0, but not necessarily for negative n.]

So, in short, the way the problem statement is worded promotes bad Ruby terminology (function, list) and bad programming practices (too specific return value, not properly signaling errors).

Anyway, back to your question.

The trouble Im having is, the problem also calls for n to be a positive integer, and if not, return an empty array:

If n is negative or zero, return an empty array/list.

As mentioned in some of the other answers, you can almost literally translate that statement to Ruby code:

# "If n is negative":
if n.negative?

# " …or zero":
if n.negative? || n.zero?

# "… return an empty array":
if n.negative? || n.zero? then return [] end

Now we can optimize this a little bit. First off, if you have a conditional expression with no else branch and only a single expression in the then branch, you can use the so-called modifier form:

return [] if n.negative? || n.zero?

Secondly, "negative or zero" is just a different way of saying "not positive", which allows us to simplify the condition:

return [] if !n.positive?

We can make this more readable by using the inverted form of the conditional expression with unless:

return [] unless n.positive?

This satisfies the requirement in the problem statement, however, as I mentioned, I personally think passing a negative length should be an error, so I would probably rather write something like this:

raise ArgumentError, "length must not be negative but you passed `#{n}`" if n.negative?
return [] if n.zero?

As I mentioned above, though, the way the problem statement forces you to write the code is not how you would actually write it in the real world. In the real world, you would decompose the problem into various orthogonal components, and make sure that each of those components can also be used separately.

The reason is that "a sequence of squares of a specific length" is a very specific problem, which it makes it very unlikely that someone else is going to have the exact same problem, and thus makes it unlikely that your code can be re-used.

I would decompose this problem into at least these subproblems:

  1. Produce an infinite stream given a subroutine to produce the next element.
  2. Take a specified number of elements from an infinite stream.
  3. Square a number.

If you have solved these three subproblems, you can solve the specific problem using the solutions by (1) producing an infinite stream of (3) squares and (2) taking only the first n elements.

What are the advantages of this approach? I see two major ones:

  • You have broken the problem down into simpler subproblems: each of those three subproblems is simpler than the original problem. Therefore, each of the three problems is easier to solve than the original one. Once you have solved the three subproblems, the original problem also becomes easy to solve because you just have to plug the three subproblems together.
  • You have built a library of three general solutions that are useful beyond the original problem. You can use these to solve other problems as well.

In fact, I have hidden the most important benefit from you: remember how I said multiple times that making the problem more general helps make the solution more reusable, so that the solution becomes useful in more contexts? Well, it turns out that #1 and #2 are so general and so useful in so many contexts, that they have already been written for us! The solutions to #1 and #2 are part of the Ruby core library, so we don't even need to write them ourselves.

You can produce an infinite Enumerator using the method Enumerator::produce, which is Ruby's name for an unfold aka Anamorphism. And you can take a specified number of elements from an Enumerable using Enumerable#take. So, all that's left for us to solve here is how to square a number, which is trivial.

You will also note that Enumerable#take returns an empty Array when you pass 0 as the number of elements and raises an ArgumentError when you pass a negative number, so by decomposing our problem and delegating the solution of sub-problem #2 to Enumerable#take we also get the error and edge case behavior we want for free.

You can already see in Cary Swoveland's answer what the resulting code looks like, so I will not repeat it here. Rather, I want to show what I meant at the very beginning when I said that returning an infinite stream of squares would be more useful because the client could then apply their own criterion for how many elements to take. Remember the problem I posed:

What if they want to sum those numbers until they reach a certain threshold? With the requirement from the problem statement, they would have to guess how many numbers they need in order to reach that threshold, whereas if you give them an infinite stream, they can just iterate the stream until they have found their answer.

If we write our method like this:

def infinite_stream_of_squares_starting_with(initial_value)
  Enumerator.produce(initial_value) { _1 * _1 }
end

Then all the client has to do is to replace Enumerable#take (which allows them to take a specific number of elements) with Enumerable#take_while (which allows them to take elements while a specific condition is met), and they can write:

def squares_until_sum_reaches_threshold(initial_value, threshold)
  sum = 0
  infinite_stream_of_squares_starting_with(initial_value).
    take_while { (sum  = _1) < threshold }
end

So, in summary, it is always a good idea to break down problems into subproblems and generalize those subproblems, because breaking the problem down makes it simpler, and generalizing makes it both more likely to be useful in other contexts, and more likely to already have been solved by someone else. In particular, you should always separate I/O from computation, and try to separate generating data, transforming data, filtering data, and reducing data from each other.

  • Related