В стремежа си да създадем лек език за програмиране с помощта на C ++, започнахме със създаването на нашия токенизатор преди три седмици и след това внедрихме оценката на израза през следващите две седмици.
Сега е време да приключите и да предоставите пълен език за програмиране, който няма да бъде толкова мощен, колкото зрял език за програмиране, но ще има всички необходими функции, включително много малък отпечатък.
Смешно ми е как новите компании имат раздели с често задавани въпроси на своите уебсайтове, които не отговарят на често задавани въпроси, а на въпроси, които искат да бъдат зададени. Ще направя същото и тук. Хората, които следят работата ми, често ме питат защо Stork не се компилира в някакъв байт код или поне в междинен език.
Щастлив съм да отговоря на този въпрос. Целта ми беше да разработя скриптов език с малък отпечатък, който лесно ще се интегрира с C ++. Нямам строга дефиниция на „малък отпечатък“, но си представям компилатор, който ще бъде достатъчно малък, за да позволи преносимост на по-малко мощни устройства и няма да консумира твърде много памет при стартиране.
Не се фокусирах върху скоростта, тъй като мисля, че ще кодирате в C ++, ако имате критична за времето задача, но ако имате нужда от някаква разширяемост, тогава език като Stork може да бъде полезен.
Не твърдя, че няма други, по-добри езици, които могат да изпълнят подобна задача (например, Луа ). Би било наистина трагично, ако те не съществуват и аз просто ви давам представа за случая на използване на този език.
Тъй като той ще бъде вграден в C ++, намирам за удобно да използвам някои съществуващи функции на C ++, вместо да пиша цяла екосистема, която ще служи за подобна цел. Не само това, но и този подход ми се струва по-интересен.
Както винаги, можете да намерите пълния изходен код на моя Страница на GitHub . Сега, нека разгледаме по-отблизо нашия напредък.
До тази част Stork беше частично завършен продукт, така че не успях да видя всичките му недостатъци и недостатъци. Тъй като обаче придоби по-цялостна форма, промених следните неща, въведени в предишни части:
function_lookup
в compiler_context
сега. function_param_lookup
се преименува на param_lookup
за да се избегне объркване.call
метод в runtime_context
което отнема std::vector
на аргументи, съхранява стар индекс на върната стойност, избутва аргументи в стека, променя индекса на върната стойност, извиква функцията, изскача аргументи от стека, възстановява стария индекс на връщаната стойност и връща резултата. По този начин не трябва да поддържаме стека на индексите на възвръщаемата стойност, както преди, защото стекът C ++ служи за тази цел.compiler_context
които се връщат чрез извиквания към неговите функции-членове scope
и function
. Всеки от тези обекти създава нови local_identifier_lookup
и param_identifier_lookup
, съответно, в техните конструктори и възстановява старото състояние в деструктора.runtime_context
, върнат от функцията член get_scope
. Тази функция съхранява размера на стека в своя конструктор и го възстановява в неговия деструктор.const
ключови думи и константни обекти като цяло. Те биха могли да бъдат полезни, но не са абсолютно необходими.var
ключовата дума е премахната, тъй като в момента изобщо не е необходима.sizeof
ключова дума, която ще провери размера на масива по време на изпълнение. Може би някои програмисти на C ++ ще намерят избора на име за богохулно, тъй като C ++ sizeof
работи по време на компилация, но избрах тази ключова дума, за да избегна сблъсък с някакво често срещано име на променлива - например size
tostring
ключова дума, която изрично преобразува всичко в string
. Това не може да бъде функция, тъй като не допускаме претоварване на функцията.Тъй като използваме синтаксис, много подобен на C и свързаните с него езици за програмиране, ще ви дам само подробностите, които може да не са ясни.
Декларациите за променлив тип са както следва:
void
, използва се само за типа връщане на функциятаnumber
string
T[]
е масив от това, което съдържа елементи от тип T
R(P1,...,Pn)
е функция, която връща тип R
и получава аргументи от типове P1
до Pn
. Всеки от тези типове може да има префикс &
ако е предадено чрез препратка.Декларацията за функцията е както следва: [public] function R name(P1 p1, … Pn pn)
Така че, трябва да има префикс function
. Ако е с префикс public
, тогава може да се извика от C ++. Ако функцията не върне стойността, тя ще изчисли стойността по подразбиране за своя тип на връщане.
Позволяваме for
-примка с декларация в първия израз. Също така разрешаваме if
-изложение и switch
-изложение с инициализационен израз, както в C ++ 17. Изложението if
започва с if
-блок, последвано от нула или повече elif
-блокове и по желание един else
-блок. Ако променливата е декларирана в израза за инициализация на if
-изложението, тя ще бъде видима във всеки от тези блокове.
Разрешаваме незадължителен номер след break
оператор, който може да се прекъсне от множество вложени цикли. Така че можете да имате следния код:
for (number i = 0; i <100; ++i) { for(number j = 0; j < 100; ++j) { if (rnd(100) == 0) { break 2; } } }
Също така, той ще се счупи от двата цикъла. Този номер се проверява по време на компилиране. Колко готино е това?
В тази част бяха добавени много функции, но ако стана твърде подробна, вероятно ще загубя дори и най-упоритите читатели, които все още са с мен. Затова умишлено ще пропусна една много голяма част от историята - компилацията.
Това е така, защото вече го описах в първо и второ части от тази поредица от блогове. Фокусирах се върху изрази, но компилирането на нещо друго не е много по-различно.
Все пак ще ви дам един пример. Този код се компилира while
изявления:
statement_ptr compile_while_statement( compiler_context& ctx, tokens_iterator& it, possible_flow pf ) { parse_token_value(ctx, it, reserved_token::kw_while); parse_token_value(ctx, it, reserved_token::open_round); expression::ptr expr = build_number_expression(ctx, it); parse_token_value(ctx, it, reserved_token::close_round); block_statement_ptr block = compile_block_statement(ctx, it, pf); return create_while_statement(std::move(expr), std::move(block)); }
Както виждате, далеч не е сложно. Той анализира while
, след това (
, след това изгражда числов израз (нямаме булеви числа) и след това анализира )
.
След това той компилира блоков оператор, който може да е вътре {
и }
или не (да, разреших блокове с едно изражение) и създава while
изявление в края.
Вече сте запознати с първите два аргумента на функцията. Третият, possible_flow
, показва разрешените команди за промяна на потока (continue
, break
, return
) в контекста, който анализираме. Бих могъл да запазя тази информация в обекта, ако изразите за компилация са функции на членове на някои compiler
клас, но не съм голям фен на мамутските класове и компилаторът определено би бил такъв клас. Предаването на допълнителен аргумент, особено тънък, няма да навреди на никого и кой знае, може би един ден ще успеем да паралелизираме кода.
Има още един интересен аспект на компилацията, който бих искал да обясня тук.
Ако искаме да поддържаме сценарий, при който две функции се извикват взаимно, можем да го направим по C-начин: чрез разрешаване на декларация напред или да имаме две фази на компилация.
Избрах втория подход. Когато дефиницията на функцията бъде намерена, ще анализираме нейния тип и име в обекта с име incomplete_function
. След това ще пропуснем тялото му, без интерпретация, като просто преброим нивото на гнездене на къдрави скоби, докато затворим първата къдрава скоба. Ще събираме жетони в процеса, ще ги съхраняваме в incomplete_function
и ще добавяме идентификатор на функция в compiler_context
.
След като предадем целия файл, ще компилираме всяка от функциите напълно, за да могат да бъдат извикани по време на изпълнение. По този начин всяка функция може да извика всяка друга функция във файла и да има достъп до всяка глобална променлива.
Глобалните променливи могат да бъдат инициализирани чрез извиквания към едни и същи функции, което веднага ни води до стария проблем „пиле и яйце“, веднага щом тези функции имат достъп до неинициализирани променливи.
Ако това някога се случи, проблемът се решава чрез хвърляне на runtime_exception
- и това е само защото съм добър. Франки, нарушаването на достъпа е най-малкото, което можете да получите като наказание за писането на такъв код.
Има два вида обекти, които могат да се появят в глобалния обхват:
Всяка глобална променлива може да бъде инициализирана с израз, който връща правилния тип. Инициализаторът се създава за всяка глобална променлива.
Всеки инициализатор връща lvalue
, така че те служат като конструктори на глобални променливи. Когато не е предоставен израз за глобална променлива, се конструира инициализаторът по подразбиране.
Това е initialize
членска функция в runtime_context
:
void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }
Извиква се от конструктора. Той изчиства контейнера на глобалната променлива, както може да бъде изрично извикан, за да нулира runtime_context
държава.
Както споменах по-рано, трябва да проверим дали имаме достъп до неинициализирана глобална променлива. Следователно това е глобалният променлив достъп:
variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx <_globals.size(), 'Uninitialized global variable access' ); return _globals[idx]; }
Ако първият аргумент се изчислява на false
, runtime_assertion
хвърля runtime_error
със съответното съобщение.
Всяка функция е реализирана като ламбда, която улавя единичния израз, който след това се изчислява с runtime_context
че функцията получава.
Както можете да видите от while
-компилацията на изложението, компилаторът се извиква рекурсивно, започвайки с блоковия оператор, който представлява блока на цялата функция.
Ето абстрактния базов клас за всички твърдения:
class statement { statement(const statement&) = delete; void operator=(const statement&) = delete; protected: statement() = default; public: virtual flow execute(runtime_context& context) = 0; virtual ~statement() = default; };
Единствената функция освен тези по подразбиране е execute
, която изпълнява логиката на оператора на runtime_context
и връща flow
, което определя къде следва да отиде логиката на програмата.
enum struct flow_type{ f_normal, f_break, f_continue, f_return, }; class flow { private: flow_type _type; int _break_level; flow(flow_type type, int break_level); public: flow_type type() const; int break_level() const; static flow normal_flow(); static flow break_flow(int break_level); static flow continue_flow(); static flow return_flow(); flow consume_break(); };
Статичните функции на създателя са самообясними и аз ги написах, за да предотвратя нелогични flow
с ненулево break_level
и типът, различен от flow_type::f_break
.
Сега, consume_break
ще създаде прекъсващ поток с едно по-малко ниво на прекъсване или, ако нивото на прекъсване достигне нула, нормалния поток.
Сега ще проверим всички видове изявления:
class simple_statement: public statement { private: expression::ptr _expr; public: simple_statement(expression::ptr expr): _expr(std::move(expr)) { } flow execute(runtime_context& context) override { _expr->evaluate(context); return flow::normal_flow(); } };
Тук, simple_statement
е изявлението, което е създадено от израз. Всеки израз може да се компилира като израз, който връща void
, така че simple_statement
може да се създаде от него. Като нито break
нито continue
или return
може да бъде част от израз, simple_statement
връща flow::normal_flow()
.
class block_statement: public statement { private: std::vector _statements; public: block_statement(std::vector statements): _statements(std::move(statements)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const statement_ptr& statement : _statements) { if ( flow f = statement->execute(context); f.type() != flow_type::f_normal ){ return f; } } return flow::normal_flow(); } };
block_statement
запазва std::vector
на изявления. Изпълнява ги един по един. Ако всеки от тях връща ненормален поток, той връща този поток веднага. Той използва RAII обект на обхват, за да позволи декларации за променлива на локален обхват.
class local_declaration_statement: public statement { private: std::vector _decls; public: local_declaration_statement(std::vector decls): _decls(std::move(decls)) { } flow execute(runtime_context& context) override { for (const expression::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return flow::normal_flow(); } };
local_declaration_statement
оценява израза, който създава локална променлива и избутва новата локална променлива в стека.
class break_statement: public statement { private: int _break_level; public: break_statement(int break_level): _break_level(break_level) { } flow execute(runtime_context&) override { return flow::break_flow(_break_level); } };
break_statement
има ниво на почивка, оценено по време на компилиране. Той просто връща потока, който съответства на това ниво на прекъсване.
class continue_statement: public statement { public: continue_statement() = default; flow execute(runtime_context&) override { return flow::continue_flow(); } };
continue_statement
просто връща flow::continue_flow()
.
class return_statement: public statement { private: expression::ptr _expr; public: return_statement(expression::ptr expr) : _expr(std::move(expr)) { } flow execute(runtime_context& context) override { context.retval() = _expr->evaluate(context); return flow::return_flow(); } }; class return_void_statement: public statement { public: return_void_statement() = default; flow execute(runtime_context&) override { return flow::return_flow(); } };
return_statement
и return_void_statement
и двете връщат flow::return_flow()
. Единствената разлика е, че първият има израза, който оценява на връщаната стойност, преди да се върне.
class if_statement: public statement { private: std::vector _exprs; std::vector _statements; public: if_statement( std::vector exprs, std::vector statements ): _exprs(std::move(exprs)), _statements(std::move(statements)) { } flow execute(runtime_context& context) override { for (size_t i = 0; i evaluate(context)) { return _statements[i]->execute(context); } } return _statements.back()->execute(context); } }; class if_declare_statement: public if_statement { private: std::vector _decls; public: if_declare_statement( std::vector decls, std::vector exprs, std::vector statements ): if_statement(std::move(exprs), std::move(statements)), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return if_statement::execute(context); } };
if_statement
, който се създава за един if
-блок, нула или повече elif
-блокове и един else
-блок (който може да е празен), оценява всеки от неговите изрази, докато един израз се изчисли на 1
. След това изпълнява този блок и връща резултата от изпълнението. Ако нито един израз не се изчислява на 1
, той ще върне изпълнението на последния (else
) блок.
if_declare_statement
е изявлението, което има декларации като първата част на клауза if. Той избутва всички декларирани променливи в стека и след това изпълнява основния си клас (if_statement
).
class switch_statement: public statement { private: expression::ptr _expr; std::vector _statements; std::unordered_map _cases; size_t _dflt; public: switch_statement( expression::ptr expr, std::vector statements, std::unordered_map cases, size_t dflt ): _expr(std::move(expr)), _statements(std::move(statements)), _cases(std::move(cases)), _dflt(dflt) { } flow execute(runtime_context& context) override { auto it = _cases.find(_expr->evaluate(context)); for ( size_t idx = (it == _cases.end() ? _dflt : it->second); idx execute(context); f.type()) { case flow_type::f_normal: break; case flow_type::f_break: return f.consume_break(); default: return f; } } return flow::normal_flow(); } }; class switch_declare_statement: public switch_statement { private: std::vector _decls; public: switch_declare_statement( std::vector decls, expression::ptr expr, std::vector statements, std::unordered_map cases, size_t dflt ): _decls(std::move(decls)), switch_statement(std::move(expr), std::move(statements), std::move(cases), dflt) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return switch_statement::execute(context); } };
switch_statement
изпълнява своите изявления един по един, но първо скача до подходящия индекс, който получава от оценката на израза. Ако някой от неговите изрази връща ненормален поток, той ще го върне незабавно. Ако има flow_type::f_break
, първо ще консумира една почивка.
switch_declare_statement
позволява декларация в заглавката си. Никой от тях не позволява деклариране в тялото.
class while_statement: public statement { private: expression::ptr _expr; statement_ptr _statement; public: while_statement(expression::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { while (_expr->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } };
class do_statement: public statement { private: expression::ptr _expr; statement_ptr _statement; public: do_statement(expression::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { do { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } while (_expr->evaluate(context)); return flow::normal_flow(); } };
while_statement
и do_while_statement
и двамата изпълняват оператора си body, докато изразът им се изчислява на 1
. Ако изпълнението се върне flow_type::f_break
, те го консумират и се връщат. Ако се върне flow_type::f_return
, те го връщат. В случай на нормално изпълнение или продължаване, те не правят нищо.
Може да изглежда така, сякаш continue
няма ефект. Вътрешното твърдение обаче беше засегнато от него. Ако беше например block_statement
, не оцени докрай.
Намирам за добре, че while_statement
се изпълнява с C ++ while
и do-statement
с C ++ do-while
.
class for_statement_base: public statement { private: expression::ptr _expr2; expression::ptr _expr3; statement_ptr _statement; public: for_statement_base( expression::ptr expr2, expression::ptr expr3, statement_ptr statement ): _expr2(std::move(expr2)), _expr3(std::move(expr3)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { for (; _expr2->evaluate(context); _expr3->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } }; class for_statement: public for_statement_base { private: expression::ptr _expr1; public: for_statement( expression::ptr expr1, expression::ptr expr2, expression::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _expr1(std::move(expr1)) { } flow execute(runtime_context& context) override { _expr1->evaluate(context); return for_statement_base::execute(context); } }; class for_declare_statement: public for_statement_base { private: std::vector _decls; expression::ptr _expr2; expression::ptr _expr3; statement_ptr _statement; public: for_declare_statement( std::vector decls, expression::ptr expr2, expression::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return for_statement_base::execute(context); } };
for_statement
и for_statement_declare
се изпълняват по същия начин като while_statement
и do_statement
. Те са наследени от for_statement_base
клас, който прави по-голямата част от логиката. for_statement_declare
се създава, когато първата част на for
-цикъла е декларация на променлива.
Това са всички класове на изявления, които имаме. Те са градивни елементи на нашите функции. Когато runtime_context
е създаден, той запазва тези функции. Ако функцията е декларирана с ключовата дума public
, тя може да бъде извикана по име.
Това завършва основната функционалност на Stork. Всичко останало, което ще опиша, са допълнителни мисли, които добавих, за да направим езика ни по-полезен.
Масивите са еднородни контейнери, тъй като могат да съдържат само елементи от един тип. Ако искаме хетерогенни контейнери, структурите веднага идват на ум.
Съществуват обаче по-тривиални хетерогенни контейнери: кортежи. Tuples могат да съхраняват елементите от различни типове, но техните типове трябва да бъдат известни по време на компилиране. Това е пример за декларация на кортеж в Stork:
[number, string] t = {22321, 'Siveric'};
Това декларира двойката number
и string
и го инициализира.
Списъците за инициализация могат да се използват и за инициализиране на масиви. Когато типовете изрази в списъка за инициализация не съвпадат с типа променлива, ще възникне грешка в компилатора.
Тъй като масивите се изпълняват като контейнери на variable_ptr
, ние получихме изпълнението на кортежи по време на изпълнение безплатно. Времето за компилация е, когато гарантираме правилния тип на съдържащите се променливи.
Би било хубаво да скриете подробностите за изпълнението от потребител на Stork и да представите езика по по-удобен за потребителя начин.
Това е класът, който ще ни помогне да постигнем това. Представям го без подробности за изпълнението:
class module { ... public: template void add_external_function(const char* name, std::function f); template auto create_public_function_caller(std::string name); void load(const char* path); bool try_load(const char* path, std::ostream* err = nullptr) noexcept; void reset_globals(); ... };
Функциите load
и try_load
ще зареди и компилира скрипта Stork от дадената пътека. Първо, един от тях може да хвърли stork::error
, но вторият ще го хване и ще го отпечата на изхода, ако е предвиден.
Функцията reset_globals
ще инициализира глобални променливи.
Функциите add_external_functions
и create_public_function_caller
трябва да се извика преди компилацията. Първият добавя функция C ++, която може да бъде извикана от Stork. Вторият създава извикващ се обект, който може да се използва за извикване на функцията Stork от C ++. Това ще доведе до грешка по време на компилация, ако типът на публичната функция не съвпада R(Args…)
по време на компилацията на скрипта Stork.
Добавих няколко стандартни функции, които могат да бъдат добавени към модула Stork.
void add_math_functions(module& m); void add_string_functions(module& m); void add_trace_functions(module& m); void add_standard_functions(module& m);
Ето пример за скрипт Stork:
function void swap(number& x, number& y) { number tmp = x; x = y; y = tmp; } function void quicksort( number[]& arr, number begin, number end, number(number, number) comp ) { if (end - begin <2) return; number pivot = arr[end-1]; number i = begin; for (number j = begin; j < end-1; ++j) if (comp(arr[j], pivot)) swap(&arr[i++], &arr[j]); swap (&arr[i], &arr[end-1]); quicksort(&arr, begin, i, comp); quicksort(&arr, i+1, end, comp); } function void sort(number[]& arr, number(number, number) comp) { quicksort(&arr, 0, sizeof(arr), comp); } function number less(number x, number y) { return x < y; } public function void main() { number[] arr; for (number i = 0; i < 100; ++i) { arr[sizeof(arr)] = rnd(100); } trace(tostring(arr)); sort(&arr, less); trace(tostring(arr)); sort(&arr, greater); trace(tostring(arr)); }
Ето частта на C ++:
#include #include 'module.hpp' #include 'standard_functions.hpp' int main() { std::string path = __FILE__; path = path.substr(0, path.find_last_of('/\') + 1) + 'test.stk'; using namespace stork; module m; add_standard_functions(m); m.add_external_function( 'greater', std::function([](number x, number y){ return x > y; } )); auto s_main = m.create_public_function_caller('main'); if (m.try_load(path.c_str(), &std::cerr)) { s_main(); } return 0; }
Стандартните функции се добавят към модула преди компилацията и функциите trace
и rnd
се използват от скрипта Stork. Функцията greater
също се добавя като витрина.
Скриптът се зарежда от файла „test.stk“, който се намира в същата папка като „main.cpp“ (чрез използване на __FILE__
дефиниция на препроцесор), и след това функцията main
е наречен.
В скрипта генерираме произволен масив, сортиращ във възходящ ред с помощта на компаратора less
, и след това в низходящ с помощта на сравнителния greater
, написан на C ++.
Можете да видите, че кодът е напълно четим за всеки, който владее C (или който и да е програмен език, получен от C).
Има много функции, които бих искал да внедря в Stork:
Липсата на време и пространство е една от причините, поради които не ги прилагаме вече. Ще се опитам да актуализирам моя Страница на GitHub с нови версии, тъй като внедрявам нови функции в свободното си време.
Създадохме нов език за програмиране!
Това отне добра част от свободното ми време през последните шест седмици, но вече мога да напиша някои сценарии и да ги видя да работят. Това правех през последните няколко дни, драскайки плешивата си глава всеки път, когато се срине неочаквано. Понякога това беше малка грешка, а понякога гадна грешка. Понякога обаче се чувствах смутен, защото ставаше въпрос за лошо решение, което вече бях споделил със света. Но всеки път щях да поправя и да продължа да кодирам.
В процеса научих за if constexpr
, който никога преди не бях използвал. Също така се запознах по-добре с rvalue-референциите и перфектното препращане, както и с други по-малки функции на C ++ 17, които не срещам ежедневно.
Кодът не е перфектен - никога не бих направил такова твърдение, но той е достатъчно добър и най-вече следва добри практики за програмиране. И най-важното - работи.
Решението да разработите нов език от нулата може да звучи налудничаво за обикновения човек или дори за средния програмист, но това е още по-голямата причина да го направите и да си докажете, че можете да го направите. Точно както решаването на труден пъзел е добро мозъчно упражнение за поддържане на психическа форма.
Тъпите предизвикателства са често срещани в нашето ежедневно програмиране, тъй като не можем да избираме само интересните аспекти от него и трябва да вършим сериозна работа, дори понякога да е скучно. Ако сте професионален разработчик, вашият първи приоритет е да доставите висококачествен код на работодателя си и да сложите храна на масата. Това понякога може да ви накара да избягвате програмирането в свободното си време и може да потисне ентусиазма от ранните ви учебни дни по програмиране.
Ако не се налага, не губете този ентусиазъм. Работете върху нещо, ако ви се струва интересно, дори ако вече е направено. Не е нужно да оправдавате причината, за да се забавлявате.
И ако можете да го включите - дори частично - във вашата професионална работа, добре за вас! Не много хора имат тази възможност.
Кодът за тази част ще бъде замразен със специален клон на моя Страница на GitHub .
Извлечението е най-малката единица от компютърната програма, която може да бъде изпълнена.
Масивите съдържат елементи от един и същи тип, докато кортежите могат да съдържат елементи от различни типове.
В някои езици за програмиране байтовият код е резултат от компилация, състояща се от инструкции от ниско ниво, които могат да бъдат изпълнени от интерпретатора.