Наверняка вы сталкивались с ситуацией, когда есть достаточно жирный метод, и вам приходится вынести часть его кода в отдельный метод и ваш класс/модуль переполняется методами, которые относятся к одному единственному методу и нигде более не используется. Ужасный каламбур, правда?


Если вы просто хотите ознакомиться с реализацией класса, то эти самые вспомогательные методы очень сильно мозолят глаза, приходится прыгать по коду туда-сюда. Да, конечно, можно разнести их по отдельным модулям, но я считаю, что зачастую это слишком избыточно (я, например, не хочу создавать модуль, который, по сути, определяет только один метод, декомпозированный на 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) { 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

Либо классом:



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

Теперь визуально сразу видно, что у нас есть один метод, пригодный для использования. А если мы захотим погрузиться в его реализацию, то проблем с чтением также возникнуть не должно, поскольку объявления лямбд достаточно броские и понятные (спасибо синтаксическому сахару).


Но ведь так нельзя!


Насколько я знаю, в 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
Как вам идея?

Проголосовал 51 человек. Воздержалось 20 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться с друзьями
-->

Комментарии (51)


  1. Zibx
    19.06.2016 01:48

    Функции не нужно создавать на каждом вызове, из стоит кэшировать. Для особенно критичных к скорости мест может иметь смысл использование eval для генерации специальной функции с заинлайленными значениями. После прогрева такая функция будет по скорости не уступать обычной (это из V8, в руби не уверен в существовании прогрева).


    1. Sna1L
      19.06.2016 01:54

      Если честно, не совсем Вас понял.


      Я в посте привел простой пример кода на JS. Подобный шаблон я несколько раз встречал в проектах и это считается говнокодом по описанным в посте причинам (пруфы можно найти).


      Вся моя идея заключается в том, чтобы принести в жертву немного скорости для того, чтобы код стал более читаемым.
      Если вы бегло посмотрите на первый пример, то Вам сразу же в глаза бросятся три метода (можно даже не читать их названия, сама суть, что их три, и два из них по сути бесполезны для читающего), а во втором случае — всего-лишь один.


      В случае JavaScript подобное вкладывание, как мне кажется, используется для того, чтобы инкапсулировать функции. В моем случае смысл исключительно во внешнем виде текста (кода).


      1. urvalla
        20.06.2016 23:29
        +1

        Я думаю, что закешировать действительно можно, например привязав к классу:

        @@rubles_per_unit||= ->(r) { r['Value'].to_f / r['Nominal'].to_f }
        


        1. Sna1L
          20.06.2016 23:45

          Не подумал об этом. Возьму на заметку, спасибо!


  1. VolCh
    19.06.2016 08:03
    -1

    Тестировать как эти лямбды?


    1. Sna1L
      19.06.2016 08:51
      +2

      Вы тестируете однострочные вспомогательные функции?


      def some_method
        # ...
        some_private_method
      end
      
      private
      
      def some_private_method
        some_private_method_2
        ...
      end
      ...


    1. Sna1L
      19.06.2016 09:25

      Добавил в пост тот же пример с валютами, но классом (тупо обернул). Так, вроде, понятнее, о чем я.


  1. youlose
    19.06.2016 08:35

    Можно вынести этот функционал в отдельный модуль и при помощи ключевого слова private указать где публичные методы, а где приватные.


    1. Sna1L
      19.06.2016 08:54

      смысл не в инкапсуляции, а в сокращении строк кода и вызуальном выделении основных методов (основных != публичных)


      Более того, мне кажется, что использовать лямбды имеет смысл для вспомогательных функций приватных методов (просто пример это не иллюстрирует).


    1. Sna1L
      19.06.2016 09:27

      Добавил в пост тот же пример с валютами, но классом (просто обернул). Так, вроде, более ясно, о чем я.


  1. michael_vostrikov
    19.06.2016 11:16

    … давайте приведу несколько синтетический пример
    … Давайте проведем отдаленный от реальности эксперимент

    А какой смысл в таких примерах, неужели у вас нет нормального рабочего примера, не оторванного от реальности? Может, если примера нет, то и проблемы нет?)

    По-моему, тут есть неправильное разбиение на функции. В rubles_per_unit() надо передавать готовые значения. Тогда это будет не вспомогательная функция, а вполне рабочая, которую можно вызывать из других мест. А для map вполне естественно указывать лямбды, потому что без map это будет просто тело цикла.

    Пример (Ruby не знаю, поэтому на PHP, уж извините):
    class CentralBankExchangeRate
    {
        public function function 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'];
    
            $rate_hash = [];
            foreach ($rates as $rate) {
                $rate_hash[$rate['CharCode']] = $this->rubles_per_unit($rate['Value'], $rate['Nominal']);
            }
        }
    
        public function rubles_per_unit($value, $nominal)
        {
            return ($value / $nominal);
        }
    }
    


    1. Sna1L
      19.06.2016 11:25

      Есть рабочий пример, но по некоторым причинам я решил его не использовать. Более того, я не уверен, что использование этого в продакшне хорошая затея, именно поэтому и написал данный пост, чтобы посмотреть, что думают люди. В первую очередь волнует вопрос производительности, т.к. мои немногочисленные коллеги, вроде, положительно отнеслись к идее.


      Относительно Вашего примера — тогда уж не rubles_per_unit, а value_per_unit;)
      Просто хотелось продемонстрировать хоть сколько-нибудь ясно, что я предлагаю.
      Пока что это не особо получилось, очевидно.


      А вообще, неужели у Вас нет в практике случаев, когда вы пишите какой-нибудь приватный метод, а за ним паровозиком идут связанные именно с ним методы (вспомогательные функции)? Вот моя идея заключается в том, чтобы этот паровозик убрать и оставить только значимую логику выделенной.
      Сами посудите, у Вас функция rubles_per_unit до безобразия проста, а азнимает аж 4 строки и бросается в глаза наравне с rate_hash, хотя вне нее, по сути, бесполезна (на самом деле, полезна, но будем считать, что она максимально специфична).


      1. Sna1L
        19.06.2016 11:31

        Вот моя идея заключается в том, чтобы этот паровозик убрать и оставить только значимую логику выделенной.


        В данном примере это не особо заметно, но представьте, что у вас N-ое количество приватных методов, и за каждым из них следует вереница из вспомогательных специфичных исключительно для него однострочников. Это заставляет глаза разбегаться, и сам по себе код класса начинает воспринимается сложнее и выглядит очень монструозным


        1. michael_vostrikov
          19.06.2016 11:51

          Согласен, мне тоже это не очень нравится. Но зачем использовать именно лямбды, если можно просто написать линейный код, и дать понятное название переменной с результатом вычисления?


          1. Sna1L
            19.06.2016 12:00

            Например, чтобы использовать их в функциях высшего порядка, как в примере:


            rates.map(&rate_hash_element).to_h

            Ситуации разные, иногда это даже ухудшает читаемость. Иногда же наоборот, повышает.
            Ruby немного почерпнул из Perl. И поэтому TMTOWTDI. А из всех вариантов решения задачи нужно выбрать тот, который лучше всего вписывается в код. Я не предлагаю использовать эти лямбды повсеместно. Просто предлагаю взять это на заметку, как один из вариантов. Другое дело, если идея слишком уж неудачная (на это есть опрос).


      1. Sna1L
        19.06.2016 13:09

        Кстати, сам метод rubles_per_unit специфичен для объектов из xml ЦБ, при этом имя у него весьма абстрактное, так что использование лямбды в данном случае только подчеркивает его специфичность.


  1. house2008
    19.06.2016 11:37
    +1

    Я вас полностью поддерживаю. Мне тоже нравится такой способ локального выноса куска функционала, и не только в руби.


    1. Sna1L
      19.06.2016 11:43

      А можете привести пару кусков кода для других языков?
      Я, вроде, и понимаю, что можно использовать много где, но приходит на ум только CoffeeScript.


      1. house2008
        19.06.2016 11:55
        +1

        На swift очень удобный синтаксис для работы с функциями. В данном случае лично для меня такой вынос делает код более красивым.

        let fn: Result<FeedParsed> -> FeedParsed? = { result in
            switch result {
            case .Success(let result):
                return result
            case .Fail(let error):
                print(error)
                return nil
            }
        }
                
        self.completion(self.parserResults.flatMap(fn))
        


  1. printercu
    19.06.2016 15:07

    Почему простой вариант не рассматривается даже? Все инкапсулированно, и лишние сущности не надо создавать.


      rates.map do |x|
        rate = x['Value'].to_f / x['Nominal'].to_f
        [x['CharCode'], rate] 
      end.to_h


    1. Sna1L
      19.06.2016 15:50

      Пример синтетический, поэтому дискретизация выглядит несколько избыточно.


      Однако относительно Вашего куска кода:


      • rate при наличии rates — не самое удачное имя для переменной
      • Не совсем ясно, что этот rate обозначает, так что стоило назвать переменную хотя бы rubles_per_unit, как в посте
      • Разнообразные куски кода, которые выделяются доп. отступом, усложняют понимание кода. В данном случае, это не заметно, если код будет большим и сложным, то эти куски кода еще и смешивают уровни абстракции (что не есть хорошо, если почитать Роберта Мартина, да Вы и сами это прекрасно понимаете, наверное)

      Неужели Ваши 4 строки выглядят понятнее, чем эти три?


      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


      1. printercu
        20.06.2016 01:00

        В вашем написании код приходится читать дважды, в разных направлениях. Сначала сверху вниз, потому что это императивный язык, потом снизу вверх, чтобы вникнуть в логику.


        rate от exchange rate, так вроде переводится обменный курс. Можно 2 слова писать, если в команде возникают недопонимания. По-моему, это лучше чем rubles_per_unit, т.к. не привязано к валюте. Ничего страшного в связи с rates не вижу, users.each { |user| ... } можно же писать.


        1. Sna1L
          20.06.2016 05:55

          В вашем написании код приходится читать дважды, в разных направлениях


          В случае небольшого скрипта это действительно может показаться странным. Хотя, например, в JS это не особо кого-то напрягает. Условно говоря:
          var callback = function() { ... }
          someFunction(callback);


          Смысл моего метода как раз-таки в том, чтобы облегчить поверхностное чтение кода, без углубления в детали, если сравнивать с вариантом, когда все вспомогательные функции вынесены в методы. Когда вы начинаете погружаться в детали, вам в любом случае придется прыгать туда-сюда. Зачастую, с помощью поиска.
          Ваш же вариант действительно выглядит корректно, однако, я его даже не рассматриваю (о, кажется, я Вас процитировал), потому что темой является именно замена однострочных методов на лямбды (наверняка ведь в Вашей практике встречались такие однострочники).
          users.each { |user| ... } можно же писать


          Можно, но, имхо, не самая удачная практика, ибо вы создаете две переменные с практически идентичными названиями. Одна опечатка и найти проблему может быть сложно. Лично я стараюсь либо во множественном числе писать user_list, либо, если это блок, в единственном писать по первой букве: users.each { |u| ... }.
          По-моему, это лучше чем rubles_per_unit, т.к. не привязано к валюте


          А я вот считаю, что именно по этой самой причине rubles_per_unit лучше, т.к. несет больше информации о содержимом (мы ведь разбираем конкретный xml с валютами по отношению к рублю).


          1. youlose
            20.06.2016 06:17
            +1

            «var callback = function() {… }
            someFunction(callback);»

            Такой подход не особо рекомендуется разными best practices, всё же рекомендуют определять функции, благо в JS они имеют область видимости как у переменных, дополнительным плюсом является нормальное название их в утилитах для разработчиков + поиск по имени функции в IDE.


            1. Sna1L
              20.06.2016 06:35

              хм… действительно.
              Мне почему-то казалось, что:


              (function() {
                someFunction(callback);
              
                function someFunction(callback) { callback(); }
                function callback() { console.log('callback'); }
              })();

              не выполнится.


              Буду знать. Надо будет посмотреть, где в проектах используется стиль, как я написал выше. Спасибо!


  1. luarviq
    19.06.2016 15:51

    А кто сказал, что «вкладывание функций друг в друга» в JS, как вы изволили выразиться, является плохой практикой, и кем это признано? Именно это «вкладывание» позволяет организовать замыкание, например, и много других вкусных вещей. А уж функциональный код на JS можно (и нужно) писать безо всяких лямбд. И с памятью и с производительностью при этом все в порядке, уверяю вас.


    1. Sna1L
      19.06.2016 15:57

      я знаю о замыканиях, уверяю Вас. В сниппете замыкание не используется (де факто), есть просто вкладывание.


      Однако, я действительно оказался несколько голословен, ибо держал в голове вот это:
      https://google.github.io/styleguide/javascriptguide.xml#Nested_functions


      и был уверен, что там предлагают от этого отказаться. Уж не знаю, с чего я взял, что это считают говнокодом, но производительность действительно падает.


      P.S. Погуглив на эту тему, нашел множество обсуждений на stackoverflow и вот эту статью:
      http://code.tutsplus.com/tutorials/stop-nesting-functions-but-not-all-of-them--net-22315
      но не уверен, что эо можно назвать авторитетным источником.


      1. luarviq
        19.06.2016 17:29
        -1

        Да с чего она падать-то будет, не могу постичь. Функция в JS — это такой же объект, как и все остальные, и даже наследуется от базового Object. С точки зрения компилятора, это то же самое, что объявить класс внутри класса (обычная практика в Java, например). А вот возможность возвратить из функции функцию, не вычисляя ее,- это уже ленивые вычисления, т.е. то, что повышает производительность.
        И кстати, если говорить о лямбдах, то они вообще не про то, о чем вы написали. Лямбда — это всего лишь способ передать функции функцию в качестве аргумента, не более того. Зачем же их использовать в столь интенсивных вычислениях, как возведение в квадрат ряда натуральных чисел?
        Теперь собственно о результатах вашего эксперимента. О чем он говорит? Что лямбда возводит натуральные числа в квадрат в 7 раз медленнее, чем обычный метод? С какой это стати? Вы считаете, что обычный метод так сильно оптимизирует возведение в квадрат? Если да, то я хочу знать этот наиболее оптимальный алгоритм вычисления функции y=x^2. Или у руби есть две разные математические библиотеки, одна для лямбд, а другая для обычных функций? И в это я готов поверить, если буду иметь результаты дебаггинга интерпретатора. Но лучше всего проводить подобные эксперименты, используя давно проверенные методы мат. статистики. По крайней мере, оценить количество фактического материала (данных), необходимое для получения более-менее достоверного результата.


        1. Sna1L
          19.06.2016 17:37

          Все очень просто.


          Каждый раз, когда мы вызываем этот метод, мы создаем новый объект-лямбду. Т.е. он инициализируется, под него выделяется память, а потом он уничтожается.


          Так понятнее, на что именно тратится время?


          Сама операция возведения в квадрат была выбрана спонтанно. С тем же успехом можно было бы взять функцию идентичности.
          В JS Вы столкнетесь с тем же самым. Попробуйте провести эксперимент для разных интерпретаторов, какие-то наверняка оптимизируют этот момент, но потеря скорости очевидна.


          В руби, кстати, proc { |x| x} является сахаром по отношению к Proc.new { |x| x }, а лямбда — это как раз-таки объект Proc


          1. Sna1L
            19.06.2016 17:57

            Собственно, я сам провел эксперимент для функции идентичности.


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

            в 6 раз медленнее, чем аналогичный код с методом.


        1. Sna1L
          19.06.2016 17:44

          И да. Мне всегда казалось, что лямбда — это объект-функция. Синтаксис -> (x) { x * x } является сахаром по отношению к lambda { |x| x * x }, который инициализирует объект класса Proc с некоторыми особенностями.


          В JS же синтаксис function f(x) { return x * x } является сахаром по отношению к var f = function() { return x * x } (источник — Крокфорд), что по сути является присвоением лямбды локальной переменной.


          1. luarviq
            19.06.2016 18:11
            -1

            Хорошо, создаем объект-лямбду. Создание объекта — дорогостоящая операция во всех языках программирования, на всех платформах, все это знают. И если руби создает объект (не очень большой) столь медленно, то к черту руби. Это единственный вывод, который можно сделать по результатам вашего эксперимента. Но даже это является спорным. Чтобы утверждать это, нужен еще один эксперимент — по выделению и освобождению памяти для объектов руби различного размера и на разных платформах. Может быть, на какой-нибудь *BSD алгоритмы выделения памяти столь совершенны, что это ваше 7g-ускорение сойдет на нет тихо и незаметно.


            1. Sna1L
              19.06.2016 18:19

              ок, к черту руби.


        1. Sna1L
          19.06.2016 17:49

          А мой эксперимент более чем красноречиво говорит о том, что использование лямбды медленнее, чем использование метода. Не понимаю, к чему Вы придираетесь. Да, мой способ оценивания, мягко говоря, кустарный, но зато наглядный. Думаю, очевидно, что операция возведения в квадрат выполняется за фиксированное время.


          1. luarviq
            19.06.2016 18:29

            Да, проблема именно в способе оценивания. Вы проводите многофакторный эксперимент, или, говоря математическим языком, находитесь в многомерном пространстве признаков, а выдаете на-гора какой-то частный случай и требуете считать это результатом. Это все равно, что теорему Пифагора a^2+b^2=c^2 распространить на все натуральные степени и сказать: готово, ребята, великая теорема Ферма доказана.


            1. Sna1L
              19.06.2016 18:37

              Ну так я ведь в посте и написал, что эксперимент удален от реальности:)


              Смысл его в том, чтобы показать, что использование лямбд заметно проигрывает по скорости использованию методов (хотя это, вроде, и так очевидно, но, судя по Вашим комментариям выше, не совсем).
              И да, я понимаю, что это плохой эксперимент. Но смысл метода в первую очередь в попытке улучшить читаемость кода. И при этом я делаю акцент на том, что еще и потеря в производительности будет.


              Я бы с радостью провел эксперимент поточнее, если бы знал, как это лучше сделать. Я не особо интересуюсь вопросами быстродействия. просто стараюсь, чтобы все было в рамках разумного. Однако у меня не хватает компетентности (признаю), чтобы грамотно оценивать скорость работы подобных вещей.


              1. luarviq
                19.06.2016 18:50

                Да, эксперимент нуждается в уточнении. Даже если отвлечься от выделения памяти и от руби. Например, некое подобие лямбды можно организовать и в С++, используя функторы. И что, эти лямбды будут работать в 7 раз медленнее, чем обычные методы классов? Я оооооочень сомневаюсь.Наоборот, я не удивлюсь, если они будут работать быстрее.


                1. youlose
                  20.06.2016 05:04

                  Вы что хотите доказать? Что С++ быстрее Ruby и кушает меньше памяти? Это факт.
                  За синтаксический сахар надо платить производительностью и потреблением памяти, но в то же время мы получаем невероятное ускорение разработки и меньшее в разы количество кода, что благоприятно сказывается на количестве багов в коде.
                  И среди интерпретируемых языков Ruby совсем не медленный.
                  Да и задач где Ruby реально будет недоставать производительности на порядки меньше, чем тех где надо сделать побыстрее и в срок и эти узкие места можно написать на том же С++ и слинковать в Ruby.

                  Проблема с производительностью у автора поста в том, что он лямбды перекомпилирует на каждом обращении к функции rate_hash и автору надо научиться пользоваться гемом benchmark, на моём компьютере с core i7 разница всего в 4 раза. А если лямбды объявить раньше, то разница на 33%. И автор как раз и пытается выяснить оправдано ли улучшение читаемости кода к ухудшению производительности. Мой опыт показывает что да, оправдано (правда способ автора мне не нравится, вот как выше printercu написал так мне понятнее).


                  1. luarviq
                    20.06.2016 17:33
                    -1

                    C++ я привел, чтобы показать, что идея эксперимента изначально некорректна, а именно некорректна постановка задачи. Сравнивать круг задач, для которых пригоден С++, а для которых руби — это, простите, какой-то моветон. Руби хорош на своих рельсах (что тоже спорно, для реализации MVC паттерна лучше взять какой-нибудь Backbone.js и горя не знать), да еще в каком-нибудь метасплойте, для обучения скрипт-кидди удаленно заливать шеллы на ни в чем не повинные серверы.
                    О лямбдах. Да, на какой-то конкретной платформе руби-лямбды работают медленнее, чем руби-методы, и что с того? Вам это что-нибудь дает? Мне лично нет. К вопросу о читаемости кода: давайте сделаем то же самое на Java8, там тоже теперь есть лямбды. Но там вам не нужно будет мучительно выбирать между читаемостью и производительностью, вы получите и то, и другое, и почти задаром, так что вышеприведенный эксперимент вообще потеряет смысл. Проблема руби в его уродливом синтаксисе и системе классов, которые вообще не классы, а @#$% знает что. Отсюда и подобные эксперименты. Работай автор на той же Java8, я уверен, ему бы и в голову не пришло этим заниматься.


                    1. youlose
                      20.06.2016 19:44
                      +1

                      «C++ я привел, чтобы показать, что идея эксперимента изначально некорректна, а именно некорректна постановка задачи.»
                      Автор пытается рассуждать на тему: «что важнее читабельность кода или производительность?», в чём некорректность от использования в примерах Ruby?

                      «Сравнивать круг задач, для которых пригоден С++, а для которых руби — это, простите, какой-то моветон»
                      Они служат для разного класса задач и люди которые врываются их сравнивать в первую очередь должны задуматься, а нужны ли их сравнения. Могу вас удивить, но, в основном, люди которые пишут на Ruby, знают не только Ruby и они имеют представление о плюсах и минусах этого языка.

                      «своих рельсах (что тоже спорно, для реализации MVC паттерна лучше взять какой-нибудь Backbone.js и горя не знать»
                      Боюсь вам надо почитать про разницу между фронтендом и бэкендом.

                      «Да, на какой-то конкретной платформе руби-лямбды работают медленнее, чем руби-методы, и что с того? Вам это что-нибудь дает? Мне лично нет.»
                      Если вам это неинтересно, проходите мимо, автор правильно проставил тэги и есть люди которым это действительно интересно.

                      «К вопросу о читаемости кода: давайте сделаем то же самое на Java8, там тоже теперь есть лямбды.»
                      Ну давайте, напишите аналог этого кода экрана так на два… =) И запускать потом через пень колоду надо будет…
                      Боюсь что С++, Java и другим языкам подобного плана нет смысла соревноваться в читаемости и компактности кода с Руби, у них другие преимущества.

                      «Проблема руби в его уродливом синтаксисе и системе классов, которые вообще не классы, а @#$% знает что. Отсюда и подобные эксперименты»
                      При чём тут это вообще не понятно, подобные эксперименты появляются от того что людям непонятно: что лучше 100500 функций в 1-2 строки, или императивный подход где код лежит в одном месте. И подобные мысли могут посещать программистов на любых языках программирования общего пользования.

                      «Работай автор на той же Java8, я уверен, ему бы и в голову не пришло этим заниматься.»
                      Как показывает мой жизненный опыт, самые любители померятся 3,14письками (производительностью) — это те кто программируют на Go, C/C++ и Java (и может быть это вполне логично, потому что на них решают задачи где как раз производительность крайне критична).


                      1. luarviq
                        24.06.2016 16:51

                        Как писали на заре ФИДО, слишкам многа букав (ниасилил). Вы, уважаемый, судя по всему, никогда не писали код С++ уровня предприятия, иначе бы вам пришлось постичь ВЕСЬ С++, а не только его какое-то подмножество (как показывает практика, неофиты обычно останавливаются на смарт-пойнтерах и забрасывают это дело, переходя на питон, руби и тому подобное). И тогда при взгляде на такие языки, как руби, у вас бы появлялись позывы к рвоте, вот прям как у меня сейчас. По поводу фронтенда и бекенда я отвечу в диалектическом ключе. Примем утверждение, что фронтенд — это перевернутый с ног на голову бекенд, и наоборот (мы не располагаем определениями этих понятий, а следовательно, по Канту, мы можем мыслить эти объекты лишь синтетически Тогда мы имеем полное право производить с ними подобные действия). Тогда, если вас (т.е. меня) тошнит от руби на бекенде (а также везде, где я вижу этот недокод с его недоклассами, недотипами и недометодами), я использую backbone на фронтенде и получаю тот же результат, и даже красивее, ибо js действительно красивый язык. Только не начинайте холивар еще и по этому поводу плз. Потому что если вы напишете, что руби тоже по-своему красивый язык, мне придется отослать вас еще и к учению Лейбница о монадах. (Это не те монады, которые используются в Haskell, но что-то общее есть.). А вообще весело у вас тут на хабре, мне нравится. Тема выеденного яйца не стоит, а такая буря в стакане. Класс!


          1. iqiaqqivik
            20.06.2016 17:12

            Ваш эксперимент говорит не о том, что «использование лямбды медленнее, чем использование метода», а о том, что десять миллионов раз объект создать дороже, чем один раз.

            Определите константу, присвойте ей вашу лямбду и внезапно выясните, что лямбда быстрее.


            1. Sna1L
              20.06.2016 17:17

              Вы говорите о причине, а я о следствии (вы меня не просветили, комментарием выше я этот момент уточнил уже)


              Смысла в том, чтобы выносить в константы, абсолютно нет. Тогда нарушите инкапсуляцию, да и один черт код начнет глаза мозолить.


              1. iqiaqqivik
                21.06.2016 08:44

                Чего-чего я нарушу?

                У меня в каждом пухлом классе есть вложенный приватный класс, который и определяет все эти константы. Расположен в самом конце файла, глаза никому не мозолит.

                Просветил ли я вас, нет ли, — не имеет никакого значения. Ваш вопрос на самом деле звучит так: «имеет ли смысл выполнить операцию сто раз вместо одного, если это [на мой взгляд] улучшает читаемость». На этот вопрос ответ всегда «нет», и лямбды тут вообще ни при чем. Если же вы хотите предметно поговорить про лямбды — то ответ как раз обратный: да, лямбдами вместо методов пользоваться можно и нужно: это [незначительно] ускоряет код и, главное, делает его гораздо более читаемым.


  1. IDMan
    20.06.2016 12:36
    +2

    Не совсем понятно, зачем одну строчку кода, которая используется один раз, выносить в лямбда. Если это независимая операция, значит место ей в методе. Если она работает только в контексте другого метода, почему не просто строчка кода, которая возвращает результат в переменную? Я делал как вы, лямбда, нужные только в пределах метода, но они использовались по несколько раз.


    1. Sna1L
      20.06.2016 17:20

      О, странно, Ваш комментарий появился как-то поздновато.


      Повторю то, что написал в ЛС:


      Я придумал очень неудачный пример. А так да, кейс использования лямбд именно такой же, как и у Вас. Так что мне приятно слышать, что кто-то этим уже и так пользуется.


      Спасибо!


      1. IDMan
        21.06.2016 18:24

        Е, нет, стоп.

        и вам приходится вынести часть его кода в отдельный метод и ваш класс/модуль переполняется методами, которые относятся к одному единственному методу и нигде более не используется

        и
        Особенно неприятно, когда эти вспомогательные функции состоят из одной строки

        это не примеры, а заглавная часть статьи, теория, так сказать. Именно к «одной строки», и «к единственному методу» у меня был вопрос. Может лямбда выглядит и более опрятно, но одна строка, которая используется один раз, причем она не делает независимую операцию (getAuthorId может состоять с одной строки, но операция определенно самостоятельная, впрочем, как и публичная), не имеет на мой взгляд смысла. А многократное использование — это совсем другое дело :)


  1. Anton3
    20.06.2016 13:21

    Создание лямбды с последующей передачей в map не является идиоматическим подходом в Ruby. «Решение с lambda» привычнее переписать так, и не только потому, что лямбда может работать медленнее, чем блок:

    rates.map{ |r| [r['CharCode'], r['Value'].to_f / r['Nominal'].to_f] }.to_h


    1. Sna1L
      20.06.2016 13:22

      Выше уже демонстрировалось подобное решение (кстати, написано оно лучше, чем у Вас) и я ответил по этому поводу.
      Вот эта ветка: https://habrahabr.ru/post/303594/#comment_9663324


      1. Anton3
        20.06.2016 14:18

        На вкус и цвет… Перед тем, как отправить, я прочитал это решение. И всё же, в данном конкретном случае предпочитаю написать более короткий «однострочник».


        1. Sna1L
          20.06.2016 14:19

          "лучше" — потому что информативнее. Про внешний вид я не спорю, тут уж кому как.