Среди на видимост

Среда на видимост

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

#include<iostream>
using namespace std;

int main()
{
    for(int x=0; x<5; x++)
    {
        cout<<x*x<<endl;
    }
    cout<<x<<endl;
    return 0;
}

Да, ама не :)
Този код ще гръмне, защото x не е дефинирано извън блока на for цикъла.

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

Вложени функции

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

Следва въпросът, който ни мъчи в тази лекция:

Защо програмата използва допълнителната функция Discriminant?

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

Така де, само веднъж няма да пречи много, нали?

Ама ако работиш два месеца по някакъв проект?
Ако пет-десет човека работят два месеца по проект, всеки слагайки своите малки "дискриминанти" в глобалното пространство винаги, когато има нужда? В крайна сметка ще имаме една неудобна и претрупана система, в която ще отнема доста време да се направят дори и тривиални неща. Да не говорим, че ще работи по-бавно. Много програмисти са си зарязвали проектите в отчаяние заради такива неща.

В Scheme можем да влагаме една функция в друга функция.

Тоест, можем да напишем нещо такова:

(define (solve_square_equation ...)
    (define (calculate_discriminant ...)
        ...
    )
    ...
    ...
    (+
        (- b)
        (sqrt (calculate_discriminant a b c))
    )
)

Функцията calculate_discriminant е създадена в *средата* на функцията solve_square_equation. Т.е. можем да използваме този израз само докато сме в solve_square_equation - отвън той не може да се вика. Това е почти същото като private методите на класовете в Java/C++. С една малка, значителна разлика. calculate_discriminant не е метод. Тя е израз. Най-простият начин да се каже е, че е едновременно и метод, и клас, и променлива1. Това е като цяло философията на функционалните езици. В calculate_discriminant също можем да си дефинираме ирази, които пък ще се виждат само от вътре в нея. Нещо повече, в средата се виждат не само изрази, а и стойности. Или по-точно виждат се само изрази, но всичко е израз :)
Тоест, следният код е валиден:
(define (solve_square_equation a b c)
    (define (calculate_discriminant)
        (- (* b b) (* 4 a c))
    )
    ...
    ...
    (+
        (- b)
        (sqrt (calculate_discriminant))
    )
)

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

Кога се използва това?

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

Ето правилото

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

На изпроводяк - ето един

Protip

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

  • глобална среда
    • израз solve_square_equation (следва неговата среда)
      • символ a
      • символ b
      • символ c
      • израз discriminant (следва среда)
        • използва символи a,b,c от средата на родителя си

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

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License