Support

If you have a problem or need to report a bug please email : support@dsprobotics.com

There are 3 sections to this support area:

DOWNLOADS: access to product manuals, support files and drivers

HELP & INFORMATION: tutorials and example files for learning or finding pre-made modules for your projects

USER FORUMS: meet with other users and exchange ideas, you can also get help and assistance here

NEW REGISTRATIONS - please contact us if you wish to register on the forum

To the rescue! A Ruby tip.

For general discussion related FlowStone

To the rescue! A Ruby tip.

Postby tulamide » Tue Sep 07, 2021 3:43 pm

In a recent conversation with Spogg I mentioned a functionality in Ruby, of which he thinks not many may know and therefore asked me to write a post about it.

When writing code, it is inevitable to cause an error at some point. Any higher language can catch such errors, which means, they are brought to the programmer's attention, before they can do any harm. Those are called "exceptions" and the process of dealing with exceptions "exception handling".

As any other high level language, Ruby also has a mechanism for exception handling. It is very convenient, flexible and at the same time very easy to use.

Whenever there is critical code, of which you must ensure that it runs no matter what, the standard behavior of Ruby will prevent that, because, if an exception is raised, Ruby tells you about the error in the info pane and then stops execution.

A better way is to use the "begin end" structure. Place the code section, that is critical, in this block. As an example, we will provoke a TypeError.

Code: Select all
ary = [] #creates an array class instance
number = ary + 3

Immediately, Ruby tells you that it can't convert a fixnum (3) into an array, and then stops execution. Now we have a mess, because the variable number was already instantiated, before the exception was raised, but because the execution was stopped, it is now of NilClass. Not at all what we need! A number is, what the rest of the program expects.

Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue
end

With this simple structure, we already managed one big step: Ruby does not print the error to the info pane and stops, but continues to run. However, under rescue, we have no code. We don't handle the exception! number is still an instance of NilClass.

The rescue keyword is important. Every exception that is raised within the begin block, will be routed to it, instead of Ruby's standard behavior. We need a way to handle the exceptions, and that is done with the exception class. It will allow us to handle an exception based on its type. Ruby already provides a fine list of exceptions that are bundled under StandardError. When we tell rescue to catch StandardError, it will catch ALL standard errors that are defined, like TypeError, NameError, ZeroDivisionError, and so many more. If you write rescue with no exception specification, Ruby assumes StandardError automatically. The following codes are the same in functionality
Code: Select all
begin
rescue
end

Code: Select all
begin
rescue StandardError
end


Let's use our rescue block now. In the simplest form, we just notify ourselves of the exception. But Ruby does that already without writing a begin end block. The next simple form would be to give a default value out, when an exception arrives.
Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue
  number = 0
end

That's better. Number is now 0 instead of nil, and the rest of the program can deal with that. Ruby continues to run flawlessly.

But there's more. Sometimes you don't want to catch ALL exceptions, as following code might already deal with some of them. In that case we just select a subclass of StandardError. In our example, it would be a TypeError, we're interested in.
Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue TypeError
  number = 0
end

Now only a type error will be caught and dealt with, but all others be raised. And you can go into much deeper detail. You can specify more than one exception to be caught in a rescue line.
Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue TypeError, NameError
  number = 0
end

Now our rescue block will react to Type and Name errors. Often times, different exceptions need different handling. No problem. Just add another rescue line (actually as many as you need).
Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue TypeError, NameError
  number = 0
rescue ZeroDivisionError
  #some code to prevent division by zero#
end

But you didn't think that was it, did you? When rescuing an exception, it might be possible to run the code, that failed before! Therefore you use the keyword retry.
Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue TypeError
  ary = 0
  retry #since we changed the array to a number, the code "number = ary + 3" will work now.
end

But that still isn't it. Sometimes you need to make sure that some code is run under all circumstances. It could be difficult to repeat the code in all rescue blocks. And so there is another helpful keyword: ensure. It does exactly what it says. It ensures that whatever code is under it, will be executed, EVEN IF AN EXCEPTION WAS NOT HANDLED BY OUR RESCUE CODES! This is important to understand. Ensure will run, even if Ruby has to stop after an unhandled exception. This is why many Ruby programmers use begin end to open files. It is dangerous and even destructive to open files, but not closing them after use. But an unhandled exception will lead to exactly that. It can be avoided with ensure, like so.
Code: Select all
f = File.open("somefile")
#the file is now open and ready to be used#
begin
  #code that works with the file#
rescue
  #code that handles errors#
ensure
  f.close unless f.nil?
  #if f is nil, the file is unavailable, else close f#
end

If now an unhandled exception is raised, the file will be closed, before Ruby stops. However, ensure will always be executed, even if there was no exception, or if all exception were handled. That means, you have to make sure, that the code under ensure will not be executed elsewhere in the begin end block! In this example the file should not be closed in the rescue block!
I still have more for you. Sometimes, instead of just defining which exceptions to catch, you may want to access the exception in the rescue block (especially useful with your own exception, see below). You do that by referencing a local variable.
Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue => exc #works also with "rescue TypeError => exc" etc., but is redundant
  watch "I caught this", exc.message
  number = 0
