Как стать автором
Обновить

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

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

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


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


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


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

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

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

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

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

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


def some_method
  # ...
  some_private_method
end

private

def some_private_method
  some_private_method_2
  ...
end
...

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

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

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


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

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

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

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

По-моему, тут есть неправильное разбиение на функции. В 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);
    }
}

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


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


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

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


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

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


rates.map(&rate_hash_element).to_h

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

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

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

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

На 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))

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


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

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


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


  • 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

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


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

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


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


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


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


А я вот считаю, что именно по этой самой причине rubles_per_unit лучше, т.к. несет больше информации о содержимом (мы ведь разбираем конкретный xml с валютами по отношению к рублю).
«var callback = function() {… }
someFunction(callback);»

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

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


(function() {
  someFunction(callback);

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

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


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

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

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


Однако, я действительно оказался несколько голословен, ибо держал в голове вот это:
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
но не уверен, что эо можно назвать авторитетным источником.

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

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


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


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


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


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

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


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 раз медленнее, чем аналогичный код с методом.

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


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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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


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


Спасибо!

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

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

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

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

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации