📜 ⬆️ ⬇️

Ruby building blocks

Showing off Ruby's cool features to the uninitiated ( or to the language sparring partner ), an excited rubyist often grabs the “powerful block syntax” in Ruby. Unfortunately, for the Pythonist or Javist, the possibilities of the notorious “powerful block syntax” remain unclear because of the lack of corresponding mechanisms in their languages.

To begin with, we usually refer to Rake, RSpec, or Sinatra as examples of the amazing use of block syntax:

Copy Source | Copy HTML<br/>get "/hello" do <br/> "Hello World" <br/> end <br/>
(see www.sinatrarb.com/intro.html - approx. transl . )

Pythonists usually indicate equivalent syntax in response :
Copy Source | Copy HTML<br/>@get( '/hi' )<br/> def hello ():<br/> return "Hello World" <br/> <br/> def hello () -> "/hi" :<br/> return "Hello World" <br/>

Although the Python version may be inferior in beauty to the Ruby version, but saying “Ruby has more features” is quite difficult. Rubists on the contrary level the argument of great semantic power, reducing it to external beauty when using this example from Sinatra.
')
Rubists, pythonists and other developers working in the field of web development, use a common language JavaScript. Describing blocks to "external" people who own JavaScript 'om, we as an example try to bring its functions. Unfortunately, this only reinforces misunderstanding.

A similar situation is observed by Ruby, when PHP or Java declares “adding closures”, many of us continue to ask “what type of closures?”

Let's get to the point


Let's get to the point and show you the best example of the usefulness of Ruby blocks.
Copy Source | Copy HTML<br/> def append (location, data)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> File . open (path, "a" ) do |file|<br/> file. puts YAML .dump(data)<br/> end <br/> <br/> return data<br/> end <br/>

Method File.open File.open takes a block as a parameter. It opens a new file (in the “append” mode) and transfers the open file to the block. When he finishes, Ruby closes the file. In addition, Ruby does not just close the file, it guarantees that the File will be closed, even if block execution ends with an exception. Let's look at the File implementation in Rubinius :
Copy Source | Copy HTML<br/> def self . open (*args)<br/> io = new *args<br/> <br/> return io unless block_given?<br/> <br/> begin <br/> yield io<br/> ensure <br/> begin <br/> io.close unless io.closed?<br/> rescue StandardError <br/> # nothing, just swallow them. <br/> end <br/> end <br/> end <br/>

This means that you can wrap the ubiquitous idioms like try / catch / finally inside methods.
Copy Source | Copy HTML<br/> # <br/> def append (location, data)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> begin <br/> file = File . open (path, "a" )<br/> file. puts YAML .dump(data)<br/> ensure <br/> file.close<br/> end <br/> <br/> return data<br/> end <br/>

Because Ruby calls ensure even when an exception occurs inside a block, the programmer can be sure that Ruby will execute the terminating logic hidden inside the method.
This example demonstrates the good quality of the implementation of lambda functions. However, blocks in Ruby turn into something completely different due to one little additional feature.
Copy Source | Copy HTML<br/> def write (location, data)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> File . open (path, "w" ) do |file|<br/> return false if Digest::MD5.hexdigest(file.read) == data.hash<br/> file. puts YAML .dump(data)<br/> end <br/> <br/> return true <br/> end <br/>

Imagine that writing data to a disk requires quite a lot of resources, and you can skip writing if the MD5 hash of the file content matches the value of the hash function of the data object. We will return false if the method did not write to the disk and true otherwise.

Ruby blocks support non-local-return ( multiple links ), which means that return from within a block behaves identically to return from the original block context. In this case, a return from inside the block returns from the write method, but Ruby still calls ensure to close the file.

You can think of non-local-return as something like this:
Copy Source | Copy HTML<br/> def write (location, data)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> File . open (path, "w" ) do |file|<br/> raise Return. new ( false ) if Digest::MD5.hexdigest(file.read) == data.hash<br/> file. puts YAML .dump(data)<br/> end <br/> <br/> return true <br/> rescue Return => e<br/> return e.object<br/> end <br/>
where Return is Return = Struct.new(:object) .

Of course, this should be supported by any reasonable implementation of lambda functions, but the Ruby version has the advantage that returning from inside the block looks the same as returning from the usual method, and at the same time it requires much less “brilliance” to achieve the effect. This feature also helps in cases where rescue or ensure already being used, avoiding puzzling combinations.