end


Last but not least, as well as handling exceptions, you can also raise one! This is helpful for various reasons, for example to re-raise an exception that was already handled. Some higher layer code might need to be informed of the error, even if it was handled.
Code: Select all
ary = [] #creates an array class instance
begin
  number = ary + 3
rescue TypeError, NameError
  number = 0
rescue ZeroDivisionError
  number = 0
  raise
end

raise without any argument in the rescue block will re-raise the same eexception, here a ZeroDivisionError.
Code: Select all
a = 0
begin
  number = 6 / a
rescue TypeError, NameError
  number = 0
rescue ZeroDivisionError
  number = 6
  raise "You idiot once again tried to divide by zero! Will you ever learn?"
end

Here you raise a RuntimeError with the message you can read there. You can also create your own exceptions that you can then raise. Exceptions are classes, just like everything else in Ruby. Just make sure to inherit from StandardError
Code: Select all
class IdiotError < StandardError
  def initialize(msg="You will never learn! Divided by Zero again!")
    super
  end
end

a = 0
begin
  b = 3 / a
rescue
  raise IdiotError
end

You can overwrite the default message as well.
Code: Select all
class IdiotError < StandardError
  def initialize(msg="You will never learn! Divided by Zero again!")
    super
  end
end

a = 0
begin
  b = 3 / a
rescue
  raise IdiotError, "OK, I was a bit harsh. But please stop dividing by Zero!"
end


I probably forgot about something. But this will give you a start into exception handling. Have fun programming!
"There lies the dog buried" (German saying translated literally)
tulamide
 
Posts: 2686
Joined: Sat Jun 21, 2014 2:48 pm
Location: Germany

Re: To the rescue! A Ruby tip.

Postby RJHollins » Tue Sep 07, 2021 6:02 pm

Excellent Help Tutorial !

Thanks Tulamide 8-)
RJHollins
 
Posts: 1567
Joined: Thu Mar 08, 2012 7:58 pm

Re: To the rescue! A Ruby tip.

Postby tulamide » Tue Sep 07, 2021 9:11 pm

And here is an actual example of how rescue and the StandardError method :backtrace helped me to find a pesky, rare issue.
rescue and trace.png
rescue and trace.png (24.2 KiB) Viewed 9921 times


I used raise here to let you see the original error message I got. It made no sense. "Hey, in the mouseMoveMethod you tried to compare a float with a float!"

It also shows, what I forgot about in the first post. When having a method, you can add a rescue block at the end. The whole method definition then acts like a begin, rescue, end block.

Back to the error. In rescue I accessed the exception via local variable and called its method :backtrace. This is -simply spoken- like a raw version of the message, that was constructed. It gives me a litle bit more detail, as it tells me that actually the error is not in the method itself, but in the sort method, that I called in line 77! Now that I knew it must have been something that made the sort comparison impossible, I could check precisely for the objects used for sorting. Obviously 0.0 and 1.0 are valid, and for reasons I omit here, @angle was also valid all the time. So it could only be @y_delta. But I still didn't know why. So I placed some watch methods, to see the content of the object. In rare cases, @y_delta can become so small, that it can't be represented with a float anymore. It becomes undefined, and the description for that is NaN ("not a number"). Comparing any float with NaN is an unordered result and must therefore raise an exception in a sorting method. However, the designer of the message belonging to the ArgumentError did a bad job by just showing the classes. At that point @y_delta was already undefined, so it should have shown "comparison of NaN with Float failed". Since NaN is not a class, but a value in Ruby, it slipped through.

The solution is now very easy. The FloatClass has a method :nan?, which returns true if the value is NaN, and I just need to check for NaN (and replace it) before passing it to the sort algorithm.
"There lies the dog buried" (German saying translated literally)
tulamide
 
Posts: 2686
Joined: Sat Jun 21, 2014 2:48 pm
Location: Germany

Re: To the rescue! A Ruby tip.

Postby Spogg » Wed Sep 08, 2021 7:13 am

Oh wow tulamide! :o
When I requested that you post the info, I had no idea of how much work it would cause you. I’m personally very grateful for you taking the time to do this for us.

BTW I somehow suspect this code is for a knob. I must be psychic :lol:
User avatar
Spogg
 
Posts: 3318
Joined: Thu Nov 20, 2014 4:24 pm
Location: Birmingham, England

Re: To the rescue! A Ruby tip.

Postby billv » Wed Sep 08, 2021 9:41 am

Great tutorial.. :D ...thanks Tulamide
billv
 
Posts: 1141
Joined: Tue Aug 31, 2010 3:34 pm
Location: Australia


Return to General

Who is online

Users browsing this forum: No registered users and 43 guests