Комментарии 50
Стрелочный функции это короткая запись анонимных функций. В принципе это всё
Ну нет. Гораздо важнее, что они сохраняют контекст (точнее, не создают свой собственный, что теоретически делает их быстрее там, где они поддерживаются нативно).
А так да, именованные функции, особенно если они длиннее 1-2 строк, проще читать и поддерживать (если они нормально названы, конечно:)).
Как насчёт такого варианта?
class $my_app {
itemsRaw() {
return $mol_http.resource( '/api' ).json().arr
}
itemsNormal() {
var items = this.itemsRaw()
items.forEach( item => {
if( !item.prop ) return
item.prop *= 2
} )
return items
}
itemsFiltered() {
return this.itemsNormal().filter( item => {
return item.props.some( prop => prop.a > 10 )
} )
}
}
Точнее так:
class $my_app {
itemsRaw() {
return $mol_http.resource( '/api' ).json().arr
}
itemsNormal() {
var items = this.itemsRaw()
items.forEach( item => item.prop.forEach( prop => {
if( !prop ) return
prop.a *= 2
} ) )
return items
}
itemsFiltered() {
return this.itemsNormal().filter( item => {
return item.props.some( prop => {
return prop.a > 10
} )
} )
}
}
Или даже так:
class $my_app {
itemsFiltered() {
return this.itemsNormal().filter( item => this.itemCheck( item ) )
}
itemCheck( item ) {
return item.props.some( prop => prop.a > 10 )
}
itemsNormal() {
return this.itemsRaw().map( item => this.itemNormalize( item ) )
}
itemNormalize( item ) {
item.prop.forEach( prop => {
if( !prop ) return
prop.a *= 2
} )
return item
}
itemsRaw() {
return $mol_http.resource( '/api' ).json().arr
}
}
Например, за этим:
class $my_app_prefixed extends $my_app {
itemsNormal() {
return [ this.itemPrefix() ].concat( super.itemsNormal() )
}
itemPrefix() {
return {
prop : [
{ a : 10 }
]
}
}
}
Это процедуры (itemNormalize) и конвертеры (itemCheck) должны быть глаголами. Геттеры же (itemsFiltered, itemsNormalized, itemsRaw) вполне могут именоваться и существительными.
Ну и конечно в случае, если принципиально важно сохранить this.
Во всем остальном лучше традиционные.
get(...).then(result => processResult(resut))
get(...).then(processResult.bind(this))
Конкретно в данном случае можно и так, но в общем случае так лучше не делать. Иллюстрация:
'33333'.split( '' ).map( parseInt ) // [3, NaN, NaN, NaN, 3]
Array#map
— передает функции обратного вызова 3 аргумента, элемент массива, индекс и сам массив.
parseInt(string, radix);
— принимает 2 аргумента строку и целое число в диапазоне между 2 и 36, представляющее собой основание системы счисления.
parseInt(3, 0) // = 3
parseInt(3, 1) // = NaN
parseInt(3, 2) // = NaN
parseInt(3, 3) // = NaN
parseInt(3, 4) // = 3
$http.request('GET', '/api')
.then(data =>
{
return data.arr
.map(item =>
{
return item.props
.filter(prop => Boolean(prop))
.map(prop =>
{
prop.a = prop.a * 2;
return prop;
});
})
.filter(item =>
{
return item.props.some(prop => prop.a > 10);
});
})
.catch(e => new TypeError(error));
Стало понятнее? На мой взгляд вся проблема тут не в =>
или function
, а в том, что используя множество вложенных функций, такой стиль кода напрочь убивает всю читаемость. Сложно понять, что где начинается, а что где заканчивается. В моём варианте я делаю отступы в цепочках (т.е. уже сами цепочки обособлены, не запутаешься), причём первый же метод в цепочке начинается на 2-й строке (с той же целью — читаемость).
А вместо длинных наименований новых методов проще вставлять комментарии. К примеру
.filter(item =>
{
// бла-бла-бла
return item.props.some(prop => prop.a > 10);
});
Кстати говоря, возвращать из .map тот же самый элемент, не совсем канонично :)
Про .map хорошо подметили, если не важна мутабельность, тогда уж использовать map по назначению, и тогда всё выглядит ещё более читабельно:
Вместо
.map(prop =>
{
prop.a = prop.a * 2;
return prop;
});
С object spread будет
.map(prop => ({...prop, a: prop.a * 2}))
или если spread режет глаз
.map(prop => Object.assign({}, prop, {a: prop.a * 2}))
Если всё-таки важна мутабельность и сохранение цепочки, то Object.assign снова же можно заюзать
.map(prop => Object.assign(prop, {a: prop.a * 2}))
Тут проблема в том, что метод оторван от контекста. Расположен чёрт знает где и не видно окружения его вызова. Да и именование метода сильно уступает комментарию по выразительности. Если писать вот в таком вот callback-стиле, то попытка раздербанить один средней сложности вложенный кусок кода на полтора десятка методов с длинными непонятными названиями приведёт к полной дезориентации. Как собственно у автора и получилось с его transformResultArr
, transformProperty
и checkProperty
. Мусорные названия однострочных методов вырванных из контекста и расположенных в произвольном порядке. А всего то нужно было отступы правильно задать.
В общем тут палка о двух концах. Когда стрелочные анонимки жиреют и становятся сложными — их нужно выносить вовне. Или же когда речь идёт о переиспользуемости кода.
async/await
касается только асинхронного кода. Синхронные цепочки трансформации данных никуда не деваются. Хотя наверное "callback-ый ад" — это не про них :) Но приведённый в примерах код как раз о них.
Забыл сказать, мне в scala
понравилось то, как удобно там организованы простые случаи callback
-ов:
collection
.map(el => el.export())
.filter(el => el.age > 10)
// =>
collection.map(_.export).filter(_.age > 10)
Плюс стандартная библиотека умеет большую часть насущных вещей из lodash.js
.
прекрасный async/await
В том, что у вас весь код усеян асинками да авайтами нет ничего прекрасного. В ноде можно писать безо всей этой ерунды:
var Future = require( 'fibers/future' )
var Fetch = require( 'fetch-promise' )
class Transport {
fetch( uri ) {
return Future.fromPromise( Fetch( uri ) ).wait()
}
fetchJSON( uri ) {
return JSON.parse( this.fetch( uri ).buf )
}
fetchTable( uri ) {
return this.fetchJSON( uri ).data.children.map( ({ data })=> data )
}
}
Не желаете написать обзорную статью про fibers
в nodeJS
? :) Было бы интересно узнать про все подводные камни и сравнение с теми же async-promise-ми. Такие вопросы как производительность, принцип работы волокон (признаться, я вот так его и не понял), обработка ошибок, сегфолты (когда баловался — часто их наблюдал). В частности интересно, почему такой подход в nodeJS есть давно, но всё остаётся уделом одиночек?
Давно хочу написать/записать, но руки не доходят.
Async функция — это фактически обёртка над генератором. Генератор — это фактически конечный автомат. А конечный автомат — это фактически switch-case с выбором ветки кода в зависимости от значения счётчика.
Волокно — это указатель на стек. В любой момент вы можете "заморозить" текущее волокно, передав управление другому (изменив указатель на другой стек), а по какому-либо событию "разморозить" и продолжить исполнение. Так как изменение указателя — тривиальная операция, волокна ещё называют "легковесными потоками". То есть они обеспечивают многопоточность даже в однопоточной среде.
Ошибки обрабатываются стандартно — через try-catch. При этом рекомендуется использовать Future (это аналог промисов, но вместо then и catch, используется wait), которые исправляют стектрейсы.
С сегфолтами не сталкивался, но расширение это довольно зрелое. А мало кем используется потому, что нет хайпа и в браузере не заведётся.
Минусы волокон:
- Это не стандарт.
- Это бинарное расширение.
- Работает только в NodeJS.
- Отладчик (во всяком случае в вебшторме) не может получить значения переменных после пробуждения волокна.
- Большее потребление памяти в сравнении с генераторами (необходимо хранить стек волокна).
Плюсы волокон:
- Это расширение платформы, а не языка.
- Асинхронность инкапсулируется в одной функции и не распространяется как вирус по всему приложению.
- Больше скорость в сравнении с генераторами. Накладные расходы только на создание и переключение волокна. Генератор даёт пенальти на каждый свой вызов.
Ну и для полноты картины: сравнение кодов с разной реализацией асинхронности.
Я там по ссылке добавил замеры времени.
- Самой быстрой, неожиданно, оказалась асинхронная версия на атомах. Видимо сказывается минимум замыканий.
- Незначительно отстаёт синхронная версия.
- С почти двухкратным отставанием идут асинхронные версии на колбэках и файберах.
- Чуть погодя — асинхронные версии на промисах и генераторах.
- И ещё в 3 раза медленней — то, что сгенерировал Babel из async/await.
Комментарии всегда намного хуже отдельных методов с говорящими названиями.
Это чем же?
- вы дублируете говорящее_название_метода дважды (вызов + определение метода)
- вы ограничены правилами именования методов, да ещё и codeStyle-ом
- имя метода максимум 1 строка
- тело метода (в данном контексте одно-двустрочник) располагается отдельно (и возможно далеко) от места использования
- для того чтобы имя метода стало говорящим в отрыве от контекста может потребоваться намного больше слов, чем комментарию по месту
- в зависимости от области видимости может возникнуть именная коллизия
За примерами ходить не нужно — в статье их навалом. Самый глупый метод — checkProperty
. Вообще ни о чём не говорящее название. А при использовании lodash всего то потребовалось бы .compact()
. По сути если стараешься писать в около-функциональном стиле, то выносить одно-двух строчные очевидные действия в отдельные методы — сильно ухудшать кодовую базу. Всё сразу становится сложным и непонятным.
Если же есть несколько строк кода, выполняющих какую-то одну цельную _функцию_ с неким смыслом — они так и должны быть выделены в функцию. Где будет ее тело, не особо важно, т.к. в IDE есть переход к определению и обратно.
Отдельно остановлюсь тут — «вы дублируете говорящее_название_метода дважды (вызов + определение метода)».
Извините, но это не дублирование. Дублирование — это фразы, имеющие одинаковый смысл. Вызов и определение метода никаким дублированием, конечно, не является. А вот когда блоки кода не выносятся в функцию — это провоцирует и дублирование, и что еще хуже — написание заново кода с тем же функционалом в другом месте.
В этом плане обычные замыкания от «стрелочных» никак не отличаются. Но боятся этого надо когда такое создание/уничтожение имеет массовый характер.
И можно было бы сделать (и имело бы смысл) код как в примере №2 — он был бы самым быстрым и про однократной работе коде, и, особенно, при многократной.
К слову, в примере со стрелочными функциями data.arr заполнится undefined.
А приведенный пример остается достаточно понятным и читабельным как-нибудь так (дальнейшая декомпозиция зависит уже от контекста и условий задачи, тестирования и многих факторов):
const requestErrorHandler =…
const requestSuccessHandler = ({ arr }) => arr
.map(({ props }) => props
.filter(prop => !!prop)
.map(prop => ({ ...prop, a: prop.a * 2 })))
.filter(({ props }) => props
.some(({ a }) => a > 10));
$http
.request('GET', '/api')
.then(requestSuccessHandler, requestErrorHandler);
const requestErrorHandler = …
const requestSuccessHandler = ({ arr }) => arr
.map(({ props }) => props
.filter(prop => !!prop)
.map(prop => ({ ...prop, a: prop.a * 2 })))
.filter(({ props }) => props
.some(({ a }) => a > 10));
$http
.request('GET', '/api')
.then(requestSuccessHandler, requestErrorHandler);
Мне кажется, что до тех пор пока вы используете только методы массива все нормально: array.map().map().filter().map().etc()
. Сложности начинаются когда это все в одном месте с промисами или чем-то похожим, т.е. в вашем примере достаточно вынести код трансформирующий результат в отдельный метод и всем все будет ясно, можно дополнительно написать обертку над request
которая будет принимать опциональную функцию трансформер.
Именно о таком я и писал :)
Просто в статью слишком большие примеры плохо пихать, так что я ограничился парой небольших абстрактных примеров.
А horizon.io явно местами переборщили. К примеру такая запись
const read_config_from_env = () => {}
вместо обычного
function read_config_from_env () {}
по моему уже верх абсурда в желании использовать только новые фичи языка.
Но предлагаю особо не холиварить по их поводу здесь, иначе это растянется не на одну сотню сообщений, уйдя далеко от темы, поскольку стрелки не единственная (и по моему не самая больня) проблема, которую у них можно откопать и разобрать (файлы на 700+ строк почти всегда очень интересные :))).
Стрелочный ад, или новый круг старой проблемы