Пристрастията към познанието ви задържат: Време е да възприемете стрелките

Преподавам JavaScript за прехрана. Наскоро се разбърках около учебната си програма, за да уча по-бързо функциите на стрелата - в рамките на първите няколко урока. Преместих го по-рано в учебната програма, защото това е изключително ценно умение и учениците вдигат къри със стрели много по-бързо, отколкото си мислех, че биха го направили.

Ако те могат да го разберат и да се възползват от него по-рано, защо да не го научим по-рано?

Забележка: Моите курсове не са предназначени за хора, които никога досега не са докосвали ред код. Повечето студенти се присъединяват, след като прекарат поне няколко месеца кодиране - самостоятелно, в начален старт или професионално. Въпреки това видях много млади разработчици с малък или никакъв опит да избират бързо тези теми.

Видях куп ученици да се запознаят добре с функциите на стрелите със стрели в рамките на един час. (Ако сте член на „Научете JavaScript с Ерик Елиът“, можете да гледате 55-минутния урок за Curry & Composition на ES6 точно сега).

Виждайки колко бързо студентите го вдигат и започват да притежават своите новооткрити правомощия за къри, винаги съм малко изненадан, когато публикувам функциите на стрелите в Twitter, а Twitterverse отговаря с възмущение при мисълта да нанесе този „нечетлив“ код на хората, които ще трябва да го поддържат.

Първо, нека ви дам пример за това, за което говорим. Първият път, когато забелязах обратната реакция, беше реакцията на Twitter към тази функция:

const secret = msg => () => msg;

Бях шокиран, когато хората в Twitter ме обвиниха, че се опитвам да объркам хората. Написах тази функция, за да демонстрирам колко лесно е да се изразяват изкривени функции в ES6. Това е най-простото практическо приложение и израз на затваряне, което мога да се сетя в JavaScript. (Свързано: „Какво е закриване?“).

Той е еквивалентен на следния израз на функция:

const secret = функция (msg) {
  функция за връщане () {
    връщане msg;
  };
};

secret () е функция, която отнема msg и връща нова функция, която връща msg. Възползвайте се от затваряния, за да фиксирате стойността на msg към каквато и стойност, която предавате в тайна ().

Ето как го използвате:

const mySecret = таен ('здравей');
моята тайна(); // 'здравей'

Оказва се, че „двойната стрела“ е това, което обърква хората. Убеден съм, че това е факт:

С запознанство функциите със стрелки в редовете са най-четим начин за изразяване на функциите за извиване в JavaScript.

Много хора ми спориха, че по-дългата форма се чете по-лесно от по-късата. Те са частично прав, но най-вече наред. Той е по-многословен и по-ясен, но не по-лесен за четене - поне не на някой, запознат със функциите на стрелките.

Възраженията, които видях в Twitter, просто не съвпадаха с гладкото учене, с което се наслаждаваха моите ученици. Според моя опит учениците поемат към функции със стрели като рибите, които водят до вода. До дни след като ги научите, те са едно със стрелките. Правят ги без усилия за справяне с всякакви предизвикателства с кодирането.

Не виждам никакъв знак, че функциите на стрелки са „трудни“ за тях да се научат, да четат или да разберат - след като направят първоначалната инвестиция да ги научат в рамките на няколко едночасови уроци и учебни сесии.

Те лесно четат функциите със стрели, които никога не са виждали, и ми обясняват какво се случва. Те естествено пишат своето, когато им представям предизвикателство.

С други думи, веднага щом се запознаят с виждането на функции със стрели, те нямат проблеми с тях. Те ги четат толкова лесно, колкото четете това изречение - и тяхното разбиране се отразява в много по-опростен код с по-малко грешки.

Защо някои хора мислят, че изразите на наследствени функции изглеждат „по-лесни“ за четене

Пристрастието към познанието е измеримо човешко когнитивно пристрастие, което ни кара да вземаме саморазрушителни решения, въпреки че сме наясно с по-добрия вариант. Продължаваме да използваме същите стари модели, въпреки че знаем за по-добри модели извън комфорта и навика.