Ruby supports calling super inside a block. Imagine that the write method was overridden in a subclass, and the same parent class method simply takes the raw data from the file and writes it to the log.
Copy Source | Copy HTML<br/> def write (location, data)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> File . open (path, "w" ) do |file|<br/> file_data = file.read<br/> super (location, file_data)<br/> return false if Digest::MD5.hexdigest(file_data) == data.hash<br/> file. puts YAML .dump(data)<br/> end <br/> <br/> return true <br/> end <br/>

In a pure lambda function script, we would need to store a reference to self, so that we can use it inside the lambda later:
Copy Source | Copy HTML<br/> def write (location, data)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> this = self <br/> File . open (path, "w" ) do |file|<br/> file_data = file.read<br/> <br/> # Ruby <br/> # non-local-super <br/> this. super . write (location, file_data)<br/> raise Return. new ( false ) if Digest::MD5.hexdigest(file_data) == data.hash<br/> file. puts YAML .dump(data)<br/> end <br/> <br/> return true <br/> rescue Return => e<br/> return e.object<br/> end <br/>

In Ruby, you can also call yield on a block obtained by a method inside another block. Imagine that the write method is invoked with a block that chooses which data to use depending on whether the file is executable:
Copy Source | Copy HTML<br/> def write (location)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> File . open (path, "w" ) do |file|<br/> file_data = file.read<br/> super (location)<br/> data = yield file<br/> return false if Digest::MD5.hexdigest(file_data) == data.hash<br/> file. puts YAML .dump(data)<br/> end <br/> <br/> return true <br/> end <br/>

This can be called via:
Copy Source | Copy HTML<br/>write( "/path/to/file" ) do |file|<br/> if file.executable?<br/> "#!/usr/bin/env ruby\nputs 'Hello World!'" <br/> else <br/> "Hello World!" <br/> end <br/> end <br/>

In a pure lambda language, we would take a block as a normal function argument and call it inside a closure:
Copy Source | Copy HTML<br/> def write (location, block)<br/> path = Pathname . new (location)<br/> raise "Location does not exist" unless path.exist?<br/> <br/> this = self <br/> File . open (path, "w" ) do |file|<br/> file_data = file.read<br/> <br/> # Ruby, <br/> # non-local-super <br/> this. super . write (location, file_data)<br/> data = block. call (file)<br/> raise Return. new ( false ) if Digest::MD5.hexdigest(file_data) == data.hash<br/> file. puts YAML .dump(data)<br/> end <br/> <br/> return true <br/> rescue Return => e<br/> return e.object<br/> end <br/>

The real advantage of the Ruby approach is that the code inside the block would be identical if the method did not accept the block. Consider the same method, taking File instead of location:
Copy Source | Copy HTML<br/> def write (file)<br/> file_data = file.read<br/> super (file)<br/> data = yield file<br/> return false if Digest::MD5.hexdigest(file_data) == data.hash<br/> file. puts YAML .dump(data)<br/> return true <br/> end <br/>

Without a block, Ruby code looks exactly the same. This means that Ruby programmers can more easily transfer repeating code to methods that take blocks without rewriting a large amount of code. It also means that using a block does not interrupt normal logic, and you can create new “control logic” constructs that behave almost identically to built-in logic constructions such as if and while.

Rails makes good use of this in respond_to , offering a convenient syntax for content negotiation:
Copy Source | Copy HTML<br/> def index <br/> @people = Person.find(:all)<br/> <br/> respond_to do | format |<br/> format .html # default action is render <br/> format .xml { render :xml => @people.xml }<br/> end <br/> end <br/>

Due to the way blocks work in Ruby, you can also return from any of the format blocks:
Copy Source | Copy HTML<br/> def index <br/> @people = Person.find(:all)<br/> <br/> respond_to do | format |<br/> format .html { redirect_to (person_path(@people.first)) and return }<br/> format .xml { render :xml => @people.xml }<br/> format .json { render :json => @people.json }<br/> end <br/> <br/> session[:web_service] = true <br/> end <br/>

We returned from HTML format after the redirect, which allowed us to perform an additional action (set: web_service in session) for other cases (XML and JSON MIME types).

Above, we have demonstrated several features of blocks in Ruby. return , yield and super together can be seen extremely rarely. Ruby programmers typically use one or more of these constructs inside blocks, simply because their use seems natural.

So why are blocks in ruby ​​better?


If you’ve gotten this far, let's look at another use of blocks in Ruby: synchronization of mutexes.

Java supports synchronization through a special synchronized :
Copy Source | Copy HTML<br/> class Example {<br/> final Lock lock = new Lock();<br/> <br/> void example() {<br/> synchronized( lock ) {<br/> // <br/> }<br/> }<br/>} <br/>

In essence, Java provides a special construct to implement the idea that a certain piece of code should be executed only once for a given instance of a synchronization object. Since Java offers a special construct, you can return synchronization code from within and runtime Java will perform the appropriate processing.

Similarly, Python required the use of try/finally prior to Python version 2.5, when a special language function was added to handle the try/finally idiom:
Copy Source | Copy HTML<br/> class Example :<br/> # <br/> def example (self):<br/> lock.acquire()<br/> try :<br/> ... access shared resource<br/> finally :<br/> lock.release() # , <br/> <br/> # <br/> def example (self):<br/> with lock:<br/> ... access shared resource <br/>

In case 2.5, the object passed to with must implement a special protocol (including the __enter__ and __exit__ methods), so the with expression cannot be used as general purpose and lightweight Ruby blocks.
Ruby represents the same concept of using a block-accepting method:
Copy Source | Copy HTML<br/> class Example <br/> @@lock = Mutex .new<br/> <br/> def example <br/> @@lock.synchronize do <br/> # <br/> end <br/> end <br/> end <br/>

It is important to note that synchronize is a normal Ruby method. The original version, written in pure Ruby, looks like this :
Copy Source | Copy HTML<br/> def synchronize <br/> lock<br/> begin <br/> yield <br/> ensure <br/> unlock<br/> end <br/> end <br/>

There are all signs that we had discussed above. It locks the object, calls the block and then makes sure that the lock is released. This means that if the Ruby programmer returns the result from within the block, synchronize will work correctly.

This example demonstrates the key power of Ruby blocks: they can easily replace language constructs. That is, a Ruby programmer can take unsafe code, paste it into a sync block, and the code will then work safely.

Postscript


Historically, I wrote my posts without a large number of links, primarily fearing their obsolescence. I receive more and more requests for annotations in my posts, so I will start doing this. Let me know if you think that my annotations in this post were useful and freely offer what you find useful in this area.

Some helpful comments after the article.



James Edward Gray II :
When using the pathname, you can translate:

Copy Source | Copy HTML<br/> File . open (path, “a”) do |file|<br/> # … <br/> end <br/>
at:
Copy Source | Copy HTML<br/>path. open (“a”) do |file|<br/> # … <br/> end <br/>


Colin Curtin :
Something to keep in mind about non-local-return: a block must have access to the context from which you want to return.

Copy Source | Copy HTML<br/> def a <br/> yield <br/> end <br/> a { return 0 } # => LocalJumpError: unexpected return <br/> <br/> def c <br/> yield <br/> end <br/> <br/> def b <br/> c { return 1 }<br/> end <br/> b # => 1 <br/> <br/> def d <br/> lambda { return 2 }.call<br/> end <br/> d # => 2 <br/>


ecin :
Remember that different ways of creating closures (Proc.new, proc, lambda) are not always equivalent to each other:

innig.net/software/ruby/closures-in-ruby.rb

...

Rit Li :
I love ruby ​​blocks. Thank you for the article.

Regarding “Rails vs Django”, there are three things that Rails wins:

1) Convention over Configuration.
Django has no large configuration files, only one settings.py file. So Django is a framework with “Easy Configuration,” not “Convention over Configuration.”

2) REST
Rails really covers REST. Seven action controller is great. Django does not have an integrated resource / route mechanism for REST. Once you start REST, you will not go back.

3) Eco system in Rails
Rails plugins are there for everything. Plus, there is commercial support, books, blogs, screencasts and hosting for Rails. Django really lacks this.

Translator postscript

PS I can post it only to my blog, because I don’t have enough specialized karma, but I hope it will be useful and I will move it, for example, to Ruby :)
PPS If necessary - add timestamps for comments and comments about python (I don't know him very well)
PPPS Please do not scold me for the content of the article - scold only for the translation :)

Source: https://habr.com/ru/post/86882/


All Articles