Використання lambda в якості локальних функцій

Напевно ви стикалися з ситуацією, коли є досить жирний метод, і вам доводиться винести частину його коду в окремий метод і ваш клас/модуль переповнюється методами, які відносяться до одного єдиного методу і ніде більше не використовується. Жахливий каламбур, правда?
Якщо ви просто хочете ознайомитися з реалізацією класу, то ці самі допоміжні методи дуже муляють очі, доводиться стрибати з кодом туди-сюди. Так, звичайно, можна рознести їх по окремим модулям, але я вважаю, що найчастіше це занадто надмірно (я, наприклад, не хочу створювати модуль, який, по суті, визначає тільки один метод, декомпозированный на n частин). Особливо неприємно, коли ці допоміжні функції складаються з одного рядка (наприклад, метод, який висмикує певний елемент з распарсенного JSON).
І раз вже я заговорив про парсингу, то давайте наведу кілька синтетичний приклад.
Кілька синтетичний приклад
Завдання
Згенерувати Hash з курсом різних валют по відношенню до рубля. Приблизно такий:
{ 'USD' => 30.0,
'EUR' => 50.0,
... }

Рішення
На сайті Центробанку є така сторінка: http://www.cbr.ru/scripts/XML_daily.asp
Власне, все можна зробити ось так:
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

Або класом:
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

Не будемо міркувати про те, які бібліотеки варто використовувати, будемо вважати, що у нас є рейки і тому скористаємося
Hash#from_xml
звідти.
Власне, наше завдання вирішує метод
#rate_hash
, в той час як решта два методи є допоміжними для нього. Погодьтеся, що їх присутність дуже сильно відволікає.
Зверніть увагу на змінну
xml_with_currencies
: її значення використовується лише один раз, а це значить, що її наявність зовсім не обов'язково і можна було написати
Hash.from_xml(uri.read)['ValCurs']['Valute']
, але, як мені здається, її використання трохи покращує читаність коду. Власне, поява допоміжних методів — це той же самий прийом, але для шматків коду.
Як ви вже, напевно, здогадалися з назви, я пропоную використовувати для таких допоміжних методів лямбы.
Рішення з 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['Value'].to_f / r['Nominal'].to_f }
rate_hash_element = -> ® { [r['CharCode'], rubles_per_unit[r]] }

rates.map(&rate_hash_element).to_h
end

Або класом:

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['Value'].to_f / r['Nominal'].to_f }
rate_hash_element = ->® { [r['CharCode'], rubles_per_unit[r]] }

rates.map(&rate_hash_element).to_h
end

#
# other private methods
#
end

Тепер візуально відразу видно, що у нас є метод, придатний для використання. А якщо ми захочемо зануритися в його реалізацію, то проблем з читанням також виникнути не повинно, оскільки оголошення лямбд досить помітні й зрозумілі (спасибі синтаксичному цукру).
Але ж так не можна!
Наскільки я знаю, в JavaScript справедливо є поганою практикою вкладання функцій один в одного:
function foo() {
return bar();

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

Справедливо, тому що кожен раз при виклику
foo()
ми створюємо функцію bar, а потім знищуємо її. Більш того, паралельне виконання декількох
foo()
створить 3 однакових функції, що ще й витрачає пам'ять. upd. тут навпаки пишуть "feel free to use them"., так що я не прав.
Але наскільки критичне питання споживання зайвих часток секунди для нашого методу? Особисто я не бачу сенсу заради виграшу в півсекунди відмовлятися від різноманітних зручних конструкцій. Наприклад:
some_list.each(&:method)

Повільнішим, ніж
some_list.each { |e| e.method }

Тому що в першому випадку використовується неявне приведення до
Proc
.
До того ж, Ruby все-таки працює на сервері, а не клієнта, так що швидкості там набагато вище (хоча тут теж можна посперечатися, адже сервер обслуговує безліч людей, і втрата навіть долі секунди в глобальному масштабі збільшується до хвилин/годин/днів)
І все ж, що зі швидкістю?
Давайте проведемо віддалений від реальності експеримент.
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}"

Запуск:
~/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

тобто використання лямбы приблизно в 7 разів повільніше аналогічного коду, що використовує метод.
Висновок
Цей пост був написаний в надії дізнатися, що думає хабросообщество з приводу використання даного "прийому".
Якщо, наприклад, швидкість не є настільки критичною, що потрібно боротися за кожні мілісекунди, а сам метод не викликається по мільйону разів в секунду, можна пожертвувати швидкістю в даному випадку? А може, ви взагалі вважаєте, що це не покращує читаність?
На жаль, наведений приклад не наочно ілюструє сенс такого дивного використання лямбд. Сенс з'являється, коли є клас з досить великою кількістю приватних методів, велика частина яких використовується в інших приватних методи, причому, тільки один раз. Це, за задумом, має полегшити розуміння реалізації роботи окремих методів класу, т. до. немає купи
def
та
end
, а є досить прості однорядкові функції (
-> (x) { ... }
)
Дякую за приділений час!
UPD.
Деякі люди, з якими я спілкувався з цього приводу, не зовсім правильно зрозуміли ідею.
  1. Я не пропоную замінювати приватні методи на лямбды. Я пропоную замінити тільки дуже прості однострочники, які ніде більше не використовуються, крім як у потрібному методі (причому сам метод, швидше за все, буде приватним).
  2. Більш того, навіть для простих однострочников потрібно виходити з ситуації і використовувати цей "прийом" тільки якщо читаність коду дійсно покращиться і при цьому просідання по швидкості не буде скільки-небудь значним.
  3. Основний профіт використання лямбд — скорочення кількості рядків коду і візуальне виділення найбільш значущих частин коду (текстовий редактор однаково підсвічує головні та допоміжні методи, а тут ми скористаємося лямбдой).
  4. Виносити в лямбды бажано чисті функції
UPD2.
До речі, в першому прикладі два допоміжних методу можна об'єднати в один:

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


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.