Функции. Зачем они нужны и как их писать, чтобы вас уважали программисты
vk f t

Функции. Зачем они нужны и как их писать, чтобы вас уважали программисты

Слож­ная важ­ная ста­тья для тех, кто хочет стать кру­тым про­грам­ми­стом

Хоро­ший про­грам­мист ста­ра­ет­ся делать свои функ­ции чисты­ми. Если знать, что это такое, мож­но сой­ти за сво­е­го, а заод­но напи­сать чита­е­мый код.

Если вы не совсем пони­ма­е­те, что такое функ­ция и зачем она нуж­на — доб­ро пожа­ло­вать в наш кат:

Что такое функ­ции и зачем они нуж­ны

Что такое функция

Функ­ция — это мини-программа внут­ри вашей основ­ной про­грам­мы, кото­рая дела­ет какую-то одну понят­ную вещь. Вы одна­жды опи­сы­ва­е­те, что это за вещь, а потом ссы­ла­е­тесь на это опи­са­ние.

Напри­мер, вы пише­те игру. Каж­дый раз, когда игрок попа­да­ет в цель, уби­ва­ет вра­га, дела­ет ком­бо, закан­чи­ва­ет уро­вень или пада­ет в лаву, вам нуж­но доба­вить или уба­вить ему очков. Это дела­ет­ся дву­мя дей­стви­я­ми: к ста­рым очкам добав­ля­ют­ся новые, на экран выво­дит­ся новая сум­ма очков. Допу­стим, эти дей­ствия зани­ма­ют 8 строк кода.

Допу­стим, в игре есть 100 ситу­а­ций, когда нуж­но доба­вить или уба­вить очки — для каж­до­го типа вра­га, пре­гра­ды, уров­ня и т. д. Что­бы в каж­дой из ста ситу­а­ций не писать одни и те же восемь строк кода, вы упа­ко­вы­ва­е­те эти восемь строк в функ­цию. И теперь в ста местах вы пише­те одну стро­ку: напри­мер, changeScore(10) — чис­ло очков повы­сит­ся на 10.

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

Зачем нужны функции

Функ­ции нуж­ны, что­бы замет­но упро­щать и сокра­щать код, адап­ти­ро­вать его для раз­ных плат­форм, делать более отка­зо­устой­чи­вым, лег­ко отла­жи­вать. И вооб­ще поря­док в функ­ци­ях — поря­док в голо­ве.

Возь­мём тот же при­мер с под­счё­том очков. Что если при добав­ле­нии очков нуж­но не толь­ко выво­дить их на экран, но и запи­сы­вать в файл? Про­сто добав­ля­е­те в опре­де­ле­нии функ­ции допол­ни­тель­ные коман­ды, свя­зан­ные с фай­ла­ми, и они теперь будут испол­нять­ся каж­дый раз, когда функ­цию сно­ва вызо­вут в основ­ной про­грам­ме. Не нуж­но пол­зать по все­му коду, искать места с добав­ле­ни­ем очков и допи­сы­вать там про фай­лы. Мень­ше руч­но­го тру­да, мень­ше опе­ча­ток, мень­ше неза­кры­тых ско­бок.

А что если нуж­но не толь­ко писать очки в файл, но и сле­дить за рекор­дом? Пишем новую функ­цию getHighScore(), кото­рая доста­ёт откуда-то рекорд по игре, и две дру­гие — setHighScore() и celebrateHighScore() — одна будет пере­за­пи­сы­вать рекорд, если мы его поби­ли, а вто­рая — как-то поздрав­лять поль­зо­ва­те­ля с рекор­дом.

    
language: JavaScript
// Объявляем новую функцию. Она будет называться changeScore и принимать один аргумент, который мы для этого фрагмента назовём howMuch. Дальше мы просто будем подавать в функцию число.

function changeScore(howMuch){

   // Прибавим к старым очкам новые

   playerScore = playerScore + howMuch;

   // Выведем новые очки на экран

   $('#scoretext').text(playerScore)

   // Узнаем, какой у нас рекорд. Для этого объявим новую переменную highScore, вызовем функцию getHighScore(), запишем её результат в эту переменную

   var highScore = getHighScore();

   // А теперь сравним, больше ли наши очки, чем рекорд по игре

   if(playerScore > highScore){

       //Рекорд побит, значит, надо его записать

       setHighScore(playerScore, playerName);

       // Делаем тут что-то, что обычно делают, когда ты побил рекорд игры. Фейерверки? Музыка? Мигание рекордных очков на экране? Мы не знаем пока, что именно будет делать эта функция, и нам это сейчас неважно

       celebrateHighScore();

   }

}


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Теперь при каж­дом сра­ба­ты­ва­нии changeScore() будет вызы­вать все осталь­ные функ­ции. И сколь­ко бы раз мы ни вызва­ли в коде changeScore(), она потя­нет за собой всё хозяй­ство авто­ма­ти­че­ски.

Сила ещё в том, что при раз­бо­ре этой функ­ции нам неваж­но, как реа­ли­зо­ва­ны getHighScore(), setHighScore() и celebrateHighScore(). Они зада­ют­ся где-то в дру­гом месте кода и в дан­ный момент нас не вол­ну­ют. Они могут брать дан­ные с жёст­ко­го дис­ка, писать их в базу дан­ных, изда­вать зву­ки и взла­мы­вать Пен­та­гон — это будет рас­пи­са­но внут­ри самих функ­ций в дру­гих местах тек­ста.

Без функ­ций труд­но пове­сить дей­ствия на какие-либо кноп­ки в интер­фей­се. Напри­мер, у вас на сай­те есть фор­ма, и при кли­ке на кноп­ку «Отпра­вить» вы хоти­те про­ве­рять, что дан­ные в фор­ме пра­виль­но вве­де­ны. Вы спо­кой­но опи­сы­ва­е­те где-то в коде функ­цию validateForm() и веша­е­те её на нажа­тие кноп­ки. Кноп­ку нажа­ли — функ­ция вызва­лась. Не нуж­но впи­сы­вать в кноп­ку пол­ный текст про­грам­мы.

А без функ­ции при­шлось бы писать огром­ную программу-валидатор пря­мо внут­ри кноп­ки. Это испол­ни­мо, но код выгля­дел бы страш­но гро­мозд­ким. Что если у вас на стра­ни­це три фор­мы, и каж­дую нуж­но вали­ди­ро­вать?

Хоро­шо напи­сан­ные функ­ции рез­ко повы­ша­ют чита­е­мость кода. Мы можем читать чужую про­грам­му, уви­деть там функ­цию getExamScore(username) и знать, что послед­няя каким-то обра­зом выяс­ня­ет резуль­та­ты экза­ме­на по такому-то юзер­ней­му. Как она там устро­е­на внут­ри, куда обра­ща­ет­ся и что исполь­зу­ет — вам неваж­но. Для нас это как бы одна про­стая понят­ная коман­да.

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

Функ­ции — это бес­ко­неч­ная радость. На этом наш экс­курс в функ­ции закон­чен, пере­хо­дим к чисто­те.

Что такое чистые функции

Есть поня­тие чистых функ­ций. Это зна­чит, что если функ­ции два раза дать на обра­бот­ку одно и то же зна­че­ние, она все­гда выдаст один и тот же резуль­тат и в про­грам­ме не изме­нит ниче­го, что не отно­сит­ся непо­сред­ствен­но к этой функ­ции. То есть у чистой функ­ции пред­ска­зу­е­мый резуль­тат и нет побоч­ных эффек­тов.

Вот при­ме­ры.

Один и тот же результат

Допу­стим, мы при­ду­ма­ли функ­цию, кото­рая счи­та­ет пло­щадь кру­га по его ради­у­су: getCircleArea(). Для наших целей мы берём чис­ло пи, рав­ное 3,1415, и впи­сы­ва­ем в функ­цию:

    
language: JavaScript
function getCircleArea(radius){

   // Задаём наше местное определение числа пи

   var localPi = 3.1415;

   // Считаем площадь: пи на радиус в квадрате. Это то же самое, что пи умножить на радиус и ещё раз умножить на радиус

   var circleArea = localPi*radius*radius;

   // Говорим функции вернуть то, что мы сейчас рассчитали

   return circleArea;

}


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Теперь этой функ­ции надо скор­мить чис­ло, и она выдаст пло­щадь кру­га:

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

Дру­гой при­мер. Мы пишем программу-таймер, кото­рая долж­на издать звук, напри­мер, за 10 секунд до кон­ца отве­дён­но­го ей вре­ме­ни. Что­бы узнать, сколь­ко оста­лось секунд, нам нуж­на функ­ция: она выяс­ня­ет коли­че­ство секунд меж­ду дву­мя отмет­ка­ми вре­ме­ни. Мы даём ей два вре­ме­ни в каком-то фор­ма­те, а функ­ция сама неким обра­зом высчи­ты­ва­ет, сколь­ко меж­ду ними секунд. Как имен­но она это счи­та­ет, сей­час неваж­но. Важ­но, что она это дела­ет оди­на­ко­во. Это тоже функ­ция с пред­ска­зу­е­мым резуль­та­том:

А теперь при­мер похо­жей функ­ции: она опре­де­ля­ет вре­мя от теку­ще­го до какого-то дру­го­го вре­ме­ни. При испол­не­нии эта функ­ция запра­ши­ва­ет теку­щее вре­мя в ком­пью­те­ре, срав­ни­ва­ет с целе­вым и дела­ет нуж­ные вычис­ле­ния. При запус­ке одной и той же функ­ции с раз­ни­цей в несколь­ко секунд она даст раз­ные резуль­та­ты:

Это функ­ция с непред­ска­зу­е­мым резуль­та­том. У неё есть непред­ска­зу­е­мая зави­си­мость, кото­рая может повли­ять на рабо­ту про­грам­мы — зави­си­мость от теку­ще­го вре­ме­ни на ком­пью­те­ре. Что если во вре­мя испол­не­ния у поль­зо­ва­те­ля обну­ли­лись часы? Или он сме­нил часо­вой пояс? Или при запро­се теку­ще­го вре­ме­ни про­ис­хо­дит ошиб­ка? Или его ком­пью­тер не под­дер­жи­ва­ет отда­чу вре­ме­ни?

С точ­ки зре­ния чистых функ­ций, пра­виль­нее будет сна­ча­ла отдель­ны­ми функ­ци­я­ми полу­чить все внеш­ние зави­си­мо­сти, про­ве­рить их и убе­дить­ся, что они под­хо­дят для нашей рабо­ты. И потом уже вызвать функ­цию с под­счё­том интер­ва­лов. Что-то вро­де тако­го:

Тогда в функ­ции getCurrentTime() мож­но будет про­пи­сать всё хозяй­ство, свя­зан­ное с полу­че­ни­ем нуж­но­го вре­ме­ни и его про­вер­кой, а в getInterval() оста­вить толь­ко алго­ритм, кото­рый счи­та­ет раз­ни­цу во вре­ме­ни.

Побочные эффекты

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

При­мер­чи­ки!

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

Мож­но напи­сать функ­цию, кото­рая счи­ты­ва­ет все зада­чи из памя­ти, нахо­дит нуж­ные и воз­вра­ща­ет. При этом на зада­чи в памя­ти это не вли­я­ет: они как были сва­ле­ны в кучу, так и оста­лись. Это функ­ция без побоч­ных эффек­тов.

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

Про­грам­ми­сты насто­ро­жен­но отно­сят­ся к мута­ци­ям, пото­му что за ними слож­но сле­дить. Что если из-за какой-то ошиб­ки функ­ции выпол­нят­ся в непра­виль­ном поряд­ке и уни­что­жат важ­ные для про­грам­мы дан­ные? Или функ­ция выпол­нит­ся непред­ска­зу­е­мо мно­го раз? Или она застря­нет в цик­ле и из-за мута­ций разо­рвёт память? Или мута­ция про­изой­дёт не с тем кус­ком про­грам­мы, кото­рый мы изна­чаль­но хоте­ли?

Вот типич­ная ошиб­ка, свя­зан­ная с мута­ци­ей. Мы пишем игру, нуж­но поме­нять сум­му игро­вых очков. За это отве­ча­ет функ­ция changeScore(), кото­рая запи­сы­ва­ет резуль­тат в гло­баль­ную пере­мен­ную playerScore — то есть мути­ру­ет эту пере­мен­ную. Мы слу­чай­но, по невни­ма­тель­но­сти, вызва­ли эту функ­цию в двух местах вме­сто одно­го, и бал­лы уве­ли­чи­ва­ют­ся вдвое. Это баг.

Дру­гая типич­ная ошиб­ка. Про­грам­мист напи­сал функ­цию, кото­рая уда­ля­ет из таб­ли­цы послед­нюю стро­ку, пото­му что был почему-то уве­рен: стро­ка будет пустой и нико­му не нуж­ной. Слу­чай­но эта функ­ция вызы­ва­ет­ся в бес­ко­неч­ном цик­ле и сти­ра­ет все стро­ки, от послед­ней к пер­вой. Дан­ные уни­что­жа­ют­ся. А вот если бы функ­ция не уда­ля­ла стро­ку из таб­ли­цы, а дела­ла новую таб­ли­цу без послед­ней стро­ки, дан­ные бы не постра­да­ли.

Без мути­ру­ю­щих функ­ций, конеч­но, мы не обой­дём­ся — нуж­но и выво­дить на экран, и писать в файл, и рабо­тать с гло­баль­ны­ми пере­мен­ны­ми. Слож­но пред­ста­вить про­грам­му, в кото­рой вооб­ще не будет мути­ру­ю­щих функ­ций. Но про­грам­ми­сты пред­по­чи­та­ют выде­лять такие функ­ции отдель­но, тести­ро­вать их осо­бо тща­тель­но, и вни­ма­тель­но сле­дить за тем, как они рабо­та­ют. Гру­бо гово­ря, если функ­ция вно­сит изме­не­ния в боль­шой важ­ный файл, она долж­на как мини­мум про­ве­рить кор­рект­ность вхо­дя­щих дан­ных и сохра­нить резерв­ную копию это­го фай­ла.

Как этим пользоваться

Когда буде­те писать свою сле­ду­ю­щую функ­цию, задай­тесь вопро­са­ми:

  1. Нет ли тут каких-то зави­си­мо­стей, кото­рые могут пове­сти себя непред­ска­зу­е­мо? Не беру ли я дан­ные неиз­вест­но отку­да? Что если все эти дан­ные у меня не возь­мут­ся или ока­жут­ся не тем, что мне надо? Как защи­тить про­грам­му на слу­чай, если этих дан­ных там не ока­жет­ся?
  2. Вли­я­ет ли эта функ­ция на дан­ные за её пре­де­ла­ми?

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

Ещё по теме