Можете да научите много повече за пристрастията към познаването (и много други начини, по които се заблуждаваме) от отличната книга „Проектът за отмяна: Приятелство, което промени нашите умове“. Тази книга трябва да се чете за всеки разработчик на софтуер, защото ви насърчава да мислите по-критично и да тествате предположенията си, за да избегнете попадането на различни когнитивни капани - а историята за това как са открити тези когнитивни капани е наистина добра, също ,

Наследените изрази на функции вероятно причиняват грешки във вашия код

Днес аз пренаписвах функция на извита стрелка от ES6 на ES5, така че да мога да я публикувам като модул с отворен код, който хората могат да използват в стари браузъри, без да се прехвърлят. Версията ES5 ме шокира.

Версията ES6 беше проста, къса и елегантна - само 4 реда.

Мислех със сигурност, че това е функцията, която ще докаже на Twitter, че стрелните функции са по-добри и че хората трябва да се откажат от наследените си функции като лошия навик, който са.

Така аз туитах:

Ето текста на функциите, в случай че изображението не работи за вас:

// извита със стрелки
const composeMixins = (... mixins) => (
  instance = {},
  mix = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => микс (... миксинс) (пример);
// срещу ES5-стил
var composeMixins = функция () {
  var mixins = [] .slice.call (аргументи);
  функция за връщане (например, микс) {
    if (! instance) instance = {};
    ако (! микс) {
      микс = функция () {
        var fns = [] .slice.call (аргументи);
        функция за връщане (x) {
          връщане fns.reduce (функция (acc, fn) {
            връщане fn (acc);
          }, х);
        };
      };
    }
    връщане mix.apply (null, mixins) (например);
  };
};

Въпросната функция е обикновена обвивка около тръба (), стандартна функционална програма за програмиране, обикновено използвана за съставяне на функции. Функция pipe () съществува в lodash като lodash / flow, в Ramda като R.pipe () и дори има собствен оператор в няколко функционални езика за програмиране.

Тя трябва да е позната на всички, запознати с функционалното програмиране. Както следва и основната му зависимост: Намаляване.

В този случай се използва за съставяне на функционални миксове, но това е неподходяща подробност (и цяла друга публикация в блога). Ето важните подробности:

Функцията поема произволен брой функционални миксини и връща функция, която ги прилага една след друга в тръбопровод - като сборна линия. Всеки функционален миксин приема инстанцията като вход и връзва някои неща върху него, преди да го прехвърли към следващата функция в тръбопровода.

Ако пропуснете инстанцията, за вас се създава нов обект.

Понякога може да искаме да съставим миксините по различен начин. Например, може да искате да прекарате compose () вместо pipe (), за да обърнете реда на приоритета.

Ако не е необходимо да персонализирате поведението, просто оставяте по подразбиране сам и получавате стандартно поведение ().

Само фактите

Мнения за четимост настрана, ето обективни факти, отнасящи се до този пример:

  • Имам многогодишен опит както с ES5, така и с ES6 функционални изрази, стрелки или друго. Пристрастието към познанието не е променлива в тези данни.
  • Написах версията ES6 за няколко секунди. Той съдържаше нулеви грешки (които знам - той преминава през всичките си тестови единици).
  • Отне ми няколко минути, за да напиша версията на ES5. Поне с порядък повече време. Минути срещу секунди Два пъти загубих мястото си във вдлъбнатините на функциите. Написах 3 грешки, всички от които трябваше да отстраня грешки и да коригирам. Две от които трябваше да прибягна до console.log (), за да разбера какво става.
  • Версията ES6 е 4 реда код.
  • Версията на ES5 е дълга 21 реда (17 всъщност съдържат код).
  • Въпреки досадната многословност, версията ES5 всъщност губи част от информационната вярност, която е налична във версията ES6. Много по-дълго е, но общува по-малко, четете за подробности.
  • Версията ES6 съдържа 2 спреда за функционални параметри. Версията ES5 пропуска разпръскванията и вместо това използва обект за неявни аргументи, което навреди на четимостта на подписа на функцията (верност надолу 1).
  • Версията ES6 дефинира по подразбиране за микс във подписа на функцията, така че можете ясно да видите, че е стойност за параметър. Версията ES5 скрива този детайл и вместо това го скрива дълбоко в функционалното тяло. (понижаване на верността 2).
  • Версията ES6 има само 2 нива на отстъп, което помага да се изясни структурата на начина, по който трябва да се чете. Версията ES5 има 6 и нивата на влагане са по-неясни, а не подпомагат четливостта на структурата на функцията (вярност 3).

Във версията ES5, pipe () заема по-голямата част от тялото на функцията - толкова много, че е малко налудничаво да се дефинира в него. Това наистина трябва да се раздели на отделна функция, за да направи версията на ES5 четена:

var pipe = функция () {
  var fns = [] .slice.call (аргументи);
  функция за връщане (x) {
    връщане fns.reduce (функция (acc, fn) {
      връщане fn (acc);
    }, х);
  };
};
var composeMixins = функция () {
  var mixins = [] .slice.call (аргументи);
  функция за връщане (например, микс) {
    if (! instance) instance = {};
    ако (! микс) микс = тръба;
    връщане mix.apply (null, mixins) (например);
  };
};

Това ми се вижда очевидно по-четимо и разбираемо.

Нека видим какво се случва, когато приложим същата „оптимизация“ за четене към версията на ES6:

const pipe = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);
const composeMixins = (... mixins) => (
  instance = {},
  микс = тръба
) => микс (... миксинс) (пример);

