Комментарии 51
Если честно, не совсем Вас понял.
Я в посте привел простой пример кода на JS. Подобный шаблон я несколько раз встречал в проектах и это считается говнокодом по описанным в посте причинам (пруфы можно найти).
Вся моя идея заключается в том, чтобы принести в жертву немного скорости для того, чтобы код стал более читаемым.
Если вы бегло посмотрите на первый пример, то Вам сразу же в глаза бросятся три метода (можно даже не читать их названия, сама суть, что их три, и два из них по сути бесполезны для читающего), а во втором случае — всего-лишь один.
В случае JavaScript подобное вкладывание, как мне кажется, используется для того, чтобы инкапсулировать функции. В моем случае смысл исключительно во внешнем виде текста (кода).
смысл не в инкапсуляции, а в сокращении строк кода и вызуальном выделении основных методов (основных != публичных)
Более того, мне кажется, что использовать лямбды имеет смысл для вспомогательных функций приватных методов (просто пример это не иллюстрирует).
Добавил в пост тот же пример с валютами, но классом (просто обернул). Так, вроде, более ясно, о чем я.
… давайте приведу несколько синтетический пример
… Давайте проведем отдаленный от реальности эксперимент
А какой смысл в таких примерах, неужели у вас нет нормального рабочего примера, не оторванного от реальности? Может, если примера нет, то и проблемы нет?)
По-моему, тут есть неправильное разбиение на функции. В 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.
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 с валютами по отношению к рублю). someFunction(callback);»
Такой подход не особо рекомендуется разными best practices, всё же рекомендуют определять функции, благо в JS они имеют область видимости как у переменных, дополнительным плюсом является нормальное название их в утилитах для разработчиков + поиск по имени функции в IDE.
хм… действительно.
Мне почему-то казалось, что:
(function() {
someFunction(callback);
function someFunction(callback) { callback(); }
function callback() { console.log('callback'); }
})();
не выполнится.
Буду знать. Надо будет посмотреть, где в проектах используется стиль, как я написал выше. Спасибо!
я знаю о замыканиях, уверяю Вас. В сниппете замыкание не используется (де факто), есть просто вкладывание.
Однако, я действительно оказался несколько голословен, ибо держал в голове вот это:
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
но не уверен, что эо можно назвать авторитетным источником.
И кстати, если говорить о лямбдах, то они вообще не про то, о чем вы написали. Лямбда — это всего лишь способ передать функции функцию в качестве аргумента, не более того. Зачем же их использовать в столь интенсивных вычислениях, как возведение в квадрат ряда натуральных чисел?
Теперь собственно о результатах вашего эксперимента. О чем он говорит? Что лямбда возводит натуральные числа в квадрат в 7 раз медленнее, чем обычный метод? С какой это стати? Вы считаете, что обычный метод так сильно оптимизирует возведение в квадрат? Если да, то я хочу знать этот наиболее оптимальный алгоритм вычисления функции y=x^2. Или у руби есть две разные математические библиотеки, одна для лямбд, а другая для обычных функций? И в это я готов поверить, если буду иметь результаты дебаггинга интерпретатора. Но лучше всего проводить подобные эксперименты, используя давно проверенные методы мат. статистики. По крайней мере, оценить количество фактического материала (данных), необходимое для получения более-менее достоверного результата.
Все очень просто.
Каждый раз, когда мы вызываем этот метод, мы создаем новый объект-лямбду. Т.е. он инициализируется, под него выделяется память, а потом он уничтожается.
Так понятнее, на что именно тратится время?
Сама операция возведения в квадрат была выбрана спонтанно. С тем же успехом можно было бы взять функцию идентичности.
В JS Вы столкнетесь с тем же самым. Попробуйте провести эксперимент для разных интерпретаторов, какие-то наверняка оптимизируют этот момент, но потеря скорости очевидна.
В руби, кстати, proc { |x| x
} является сахаром по отношению к Proc.new { |x| x }
, а лямбда — это как раз-таки объект Proc
И да. Мне всегда казалось, что лямбда — это объект-функция. Синтаксис -> (x) { x * x }
является сахаром по отношению к lambda { |x| x * x }
, который инициализирует объект класса Proc с некоторыми особенностями.
В JS же синтаксис function f(x) { return x * x }
является сахаром по отношению к var f = function() { return x * x }
(источник — Крокфорд), что по сути является присвоением лямбды локальной переменной.
А мой эксперимент более чем красноречиво говорит о том, что использование лямбды медленнее, чем использование метода. Не понимаю, к чему Вы придираетесь. Да, мой способ оценивания, мягко говоря, кустарный, но зато наглядный. Думаю, очевидно, что операция возведения в квадрат выполняется за фиксированное время.
Ну так я ведь в посте и написал, что эксперимент удален от реальности:)
Смысл его в том, чтобы показать, что использование лямбд заметно проигрывает по скорости использованию методов (хотя это, вроде, и так очевидно, но, судя по Вашим комментариям выше, не совсем).
И да, я понимаю, что это плохой эксперимент. Но смысл метода в первую очередь в попытке улучшить читаемость кода. И при этом я делаю акцент на том, что еще и потеря в производительности будет.
Я бы с радостью провел эксперимент поточнее, если бы знал, как это лучше сделать. Я не особо интересуюсь вопросами быстродействия. просто стараюсь, чтобы все было в рамках разумного. Однако у меня не хватает компетентности (признаю), чтобы грамотно оценивать скорость работы подобных вещей.
За синтаксический сахар надо платить производительностью и потреблением памяти, но в то же время мы получаем невероятное ускорение разработки и меньшее в разы количество кода, что благоприятно сказывается на количестве багов в коде.
И среди интерпретируемых языков Ruby совсем не медленный.
Да и задач где Ruby реально будет недоставать производительности на порядки меньше, чем тех где надо сделать побыстрее и в срок и эти узкие места можно написать на том же С++ и слинковать в Ruby.
Проблема с производительностью у автора поста в том, что он лямбды перекомпилирует на каждом обращении к функции rate_hash и автору надо научиться пользоваться гемом benchmark, на моём компьютере с core i7 разница всего в 4 раза. А если лямбды объявить раньше, то разница на 33%. И автор как раз и пытается выяснить оправдано ли улучшение читаемости кода к ухудшению производительности. Мой опыт показывает что да, оправдано (правда способ автора мне не нравится, вот как выше printercu написал так мне понятнее).
О лямбдах. Да, на какой-то конкретной платформе руби-лямбды работают медленнее, чем руби-методы, и что с того? Вам это что-нибудь дает? Мне лично нет. К вопросу о читаемости кода: давайте сделаем то же самое на Java8, там тоже теперь есть лямбды. Но там вам не нужно будет мучительно выбирать между читаемостью и производительностью, вы получите и то, и другое, и почти задаром, так что вышеприведенный эксперимент вообще потеряет смысл. Проблема руби в его уродливом синтаксисе и системе классов, которые вообще не классы, а @#$% знает что. Отсюда и подобные эксперименты. Работай автор на той же Java8, я уверен, ему бы и в голову не пришло этим заниматься.
Автор пытается рассуждать на тему: «что важнее читабельность кода или производительность?», в чём некорректность от использования в примерах Ruby?
«Сравнивать круг задач, для которых пригоден С++, а для которых руби — это, простите, какой-то моветон»
Они служат для разного класса задач и люди которые врываются их сравнивать в первую очередь должны задуматься, а нужны ли их сравнения. Могу вас удивить, но, в основном, люди которые пишут на Ruby, знают не только Ruby и они имеют представление о плюсах и минусах этого языка.
«своих рельсах (что тоже спорно, для реализации MVC паттерна лучше взять какой-нибудь Backbone.js и горя не знать»
Боюсь вам надо почитать про разницу между фронтендом и бэкендом.
«Да, на какой-то конкретной платформе руби-лямбды работают медленнее, чем руби-методы, и что с того? Вам это что-нибудь дает? Мне лично нет.»
Если вам это неинтересно, проходите мимо, автор правильно проставил тэги и есть люди которым это действительно интересно.
«К вопросу о читаемости кода: давайте сделаем то же самое на Java8, там тоже теперь есть лямбды.»
Ну давайте, напишите аналог этого кода экрана так на два… =) И запускать потом через пень колоду надо будет…
Боюсь что С++, Java и другим языкам подобного плана нет смысла соревноваться в читаемости и компактности кода с Руби, у них другие преимущества.
«Проблема руби в его уродливом синтаксисе и системе классов, которые вообще не классы, а @#$% знает что. Отсюда и подобные эксперименты»
При чём тут это вообще не понятно, подобные эксперименты появляются от того что людям непонятно: что лучше 100500 функций в 1-2 строки, или императивный подход где код лежит в одном месте. И подобные мысли могут посещать программистов на любых языках программирования общего пользования.
«Работай автор на той же Java8, я уверен, ему бы и в голову не пришло этим заниматься.»
Как показывает мой жизненный опыт, самые любители померятся 3,14письками (производительностью) — это те кто программируют на Go, C/C++ и Java (и может быть это вполне логично, потому что на них решают задачи где как раз производительность крайне критична).
Определите константу, присвойте ей вашу лямбду и внезапно выясните, что лямбда быстрее.
Вы говорите о причине, а я о следствии (вы меня не просветили, комментарием выше я этот момент уточнил уже)
Смысла в том, чтобы выносить в константы, абсолютно нет. Тогда нарушите инкапсуляцию, да и один черт код начнет глаза мозолить.
У меня в каждом пухлом классе есть вложенный приватный класс, который и определяет все эти константы. Расположен в самом конце файла, глаза никому не мозолит.
Просветил ли я вас, нет ли, — не имеет никакого значения. Ваш вопрос на самом деле звучит так: «имеет ли смысл выполнить операцию сто раз вместо одного, если это [на мой взгляд] улучшает читаемость». На этот вопрос ответ всегда «нет», и лямбды тут вообще ни при чем. Если же вы хотите предметно поговорить про лямбды — то ответ как раз обратный: да, лямбдами вместо методов пользоваться можно и нужно: это [незначительно] ускоряет код и, главное, делает его гораздо более читаемым.
О, странно, Ваш комментарий появился как-то поздновато.
Повторю то, что написал в ЛС:
Я придумал очень неудачный пример. А так да, кейс использования лямбд именно такой же, как и у Вас. Так что мне приятно слышать, что кто-то этим уже и так пользуется.
Спасибо!
и вам приходится вынести часть его кода в отдельный метод и ваш класс/модуль переполняется методами, которые относятся к одному единственному методу и нигде более не используется
и
Особенно неприятно, когда эти вспомогательные функции состоят из одной строки
это не примеры, а заглавная часть статьи, теория, так сказать. Именно к «одной строки», и «к единственному методу» у меня был вопрос. Может лямбда выглядит и более опрятно, но одна строка, которая используется один раз, причем она не делает независимую операцию (getAuthorId может состоять с одной строки, но операция определенно самостоятельная, впрочем, как и публичная), не имеет на мой взгляд смысла. А многократное использование — это совсем другое дело :)
rates.map{ |r| [r['CharCode'], r['Value'].to_f / r['Nominal'].to_f] }.to_h
Выше уже демонстрировалось подобное решение (кстати, написано оно лучше, чем у Вас) и я ответил по этому поводу.
Вот эта ветка: https://habrahabr.ru/post/303594/#comment_9663324
Использование lambda в качестве локальных функций