⬆️ ⬇️

Using lambda as local functions

Surely, you have come across a situation where there is a fairly bold method, and you have to put some of its code into a separate method and your class / module is overwhelmed with methods that belong to a single method and are not used anywhere else. Awful pun, right?



If you just want to familiarize yourself with the implementation of the class, then these very auxiliary methods are very much callous eyes, you have to jump on the code back and forth. Yes, of course, you can spread them out into separate modules, but I think that this is often too redundant (for example, I don’t want to create a module that, in fact, defines only one method decomposed into n parts). It is especially unpleasant when these auxiliary functions consist of a single string (for example, a method that pulls a specific element from parsed JSON).



And since I started talking about parsing, let's give a somewhat synthetic example.



Somewhat synthetic example



Task



Generate Hash with the exchange rate of different currencies against the ruble. Something like this:



{ 'USD' => 30.0, 'EUR' => 50.0, ... } 


Decision



The site of the Central Bank has this page: http://www.cbr.ru/scripts/XML_daily.asp



Actually, everything can be done like this:



 require 'open-uri' require 'active_support/core_ext/hash' # for Hash#from_xml def rate_hash uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp') xml_with_currencies = uri.read rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute'] rates.map(&method(:rate_hash_element)).to_h end def rate_hash_element(rate) [rate['CharCode'], rubles_per_unit(rate)] end def rubles_per_unit(rate) rate['Value'].to_f / rate['Nominal'].to_f end 


Either by class:



 require 'open-uri' require 'active_support/core_ext/hash' # for Hash#from_xml class CentralBankExchangeRate def rubles_per(char_code) rate_hash_from_cbr[char_code] || fail('I dunno :C') end # # other public methods for other currencies # private # Gets daily rates from Central Bank of Russia def rate_hash_from_cbr uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp') xml_with_currencies = uri.read rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute'] rates.map(&method(:rate_hash_element)).to_h end # helper method for #rate_hash_from_cbr def rate_hash_element(rate) [rate['CharCode'], rubles_per_unit[rate]] end # helper method for #rate_hash_element def rubles_per_unit(rate) rate['Value'].to_f / rate['Nominal'].to_f end # # other private methods # end 


Let's not talk about which libraries should be used, we will assume that we have rails and therefore we will use Hash#from_xml from there.



Actually, our task is solved by the #rate_hash method, while the remaining two methods are auxiliary for it. Agree that their presence is very distracting.



Pay attention to the variable xml_with_currencies : its value is used only once, which means that its presence is not necessary and you could write Hash.from_xml(uri.read)['ValCurs']['Valute'] , however, I think its use slightly improves the readability of the code. Actually, the appearance of auxiliary methods is the same technique, but for pieces of code.



As you have probably guessed from the title, I propose to use lamba for such auxiliary methods.



Solution with lambda



 require 'open-uri' require 'active_support/core_ext/hash' # for Hash#from_xml def rate_hash uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp') xml_with_currencies = uri.read rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute'] rubles_per_unit = -> (r) { r['Value'].to_f / r['Nominal'].to_f } rate_hash_element = -> (r) { [r['CharCode'], rubles_per_unit[r]] } rates.map(&rate_hash_element).to_h end 


Either by class:



 require 'open-uri' require 'active_support/core_ext/hash' # for Hash#from_xml class CentralBankExchangeRate def rubles_per(char_code) rate_hash_from_cbr[char_code] || fail('I dunno :C') end # # other public methods for other currencies # private # Gets daily rates from Central Bank of Russia def rate_hash_from_cbr uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp') xml_with_currencies = uri.read rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute'] rubles_per_unit = ->(r) { r['Value'].to_f / r['Nominal'].to_f } rate_hash_element = ->(r) { [r['CharCode'], rubles_per_unit[r]] } rates.map(&rate_hash_element).to_h end # # other private methods # end 


Now visually it is immediately obvious that we have one method suitable for use. And if we want to immerse ourselves in its implementation, then problems with reading should not arise either, since the lambda declarations are quite catchy and understandable (thanks to the syntactic sugar).



But this is impossible!



As far as I know, in JavaScript, it is rightly a bad practice to embed functions into each other:



 function foo() { return bar(); function bar() { return 'bar'; } } 


Fair enough, because every time we call foo() we create a function bar, and then destroy it. Moreover, parallel execution of several foo() will create 3 identical functions, which also spends memory. upd. here, on the contrary, they write "feel free to use them"., so I'm wrong.



But how critical is the question of consuming the extra fractions of a second for our method? Personally, I do not see the point in order to win half a second to give up various convenient designs. For example:



 some_list.each(&:method) 


Slower than



 some_list.each { |e| e.method } 


Because in the first case implicit casting to Proc .



In addition, Ruby still works on the server, not the client, so the speed is much higher there (although it can also be argued, because the server serves many people, and the loss of even a fraction of a second on a global scale increases to minutes / hours / days )



And yet, what about speed?



Let's conduct an experiment remote from reality.



using_lambda.rb:



 N = 10_000_000 def method(x) sqr = ->(x) { x * x } sqr[x] end t = Time.now N.times { |i| method(i) } puts "Lambda: #{Time.now - t}" 


using_method.rb:



 N = 10_000_000 def method(x) sqr(x) end def sqr(x) x * x end t = Time.now N.times { |i| method(i) } puts "Method: #{Time.now - t}" 


Run:



 ~/ruby-test $ alias test-speed='ruby using_lambda.rb; ruby using_method.rb' ~/ruby-test $ rvm use 2.1.2; test-speed; rvm use 2.2.1; test-speed; rvm use 2.3.0; test-speed Using /Users/nondv/.rvm/gems/ruby-2.1.2 Lambda: 11.564349 Method: 1.523036 Using /Users/nondv/.rvm/gems/ruby-2.2.1 Lambda: 9.270079 Method: 1.523763 Using /Users/nondv/.rvm/gems/ruby-2.3.0 Lambda: 9.254366 Method: 1.333142 


Those. Using Lyamba is about 7 times slower than similar code using the method.



Conclusion



This post was written in the hope of finding out what the habial community thinks about the use of this "technique".



If, for example, the speed is not so critical that you need to fight for every millisecond, and the method itself is not called a million times a second, can you sacrifice speed in this case? Or maybe you generally think that this does not improve readability?



Unfortunately, this example does not clearly illustrate the meaning of such a strange use of lambda. Meaning appears when there is a class with a sufficiently large number of private methods, most of which are used in other private methods, moreover, only once. This idea should facilitate understanding of the implementation of the work of individual class methods, since there is no heap of def and end , but rather simple one-line functions ( -> (x) { ... } )



Thank you for your time!



UPD.

Some people with whom I spoke on this occasion did not quite understand the idea.



  1. I do not propose to replace all private methods with lambda. I propose to replace only very simple one-liners, which are not used anywhere else except in the required method (and the method itself is likely to be private).
  2. Moreover, even for simple odnostrochnnikov need to proceed from the situation and use this "technique" only if the readability of the code really improves and at the same time the speed sink will not be any significant.
  3. The main profit of using lambdas is reducing the number of lines of code and visually highlighting the most significant parts of the code (the text editor equally highlights the main and auxiliary methods, and here we will use the lambda).
  4. Deliver clean functions to lambda


UPD2.

By the way, in the first example, two auxiliary methods can be combined into one:



 def rate_hash_element(rate) rubles_per_unit = rate['Value'].to_f / rate['Nominal'].to_f [rate['CharCode'], rubles_per_unit] end 


UPD3. from 10.08.2016



It turns out that this technique is mentioned in ruby-style-guide. (Link) [ https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#no-nested-methods ]



')

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



All Articles