Подобно на оптимизацията на ES5, тази версия е по-подробна (добавя нова променлива, която не е била там преди). За разлика от версията ES5, тази версия не е значително по-четена след абстракция на дефиницията на тръбата. В края на краищата тя вече има име на променлива, ясно й е присвоено в подписа на функцията: mix.

Определението за микс вече се съдържаше в неговия собствен ред, което прави малко вероятно читателите да се объркват къде ще свърши и останалата част от функцията продължава.

Сега имаме 2 променливи, представляващи едно и също нещо вместо 1. Много ли сме спечелили? Не очевидно, не.

Така че защо ES5 версията очевидно е по-добра със същата функция абстрахирана?

Защото ES5 версията очевидно е по-сложна. Източникът на тази сложност е същността на този въпрос. Аз твърдя, че източникът на сложността се свежда до синтактичен шум и че синтаксисният шум затъмнява значението на функцията, не помага.

Нека сменим предавките и елиминираме още няколко променливи. Нека използваме ES6 и за двата примера и сравняваме само стрелките и наследените изрази на функции:

var composeMixins = функция (... mixins) {
  функция за връщане (
    instance = {},
    mix = функция (... fns) {
      функция за връщане (x) {
        връщане fns.reduce (функция (acc, fn) {
          връщане fn (acc);
        }, х);
      };
    }
  ) {
    връщане микс (... миксини) (пример);
  };
};

Това изглежда значително по-четено за мен. Всичко, което променихме, е, че се възползваме от синтаксиса на параметъра за почивка и параметри по подразбиране. Разбира се, ще трябва да сте запознати с синтаксиса за почивка и подразбиране, за да може тази версия да бъде по-четена, но дори и да не сте, мисля, че е очевидно, че тази версия все още е по-малко затрупана.

Това помогна много, но все още ми е ясно, че тази версия все още е достатъчно затрупана, че абстрахирането на тръбата () в нейната собствена функция очевидно би помогнало:

const pipe = функция (... fns) {
  функция за връщане (x) {
    връщане fns.reduce (функция (acc, fn) {
      връщане fn (acc);
    }, х);
  };
};
// Legacy функции изрази
const composeMixins = функция (... mixins) {
  функция за връщане (
    instance = {},
    микс = тръба
  ) {
    връщане микс (... миксини) (пример);
  };
};

Това е по-добре, нали? Сега, когато задаването на микс заема само един ред, структурата на функцията е много по-ясна - но все още има твърде много синтаксичен шум за моя вкус. В composeMixins () не ми е ясно от пръв поглед къде свършва една функция и започва друга.

