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

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

Ако пишете 100% без правописни грешки, перфектен код през цялото време, това може да ви се стори малко сухо. Или ако смятате, че би било добре Apple да премахне клавиша за връщане назад, както направиха с клавиша за изход, вероятно трябва да продължите :/

Целта на това не е да обезкуражи никого да пише в по-функционален стил. Нито пък обезсърчава използването на някоя от функционалните библиотеки със страхотно качество като Ramda, lodash_fp или Sanctuary. Обичам да пиша код в по-функционален стил и разбирам много добри причини да го използвам и популяризирам.

Стига с отказите от отговорност.

Открих, че в определени контексти, особено при използване на стил на програмиране tacit / point-free в Javascript, това може да доведе до объркване на следите на стека и бариери пред отстраняването на грешки.

Нека започнем, като разгледаме проследяване на стека от неочаквана грешка в код на някакъв функционален стил. Вероятно е просто някаква глупава печатна грешка, в този случай е от нещо доста „просто“, използващо библиотеката „Ramda“:

Когато виждам подобно нещо, не мога да не си спомня разочарованието, което видях и изпитах, докато пиша и отстранявам грешки на Angularjs 1.x конзолни грешки.

В тази ситуация може да бъде доста изкушаващо да мислите наивно:

„Ако стекът за извикване на грешка показва само редове от зависимост, следователно грешката трябва да е грешка в зависимостта!“

Мисъл, която никой програмист никога не е имал ← Сарказъм

Колко проблеми в github и т.н. сте виждали затворени от някой, който коментира нещо като:

„Уау, извинете моя грешка, не видях...“

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

Това, което можем да видим в стека за извикване, са имена на функции от много общи функционални модели като pipe, curry и map. Проблемът е, че вероятно ги използвате в цялата си кодова база. Така че това е почти толкова добро, колкото да знаете, че пътеката на кода използва if и else изрази. Безполезно, ако това идва от производството, успех в знанието къде да започнете ефективно отстраняване на грешки на проблема.

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

Това е комбинация от:

  • „Функционално къри“
  • Стилове мълчаливо / без точки.
  • Динамичната природа на Javascript
  • Асинхронни методи
  • Големи количества библиотечен код

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

Сравняване на императивни и функционални примери

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

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

const response = {
  messages: [
    {
      user: {
        role: 'The first ones',
        name: 'Kosh',
        species: 'Vorlon'
      },
      text: 'Who are you?'
    }
  ]
}

От нас се изисква да покажем текст, разделен със запетая, в изглед. Текстът трябва да съдържа списък с всички потребителски имена и техните видове. Всеки потребител трябва да бъде в следния формат:

`${userName} is a ${species}`
// Eg =>  Kosh is a Vorlon, ...

За да илюстрираме, нека приложим това, като използваме няколко различни стила.

Функционално без точки

const userDisplayText = R.pipe(
  R.prop(['user']),
  R.props(['name', 'species']),
  R.intersperse('is a'),
  R.join(' ')
)
const messagesDisplayText = R.pipe(
  R.prop('messages'),
  R.map(userDisplayText),
  R.join(', ')
)

Императивен

function messagesDisplayText(response) {
  var users = []
  for (var i = 0; i < response.messages.length; i++) {
    var user = response.messages[i].user
    var details = [
      user.name,
      'is a',
      user.species
    ]
    users.push(details.join(' '))
  }
  return users.join(', ');
}

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

const response = {
  messages: [
    {
      user: {
        name: 'Kosh',
        species: 'Vorlon'
      },
      text: 'Who are you?'
    },
    {
      user: null,
      text: 'What do you want?',
    },
  ]
}

Как можехме да знаем? :(

Ако познахте, да, това е причината за грешката, която видяхте за първи път в началото на публикацията.

Нека видим това, докато използвате „Пауза при изключение“ в Chromium и сравним стиловете един до друг:

Функционален:

наложително:

В този конкретен сценарий е почти като черно и бяло. Императивният стек за извикване ви показва точното име на функцията и номера на реда, от които се нуждаете, за да разберете проблема.

Защо да продължите с функционален стил?

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

Извън този пример причината за проблема може да не е от нещо толкова просто или толкова малко вероятно, като отговор на сървъра, който връща неочакван подпис на полезен товар. Знаейки защо стойността е null може да бъде много по-малко тривиално.

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

Дискусията Functional vs Imperative е извън обхвата на тази публикация. Така че свалете бойните ръкавици, вероятно всичко е казано преди ;)

Както и да е, надеждата не е загубена в разбирането на грешки като тази, нека работим с някои техники за отстраняване на грешки.

Черен бокс Devtools

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

И така, използвайки примера по-горе, ето „Пауза при неуловени изключения“ на функционалния пример с всички линии на Ramda в черна кутия:

Ура в този случай, след като скрих 20 кадъра на Ramda, вече можете да видите къде съм написал код на main.tsx:24. Точно тук извиках метода без точки, който води до изключението.

Полезно е да знаете, че тези филтри могат да останат постоянни при презареждане и също така да поддържат Regex, ако трябва да филтрирате нещо по-специфично. Blackboxing също има „тези ефекти“, за които трябва да знаете:

  • Изключенията, хвърлени от кода на библиотеката, няма да поставят на пауза (ако е разрешена Пауза при изключения),
  • Стъпването в/навън/отвъд заобикаля кода на библиотеката,
  • Точките на прекъсване на слушателя на събития не нарушават кода на библиотеката,
  • Дебъгерът няма да постави на пауза никакви точки на прекъсване, зададени в кода на библиотеката.

Ограничение за проследяване на стека

Ако пишете много код с функционален стил, обичайно е проследяването на стека да е доста голямо. Поради очевидни причини, свързани с ефективността, браузърите поставят ограничения. Така че, ако наистина сте в затруднение, някои браузъри като Chromium ви позволяват да увеличите ограничението за проследяване на стека чрез този глобален API:

Error.stackTraceLimit = number

Имайте предвид, че този API не е нещо, предназначено за използване в производството.

Обвиване на именувани функции

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

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

Можете да добавите именувана функция към трасирането на стека на стилизирана функция без точки само като я обвиете в едно:

function namedMessagesDisplayText(response) {
  // the point-free style function
  return messagesDisplayText(response)
}

Сега, като инспектирате стека на повикванията, трябва да видите името на функцията namedMessagesDisplayText:

Имайки това предвид, бихте могли да избегнете излишната функция, като се въздържате от чисто point-free стил и вместо това напишете това:

function messagesDisplayText (response) {
  return R.pipe(
    R.prop('messages'),
    R.map(userDisplayText),
    R.join(', ')
  )(response)
}

Помощници за регистриране

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

Същността е просто да създадете функции за регистриране, които можете да вмъкнете във вашите композиции:

const traceUser = (data) => {
  console.log('the user', data)
  return data
}
const userLabelText = R.pipe(
  R.prop(['user']),
  traceUser, // <-- just another part of the pipe :)
  R.props(['name', 'species']),
  R.intersperse('is a'),
  R.join(' ')
)

Сега във вашата конзола или помощни средства за регистриране можете да видите какво се случва:

// the user {role: "The first ones", name: "Kosh", species: "Vorlon"}
// main.tsx:26 the user null

Точки на прекъсване

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

Ако се опитате да поставите точка на прекъсване на линия в канал или да композирате, просто няма да работи. Или ако се опитате да „влезете“ във функционален модел като „R.cond“, който по същество капсулира if/else, if/else, ще трябва да преминете през кода на библиотеката, увеличавайки размера на стека на повикванията. Ако беше само if {} else .. или може би switch израз, дебъгерът ще премине само през вашия код.

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

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

const debug = item => {
  debugger // <-- break here
  return item
}
const userLabelText = R.pipe(
  R.prop(['user']),
  debug, // <-- put this wherever in the pipe you need to
  R.props(['name', 'species']),
  R.intersperse('is a'),
  R.join(' ')
)

Намирам това за особено полезно при изучаване на apis и мислене във функционален стил.

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

Тъй като това е точно като всеки друг израз във вашия код, можете също да го обвиете във всеки израз, за ​​да имате условна точка на прекъсване:

const debug = item => {
  if (item === null) debugger
  return item
}

Маймунски кърпежи

Интересен подход, предназначен само за разработка, е да се обвие функция от по-висок ред, за да се улови контекст като името на функцията и персонализираното проследяване на стека чрез хвърляне и улавяне на грешка. @jacobp100има страхотен пример с използване на Ramda тръба, който си струва да се провери. Предполагам, че тази идея може да бъде полезна по други творчески начини.

Тип система за изпълнение

За предоставяне на някои по-полезни съобщения за грешка може да помогне допълнителен контекст от средата на изпълнение. Sanctuary позволява съобщения и документация на място. Те са подобни на съобщение за грешка „power assert like“:

S.add(2, true);
// ! TypeError: Invalid value
//
//   add :: FiniteNumber -> FiniteNumber -> FiniteNumber
//                          ^^^^^^^^^^^^
//                               1
//
//   1)  true :: Boolean
//
//   The value at position 1 is not a member of ‘FiniteNumber’.
//
//   See https://github.com/sanctuary-js/sanctuary-def/tree/v0.14.0#FiniteNumber for information about the sanctuary-def/FiniteNumber type.

Проверка на типа по време на компилиране

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

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

В някои сценарии открих, че стриктното въвеждане на всичко религиозно може да бъде доста тромаво с малка възвращаемост. Например, помислете за стриктно въвеждане на голям pipe или compose с Generics от дефинициите на Ramda @types/ramda.

За сравнително голяма тръба помислете за стриктно съответствие с този интерфейс:

pipe<V0, T1, T2, T3, T4, T5, T6, T7, T8, T9>(
    fn0: (x0: V0) => T1,
    fn1: (x: T1) => T2,
    fn2: (x: T2) => T3,
    fn3: (x: T3) => T4,
    fn4: (x: T4) => T5,
    fn5: (x: T5) => T6,
    fn6: (x: T6) => T7,
    fn7: (x: T7) => T8,
    fn8: (x: T8) => T9): (x0: V0) => T9;

Друг по-малък пример, без прекъсвания на редовете:

compose<V0, T1, T2, T3, T4, T5, T6>(fn5: (x: T5) => T6, fn4: (x: T4) => T5, fn3: (x: T3) => T4, fn2: (x: T2) => T3, fn1: (x: T1) => T2, fn0: (x: V0) => T1): (x: V0) => T6

Мисля, че това е доста податливо на потребителски грешки и разочарование. Вашите типове най-вероятно имат имена, по-дълги от два знака, така че може да се нуждаете от обвиване на текст. Открих, че писането в Generics може да отнеме повече време, отколкото писането на самата композиция.

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

Не позволявайте обаче това да ви плаши от @types/ramda. Вероятно избирам череши в най-лошия случай. Всъщност е най-вече чудесен за използване и може да бъде безценен, когато трябва да преработите кода.

Последни мисли

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

Имайки предвид това, сега съществува цяла индустрия от инструменти, за да предостави някои мощни начини за улавяне на грешки, потребителски пътеки за навигация и следи на стека на грешки, когато се случват. Ако не използвате нищо, страхотно решение за плащане е sentry, който също е BSD3 лицензиран.

Функционалният стил на програмиране не е сребърен куршум, особено когато го използвате в браузъри и Javascript. Ако пишете много функционален код в голяма сложна кодова база. Моята препоръка е да не започвате, като използвате изцяло стил „без точки“. Вместо това се уверете, че свързвате здравословното използване на наименувани функции в основната бизнес логика и публичните API. Също така помислете за правилното въвеждане на тези функции с Typescript или Flow, така че да е по-лесно за поддръжка и преработване.

Не можах да намеря много друго, написано за този вид неща. Така че насърчавам другите да го направят. Освен ако не предложите на хората да инвестират в стойка за монитор, която поддържа портретен режим. По този начин целият стек за повиквания може да се побере в изгледа;)

Бих искал да призная една фантастична книга за функционално програмиране като „предимно адекватно ръководство“, техниката за регистриране също е спомената в раздела за отстраняване на грешки.

crossposted от: https://christopherdecoster.com/posts/debugging-fp-js/