Вместо да извиква функционални тела, тази функционална ключова дума изглежда визуално се съчетава с идентификаторите около нея. Има функции, криещи се в моята функция! Откъде започва подписването на параметъра и тялото на функцията? Мога да го разбера, ако погледна внимателно, но не ми е визуално очевидно.

Какво ще стане, ако успеем да се отървем от ключовата дума на функцията и да извикаме стойности за връщане, като визуално ги посочим с голяма тлъста стрелка => вместо да напишем ключова дума за връщане, която се съчетава с околните идентификатори?

Оказва се, че можем и ето как изглежда:

const composeMixins = (... mixins) => (
  instance = {},
  микс = тръба
) => микс (... миксинс) (пример);

Сега трябва да е ясно какво се случва. composeMixins () е функция, която приема произволен брой миксини и връща функция, която приема два незадължителни параметъра, например и микс. Той връща резултата от тръбния екземпляр чрез съставените миксини.

Само още нещо ... ако приложим същата оптимизация към pipe (), тя магически се трансформира в еднолинейна:

const pipe = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);

С тази дефиниция на една линия, предимството да я абстрахирате в собствената си функция е по-малко ясно. Не забравяйте, че тази функция съществува като помощна програма в Lodash, Ramda и куп други библиотеки, но наистина ли си струва разхода за импортиране на друга библиотека?

Дали си струва дори да го издърпате в собствената си линия? Вероятно. Те наистина са две различни функции и разделянето им прави това по-ясно.

От друга страна, въвеждането му в реда изяснява очакванията за типа и употребата, когато погледнете подписа на параметъра. Ето какво се случва, когато го създадем онлайн:

const composeMixins = (... mixins) => (
  instance = {},
  mix = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => микс (... миксинс) (пример);

Сега се върнахме към оригиналната функция. По пътя не сме изхвърлили никакво значение. Всъщност, като декларираме нашите параметри и стойности по подразбиране, ние добавихме информация за това как се използва функцията и как могат да изглеждат стойностите на параметрите.

Целият допълнителен код във версията ES5 беше само шум. Синтаксисен шум. Той не е използвал никаква полезна цел, освен да аклиматизира хора, непознати с функциите на стрелата.

След като сте се запознали достатъчно с функциите на стрелите, трябва да е ясно, че оригиналната версия е по-четена, тъй като има много по-малко синтаксис, в който да се загубите.

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

Подозирам, че има много грешки, които се крият в наследени функции, които ще бъдат намерени и елиминирани, ако трябва да надстроите до стрелките.

Също така се надявам, че вашият екип ще стане значително по-продуктивен, ако се научите да възприемате и предпочитате повече от краткия синтаксис, наличен в ES6.

Макар и да е вярно, че понякога нещата са по-лесни за разбиране, ако са направени изрично, но също така е вярно, че като цяло по-малко код е по-добър.

Ако по-малко код може да постигне едно и също нещо и да комуникира повече, без да жертва никакво значение, това е обективно по-добре.

Ключът към познаването на разликата е смисълът. Ако повече код не успее да добави повече значение, този код не трябва да съществува. Тази концепция е толкова основна, тя е добре позната стилова насока за естествен език.

Същото ръководство за стил важи за изходния код. Прегърнете го и вашият код ще бъде по-добър.

В края на деня светлина в тъмнината. В отговор на поредното чуруликане, казващо, че версията ES6 е по-нечетлива:

Време е да се запознаете със състава на ES6, къри и функция.

Следващи стъпки

Членовете на „Научете JavaScript с Ерик Елиът“ могат да гледат 55-минутния урок за Curry & Composition на ES6 в момента.

Ако не сте член, пропускате!

Ерик Елиът е автор на „Програмиране на JavaScript приложения“ (O’Reilly) и „Научете JavaScript с Ерик Елиът“. Той е допринесъл за софтуерни преживявания за Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC и най-добрите звукозаписни артисти, включително Usher, Frank Ocean, Metallica и много други.

Той прекарва по-голямата част от времето си в района на залива Сан Франциско с най-красивата жена в света.