Пишем простой интерпретатор на C++ с помощью TDD, часть 1
Многие C++ программисты слышали про разработку через тестирование. Но почти все материалы по данной теме касаются более высокоуровневых языков и сосредоточены больше на общей теории, чем на практике. Итак, в данной статье я попробую привести пример пошаговой разработки через тестирование небольшого проекта на C++. А именно, как можно предположить из названия, простого интерпретатора математических выражений. Такой проект также является неплохой code kata, так как на его выполнение затрачивается не более часа (если не писать параллельно статью об этом).
Архитектура
Несмотря на то, что при использовании TDD архитектура приложения постепенно проявляется сама собой, начальная её проработка всё же необходима. Благодаря этому может значительно снизиться общее время, затраченное на реализацию. Это особенно эффективно в тех случаях, когда существуют готовые примеры подобных систем, которые можно взять за образец. В данном случае, существует вполне устоявшееся мнение о том, как должны быть устроены компиляторы и интерпретаторы, чем и можно воспользоваться.
Существует множество библиотек и инструментов, которые могут облегчить разработку интерпретаторов и компиляторов. Начиная от Boost.Spirit и заканчивая ANTLR и Bison. Можно даже запустить канал интерпретатора командной строки через popen и вычислить выражение через него. Целью данной статье является пошаговая разработка достаточно сложной системы с помощью TDD, поэтому будет использоваться только стандартная библиотека C++ и встроенный в IDE тестовый фреймворк.
Для начала, составим список того, что должен уметь наш простой интерпретатор, в порядке убывания приоритета:
- Вычислять значение математического выражения, состоящего из чисел с плавающий точкой и математических операторов (-+/*).
- Учёт приоритета операторов.
- Учёт скобок.
- Унарные плюс и минус.
- Вычисление нескольких выражений, разделённых точкой с запятой (;).
- Встроенные константы (pi, e).
- Создание собственных констант с помощью оператора присваивания (=).
- Встроенные функции с переменным числом аргументов.
- Задание новых функций.
В данной статье будет реализация только первых трёх пунктов. Сам проект концептуально будет состоять из четырёх частей:
- Лексический анализатор. Преобразовывает входную строку в последовательность токенов.
- Синтаксический анализатор. Строит из токенов синтаксическое представление в виде постфиксной нотации. Делать это будем без рекурсии и таблиц, с помощью алгоритма сортировочной станции.
- Вычислитель. Вычисляет результат выражения на стековой машине.
- Собственно, интерпретатор. Служит фасадом для вышеперечисленных частей.
Инструментарий
Программа будет писаться в Visual Studio 2013 с установленным Visual C++ Compiler Nov 2013 CTP. Тесты будут на основе встроенного в студию тестового фреймворка для C++ проектов CppUnitTestFramework. Он предоставляет минимальную поддержку для написания модульных тестов (по сравнению с Boost.Test, или CppUTest), но, с другой стороны, хорошо интегрирован в среду разработки. Альтернативой может служить Eclipse с установленным плагином C/C++ Unit и настроенным Boost.Test, GTest, или QtTest. В такой конфигурации рекомендую использовать clang, так как он предоставляет несколько мощнейших compile- и runtime анализаторов, в результате чего, в связке с TDD, код становится совершенно неуязвимым для ошибок.
Итак, создадим новый проект типа «Native Unit Test Project» и удостоверимся, что всё компилируется.
Лексер
Начнём с разработки лексера. Будем следовать привычному для TDD циклу Red-Green-Refactor:
- Написать тест и заставить его падать (Red).
- Заставить его пройти (Green).
- Улучшить дизайн (Refactor).
- В ответ на пустое выражение, должен возвращаться пустой список токенов.
TEST_CLASS(LexerTests) < public: TEST_METHOD(Should_return_empty_token_list_when_put_empty_expression) < Tokens tokens = Lexer::Tokenize(""); Assert::IsTrue(tokens.empty()); >>;
В CppUnitTestFramework макрос TEST_CLASS генерирует класс, в котором будут размещаться тестовые методы. Макрос TEST_METHOD , соответственно, создаёт сам тестовый метод. Необходимо учесть, что экземпляр класса создаётся только один раз перед запуском всех находящихся в нём тестов. В Boost.Test, к примеру, экземпляр класса создаётся каждый раз заново перед запуском каждого теста. Следовательно, тот, код, который необходимо выполнить перед каждым тестом, будет помещаться в метод, объявленный с помощью макроса TEST_METHOD_INITIALIZE , а тот, который после, в TEST_METHOD_CLEANUP . Все методы утверждений являются статическими и располагаются в классе Assert . Их немного, но основную функциональность они покрывают.
Вернёмся к нашему тесту. Он не то, чтобы не проходит, он даже не компилируется. Создадим функцию Tokenize в пространстве имён Lexer , принимающую строку и возвращающую std::vector , скрытый для удобства за псевдонимом Tokens . Я решил пока что не создавать дополнительные классы и ограничиться обычной функцией.
#pragma once; #include namespace Interpreter < struct Token <>; typedef std::vector Tokens; namespace Lexer < inline Tokens Tokenize(std::string expr) < throw std::exception(); >> // namespace Lexer > // namespace Interpreter
Сейчас проект компилируется, но тест, что было ожидаемо, падает. Для определения того, что же писать дальше, можно воспользоваться техникой The Transformation Priority Premise (TPP) авторства Роберта Мартина. Трансформации являются аналогами рефакторингов, но, в отличии от них, используются для изменения поведения кода, тогда как рефакторинг к изменению поведения не приводит. Каждая трансформация ведёт к изменению кода от более конкретного к более общему. Главная их особенность в том, что они имеют разные приоритеты, в зависимости от которых выбирается то, какой код писать для прохождения теста и какой будет следующий тест. А именно, те трансформации, которые проще (располагаются выше в списке) должны быть более предпочтительны, чем те, которые снизу. При намерении создать новый тест, выбирается такой, что для его прохождения нужно применить более простую трансформацию. Это не является строгим правилом, но следование TPP может вести к более простому коду за меньшее количество шагов.
Сам список трансформаций:
- (<> → nil) Заменить отсутствие кода на код, использующий нулевое значение.
- (nil → constant) Заменить нулевое значение константой.
- (constant → constant+) Заменить простую константу более сложной (строку из одной буквы на строку из нескольких букв, к примеру).
- (constant → scalar) Заменить константу на переменную, или аргумент.
- (statement → statements) Добавить безусловный оператор (break, continue, return и подобное).
- (unconditional → if) Разбить поток выполнения с помощью условного оператора.
- (scalar → array) Заменить переменную/аргумент массивом.
- (array → container) Заменить массив более сложным контейнером.
- (statement → recursion) Заменить выражение рекурсией.
- (if → while) Заменить условный оператор циклом.
- (expression → function) Заменить выражение функцией.
- (variable → assignment) Изменить значение переменной.
inline Tokens Tokenize(std::string expr) < return<>; >
Посмотрим, что должен уметь лексер для выполнения первого пункта требований. Занесём это в список тестов.
В ответ на пустое выражение должен возвращаться пустой список токенов.- В ответ на строку с оператором должен возвращаться токен с оператором.
- В ответ на строку с цифрой должен возвращаться токен с числом.
- В ответ на строку с числом с плавающей точкой должен возвращаться токен с этим числом.
- В ответ на строку с простым выражением должен возвращаться список соответствующих токенов.
- Пробелы между числами и операторами должны игнорироваться.
TEST_METHOD(Should_tokenize_single_plus_operator) < Tokens tokens = Lexer::Tokenize(L"+"); AssertRange::AreEqual(< Operator::Plus >, tokens); >
Здесь AssertRange — это пространство имён, в которое я поместил функцию утверждения AreEqual , сравнивающую две последовательности, а точнее, список инициализации и последовательность.
AssertRange
namespace AssertRange < templatestatic void AreEqual(initializer_list expect, const ActualRange &actual) < auto actualIter = begin(actual); auto expectIter = begin(expect); Assert::AreEqual(distance(expectIter, end(expect)), distance(actualIter, end(actual)), L"Size differs."); for(; expectIter != end(expect) && actualIter != end(actual); ++expectIter, ++actualIter) < auto message = L"Mismatch in position " + to_wstring(distance(begin(expect), expectIter)); Assert::AreEqual(*expectIter, *actualIter, message.c_str()); >> > // namespace AssertRange
Также пришлось изменить определение токена и добавить перечисление Operator с названиями арифметических операторов. Можно было бы использовать для этой цели просто тип wchar_t с символом оператора, но тогда в будущем придётся иметь дело с тем, как различать бинарные и унарные операции.
enum class Operator : wchar_t < Plus = L'+', >; typedef Operator Token;
Для успешной компиляции тестов, для каждого класса, экземпляр которого передаётся в статические методы класса Assert , необходимо определить функцию ToString , возвращающий его строковое представление.
std::wstring ToString(const Token &)
inline std::wstring ToString(const Token &token) < return< static_cast(token) >; >
После этого тест компилируется, но не проходит, так как мы продолжаем возвращать пустую последовательность токенов. Исправим это, применив трансформацию (unconditional → if).
inline Tokens Tokenize(std::wstring expr) < if(expr.empty()) < return<>; > return< static_cast(expr[0]) >; >
В ответ на пустое выражение должен возвращаться пустой список токенов.В ответ на строку с оператором должен возвращаться токен с оператором.- В ответ на строку с цифрой должен возвращаться токен с числом.
- …
TEST_METHOD(Should_tokenize_single_digit) < Tokens tokens = Lexer::Tokenize(L"1"); AssertRange::AreEqual(< 1.0 >, tokens); >
Здесь возникает проблема представления токенов. С одной стороны, они должны хранить коды операторов, с другой — числа. Решений в данном случае, несколько:
- Создать два класса токенов для операторов и числе, унаследовав их от общего класса Token . После этого приводить его с помощью dynamic_cast для извлечения кода числа, или кода оператора.
- То же, что в варианте выше, но вместо каста использовать двойную диспетчеризацию.
- В качестве токенов может быть std::function в которой будет храниться замыкание с необходимыми данными. Получать данные из замыкания можно с помощью посетителя.
- Использовать Boost.Any, или что-либо подобное.
- Использовать обычную структуру с полями для каждого вида данный и флагом типа.
- …
- Создать токен с оператором и получить его тип.
- Создать токен с числом и получить его тип.
- Создать токен с оператором и получить этот оператор.
- Создать токен с числом и получить это число.
enum class TokenType < Operator, Number >; class Token < public: Token(Operator) <>TokenType Type() const < return TokenType::Operator; >>; … TEST_CLASS(TokenTests) < public: TEST_METHOD(Should_get_type_for_operator_token) < Token opToken(Operator::Plus); Assert::AreEqual(TokenType::Operator, opToken.Type()); >>;
Добавив метод ToString для перечисления TokenType и подправив аналогичный метод для самого токена, заставим всё компилироваться, а тесты проходить. Напишем следующий тест из списка.
TEST_METHOD(Should_get_type_for_number_token)
Он не проходит. Примени трансформацию (constant → scalar) для класса токена.
class Token < public: Token(Operator) :m_type(TokenType::Operator) <>Token(double) :m_type(TokenType::Number) <> TokenType Type() const < return m_type; >private: TokenType m_type; >;
- …
Создать токен с оператором и получить его тип.Создать токен с числом и получить его тип.- Создать токен с оператором и получить этот оператор.
- Создать токен с числом и получить это число.
TEST_METHOD(Should_get_operator_code_from_operator_token) < Token token(Operator::Plus); Assert::AreEqual(Operator::Plus, token); >
Для удобства преобразования токена к нужному типу будем использовать оператор неявного приведения.
class Token < public: Token(Operator op) :m_type(TokenType::Operator), m_operator(op) <>operator Operator() const < return m_operator; >… Operator m_operator; >;
Аналогично напишем тест для числового токена.
TEST_METHOD(Should_get_number_value_from_number_token) < Token token(1.23); Assert::AreEqual(1.23, token); >
Так как в токене не может одновременно храниться и оператор, и число, то их поля можно объединить в union . Также добавим проверку на операцию приведения к неверному типу.
class Token < public: Token(Operator op) :m_type(TokenType::Operator), m_operator(op) <>Token(double num) :m_type(TokenType::Number), m_number(num) <> TokenType Type() const < return m_type; >operator Operator() const < if(m_type != TokenType::Operator) throw std::logic_error("Should be operator token."); return m_operator; >operator double() const < if(m_type != TokenType::Number) throw std::logic_error("Should be number token."); return m_number; >private: TokenType m_type; union < Operator m_operator; double m_number; >; >; inline std::wstring ToString(const Token &token) < switch(token.Type()) < case TokenType::Number: return std::to_wstring(static_cast(token)); case TokenType::Operator: return ToString(static_cast(token)); default: return "Unknown token."; > >
Все тесты, относящиеся к токену проходят, можно восстановить предыдущие тесты и убедиться, что ничего не сломалось. Последний тест всё так же не проходит. Приступим к его исправлению.
- В ответ на строку с цифрой должен возвращаться токен с числом.
- В ответ на строку с числом с плавающей точкой должен возвращаться токен с этим числом.
- В ответ на строку с простым выражением должен возвращаться список соответствующих токенов.
- Пробелы между числами и операторами должны игнорироваться.
if(expr[0] >= '0' && expr[0] ; > return< static_cast(expr[0]) >;
Выгладит пока что не очень симпатично, но тест проходит.
- …
В ответ на строку с цифрой должен возвращаться токен с числом.- В ответ на строку с числом с плавающей точкой должен возвращаться токен с этим числом.
TEST_METHOD(Should_tokenize_floating_point_number) < Tokens tokens = Lexer::Tokenize(L"12.34"); AssertRange::AreEqual(< 12.34 >, tokens); >
Вспомним, что в стандартной библиотеке C есть такие функции, как isdigit , проверяющая, что данный символ является цифрой и atof , преобразующая строку в число, а также их аналоги для wchar_t . Применим (expression → function). После этого небольшого изменения данный тест также начал проходить.
inline Tokens Tokenize(std::wstring expr) < const wchar_t *current = expr.c_str(); if(!*current) return<>; if(iswdigit(*current)) return< _wtof(current) >; return< static_cast(*current) >; >
После этого можно приступить и к более сложным тестам. Попробуем обработать полюс и число одновременно.
TEST_METHOD(Should_tokenize_plus_and_number) < Tokens tokens = Lexer::Tokenize(L"+12.34"); AssertRange::AreEqual(< Token(Operator::Plus), Token(12.34) >, tokens); >
Тест не компилируется, так как не хватает оператора сравнения для токена. Исправим это, теперь тест просто не проходит. Для начала сделаем небольшой рефакторинг. Добавим переменную result , в которую будем помещать токены.
inline Tokens Tokenize(std::wstring expr) < Tokens result; const wchar_t *current = expr.c_str(); if(!*current) return result; if(iswdigit(*current)) < result.push_back(_wtof(current)); >else < result.push_back(static_cast(*current)); > return result; >
Теперь заставить пройти тест довольно просто: применим трансформацию (if → while). Можно было бы использовать рекурсию, но я решил целенаправленно делать не рекурсивный алгоритм.
inline Tokens Tokenize(std::wstring expr) < Tokens result; const wchar_t *current = expr.c_str(); while(*current) < if(iswdigit(*current)) < wchar_t *end = nullptr; result.push_back(wcstod(current, &end)); current = end; >else < result.push_back(static_cast(*current)); ++current; > > return result; >
Функция wcstod делает то же самое, что и _wtof , но также возвращает указатель на следующий за числом символ в строке. Так как все операторы на данный момент состоят из одного символа, то во втором случае просто передвигаем указатель на текущий символ на одну позицию вперёд. Как видим, теперь все тесты проходят.
В ответ на строку с простым выражением должен возвращаться список соответствующих токенов.- Пробелы между числами и операторами должны игнорироваться.
TEST_METHOD(Should_skip_spaces) < Tokens tokens = Lexer::Tokenize(L" 1 + 12.34 "); AssertRange::AreEqual(< Token(1.0), Token(Operator::Plus), Token(12.34) >, tokens); >
Применим (unconditional → if) добавив проверку на то, что символ является оператором.
while(*current) < if(iswdigit(*current)) < wchar_t *end = nullptr; result.push_back(wcstod(current, &end)); current = end; >else if(*current == static_cast(Operator::Plus)) < result.push_back(static_cast(*current)); ++current; > else < ++current; >>
На данном этапе проведём рефакторинг данной функции. Выделим логику в отдельный класс и разобьём на отдельный методы. Поместим этот класс в пространство имён Detail чтобы не засорять публичный интерфейс лексера. Теперь функция Tokenize просто будет служить фасадом для модуля лексера.
inline Tokens Tokenize(const std::wstring &expr)
Класс Detail::Tokenizer
namespace Detail < class Tokenizer < public: Tokenizer(const std::wstring &expr) : m_current(expr.c_str()) <>void Tokenize() < while(!EndOfExperssion()) < if(IsNumber()) < ScanNumber(); >else if(IsOperator()) < ScanOperator(); >else < MoveNext(); >> > const Tokens &Result() const < return m_result; >private: bool EndOfExperssion() const < return *m_current == L'\0'; >bool IsNumber() const < return iswdigit(*m_current) != 0; >void ScanNumber() < wchar_t *end = nullptr; m_result.push_back(wcstod(m_current, &end)); m_current = end; >bool IsOperator() const < return *m_current == static_cast(Operator::Plus); > void ScanOperator() < m_result.push_back(static_cast(*m_current)); MoveNext(); > void MoveNext() < ++m_current; >const wchar_t *m_current; Tokens m_result; >; > // namespace Detail
Как видно, извлечение класса сделало код гораздо понятнее. Без тестов такой рефакторинг был бы, как минимум, рискованным. Теперь добавим поддержку скобок и остальных операторов.
TEST_METHOD(Should_tokenize_complex_experssion) < Tokens tokens = Lexer::Tokenize(L"1+2*3/(4-5)"); AssertRange::AreEqual(< Token(1), Token(Operator::Plus), Token(2), Token(Operator::Mul), Token(3), Token(Operator::Div), Token(Operator::LParen), Token(4), Token(Operator::Minus), Token(5), Token(Operator::RParen) >, tokens); >
Добавим необходимые операторы к перечислению Operator , чтобы заставить тест компилироваться.
enum class Operator : wchar_t < Plus = L'+', Minus = L'-', Mul = L'*', Div = L'/', LParen = L'(', RParen = L')', >;
Тест не проходит. Чтобы это исправить необходимо всего лишь изменить метод IsOperator класса Tokenizer .
bool IsOperator() const < auto all = < Operator::Plus, Operator::Minus, Operator::Mul, Operator::Div, Operator::LParen, Operator::RParen >; return std::any_of(all.begin(), all.end(), [this](Operator o) (o); >); >
Все тесты проходят и можно приступить к написанию парсера. Ниже приводится весь исходный код на данный момент.
Interpreter.h
#pragma once; #include #include #include namespace Interpreter < enum class Operator : wchar_t < Plus = L'+', Minus = L'-', Mul = L'*', Div = L'/', LParen = L'(', RParen = L')', >; inline std::wstring ToString(const Operator &op) < return< static_cast(op) >; > enum class TokenType < Operator, Number >; inline std::wstring ToString(const TokenType &type) < switch(type) < case TokenType::Operator: return L"Operator"; case TokenType::Number: return L"Number"; default: throw std::out_of_range("TokenType"); >> class Token < public: Token(Operator op) :m_type(TokenType::Operator), m_operator(op) <>Token(double num) :m_type(TokenType::Number), m_number(num) <> TokenType Type() const < return m_type; >operator Operator() const < if(m_type != TokenType::Operator) throw std::logic_error("Should be operator token."); return m_operator; >operator double() const < if(m_type != TokenType::Number) throw std::logic_error("Should be number token."); return m_number; >friend inline bool operator==(const Token &left, const Token &right) < if(left.m_type == right.m_type) < switch(left.m_type) < case Interpreter::TokenType::Operator: return left.m_operator == right.m_operator; case Interpreter::TokenType::Number: return left.m_number == right.m_number; default: throw std::out_of_range("TokenType"); >> return false; > private: TokenType m_type; union < Operator m_operator; double m_number; >; >; inline std::wstring ToString(const Token &token) < switch(token.Type()) < case TokenType::Number: return std::to_wstring(static_cast(token)); case TokenType::Operator: return ToString(static_cast(token)); default: throw std::out_of_range("TokenType"); > > typedef std::vector Tokens; namespace Lexer < namespace Detail < class Tokenizer < public: Tokenizer(const std::wstring &expr) : m_current(expr.c_str()) <>void Tokenize() < while(!EndOfExperssion()) < if(IsNumber()) < ScanNumber(); >else if(IsOperator()) < ScanOperator(); >else < MoveNext(); >> > const Tokens &Result() const < return m_result; >private: bool EndOfExperssion() const < return *m_current == L'\0'; >bool IsNumber() const < return iswdigit(*m_current) != 0; >void ScanNumber() < wchar_t *end = nullptr; m_result.push_back(wcstod(m_current, &end)); m_current = end; >bool IsOperator() const < auto all = < Operator::Plus, Operator::Minus, Operator::Mul, Operator::Div, Operator::LParen, Operator::RParen >; return std::any_of(all.begin(), all.end(), [this](Operator o) ); > void ScanOperator() < m_result.push_back(static_cast(*m_current)); MoveNext(); > void MoveNext() < ++m_current; >const wchar_t *m_current; Tokens m_result; >; > // namespace Detail inline Tokens Tokenize(const std::wstring &expr) < Detail::Tokenizer tokenizer(expr); tokenizer.Tokenize(); return tokenizer.Result(); >> // namespace Lexer > // namespace Interpreter
InterpreterTests.cpp
#include "stdafx.h" #include "CppUnitTest.h" #include "Interpreter.h" namespace InterpreterTests < using namespace Microsoft::VisualStudio::CppUnitTestFramework; using namespace Interpreter; using namespace std; namespace AssertRange < templatestatic void AreEqual(initializer_list expect, const ActualRange &actual) < auto actualIter = begin(actual); auto expectIter = begin(expect); Assert::AreEqual(distance(expectIter, end(expect)), distance(actualIter, end(actual)), L"Size differs."); for(; expectIter != end(expect) && actualIter != end(actual); ++expectIter, ++actualIter) < auto message = L"Mismatch in position " + to_wstring(distance(begin(expect), expectIter)); Assert::AreEqual(*expectIter, *actualIter, message.c_str()); > > > // namespace AssertRange TEST_CLASS(LexerTests) < public: TEST_METHOD(Should_return_empty_token_list_when_put_empty_expression) < Tokens tokens = Lexer::Tokenize(L""); Assert::IsTrue(tokens.empty()); >TEST_METHOD(Should_tokenize_single_plus_operator) < Tokens tokens = Lexer::Tokenize(L"+"); AssertRange::AreEqual(< Operator::Plus >, tokens); > TEST_METHOD(Should_tokenize_single_digit) < Tokens tokens = Lexer::Tokenize(L"1"); AssertRange::AreEqual(< 1.0 >, tokens); > TEST_METHOD(Should_tokenize_floating_point_number) < Tokens tokens = Lexer::Tokenize(L"12.34"); AssertRange::AreEqual(< 12.34 >, tokens); > TEST_METHOD(Should_tokenize_plus_and_number) < Tokens tokens = Lexer::Tokenize(L"+12.34"); AssertRange::AreEqual(< Token(Operator::Plus), Token(12.34) >, tokens); > TEST_METHOD(Should_skip_spaces) < Tokens tokens = Lexer::Tokenize(L" 1 + 12.34 "); AssertRange::AreEqual(< Token(1.0), Token(Operator::Plus), Token(12.34) >, tokens); > TEST_METHOD(Should_tokenize_complex_experssion) < Tokens tokens = Lexer::Tokenize(L"1+2*3/(4-5)"); AssertRange::AreEqual(< Token(1), Token(Operator::Plus), Token(2), Token(Operator::Mul), Token(3), Token(Operator::Div), Token(Operator::LParen), Token(4), Token(Operator::Minus), Token(5), Token(Operator::RParen) >, tokens); > >; TEST_CLASS(TokenTests) < public: TEST_METHOD(Should_get_type_for_operator_token) < Token opToken(Operator::Plus); Assert::AreEqual(TokenType::Operator, opToken.Type()); >TEST_METHOD(Should_get_type_for_number_token) < Token numToken(1.2); Assert::AreEqual(TokenType::Number, numToken.Type()); >TEST_METHOD(Should_get_operator_code_from_operator_token) < Token token(Operator::Plus); Assert::AreEqual(Operator::Plus, token); > TEST_METHOD(Should_get_number_value_from_number_token) < Token token(1.23); Assert::AreEqual(1.23, token); > >; >
Код к статье на GitHub. Названия коммитов и их порядок соответствуют тестам и рефакторингам, описанным выше. Окончанию первой части соответствует метка «Конец_первой_части».
Разработка парсера будет рассмотрена во второй части. Разработка вычислителя и фасада для парсера, а также рефакторинг всего кода будут рассмотрены в третьей части.
- c++
- TDD
- интерпретатор
- test driven development
- разработка через тестирование
- рефакторинг
- юнит-тесты
Интерпретатор
Интерпретатор (interpreter) — это программа, которая выполняет код, написанный на языке программирования. Она не переводит его в машинные коды целиком, а построчно принимает команды и сразу выполняет их. Можно отдать интерпретатору команду и сразу понять, сработала ли она.

«IT-специалист с нуля» наш лучший курс для старта в IT
Это один из двух способов перевода кода в понятный компьютеру вид. Второй вариант — компиляция, когда код «переводят» в машинные команды целиком. Мы расскажем об их различиях ниже.
Языки программирования, которые работают с интерпретаторами, называются интерпретируемыми. Большинство популярных сейчас языков именно такие. Некоторые из них — популярный вариант для обучения информатике и программированию.
Для чего нужны интерпретаторы
Языки программирования создаются такими, чтобы писать на их было удобно человеку. Они близки к английскому языку, команды на них — человекопонятные. Это в первую очередь касается высокоуровневых ЯП — тех, которые ближе к человеку, чем к «железу».
Компьютер такие языки не понимает. По умолчанию код на них для него — обычный текст, а не команды. Ведь компьютер — вычислительная машина, для которой понятен не текст, а наборы из нулей и единиц. Но писать команды на так называемых машинных кодах — задача очень трудная, почти невозможная. Ведь они очень длинные, особенно если программа сложная, и непонятны человеку.
Поэтому при разработке языка программирования для него всегда создается программа, переводящая код на нем в понятный компьютеру вид. Интерпретатор — один из вариантов, как можно реализовать этот процесс. Программа на интерпретируемом языке без интерпретатора просто не запустится — компьютер ее не поймет.
То есть интерпретатор нужен, чтобы программы на том или ином языке могли запускаться и выполняться.
Профессия / 8 месяцев
IT-специалист с нуля
Попробуйте 9 профессий за 2 месяца и выберите подходящую вам

Как работает интерпретатор
Интерпретатор получает на вход команду, написанную на языке программирования, читает ее и сразу же выполняет. Его работу можно условно разделить на два режима: интерактивный и пакетный.
Интерактивный режим. Это обычно работа в консоли. Его еще называют REPL — Read-eval-print loop, цикл чтения, исполнения и печати. Работает он так. Человек пишет в консоли какую-то команду интерпретатору, и она тут же выполняется, как только он нажимает Enter.
Обычно для этого сначала нужно запустить интерпретатор отдельной командой, но не всегда. Например, если команда на JavaScript пишется в консоли браузера, ничего дополнительно включать не надо — в браузеры по умолчанию встроен интерпретатор JS.
Пакетный режим. Он используется для более крупных задач, таких, которые не получится писать построчно и выполнять сразу же. Человек пишет какой-то код в файле, сохраняет его с нужным расширением и отдает интерпретатору. Тот получает файл, построчно считывает написанный там код и выполняет его.
В пакетном режиме пишется большинство программ: писать их в консоль и сразу же выполнять было бы неудобно. А REPL используют для быстрых подсчетов, отправки запросов и других действий, которым достаточно одной команды.
Разница между интерпретатором и компилятором
Еще один способ «переводить» код в понятный машине вид — компиляция. Программы, которые ею занимаются, называются компиляторами, а языки, которые с ними работают, — компилируемыми. Разница в том, что компилятор ничего не выполняет: он получает на вход файл с кодом, расшифровывает его и переводит в машинные коды целиком. Получается исполняемый файл, который можно запустить внутри той или иной операционной системы.
У интерпретаторов и компиляторов есть ряд различий — теоретических и чисто практических.
- Интерпретатор работает с кодом построчно, а компилятор переводит весь блок кода целиком.
- Интерпретатор исполняет код, как только «прочтет» нужную строку, а компилятор отдает его на выполнение системе — сам он только переводит.
- С компилятором нельзя работать в режиме REPL — только в пакетном.
Интерпретатор можно сравнить с синхронным переводчиком, который сразу же озвучивает перевод. А компилятор — с литературным переводчиком, который переводит тексты, а потом отправляет перевод тем, кто будет с ним работать.

Курс для новичков «IT-специалист
с нуля» – разберемся, какая профессия вам подходит, и поможем вам ее освоить
Известные интерпретируемые языки и их применение
Самые популярные сегодня языки, работающие с интерпретатором, — JavaScript и Python. Первый обрел популярность благодаря тому, что именно он работает в браузере. Поэтому им активно пользуются в вебе, особенно во фронтенде. Python же применяют в машинном обучении, анализе данных, при работе с математикой, аналитикой и автоматизацией, а еще в вебе и во многих других отраслях.
Другие примеры интерпретируемых языков — PHP, который активно применяют в вебе для «серверной» части, универсальный Ruby и Perl — его часто используют для автоматизации, системного администрирования или работы с текстом.
Интерпретируемые языки не зависят от системы, поэтому их в теории можно использовать где угодно. Главное, чтобы на конечном устройстве, где будет запускаться код, был установлен интерпретатор.
А вот там, где нужно максимальное быстродействие или близость к «железу», обычно предпочитают компилируемые языки. В них код исполняется быстрее за счет перевода в машинные коды: система понимает их и быстро выполняет.
У некоторых языков, таких как Basic или Python, есть и компилируемая, и интерпретируемая версии. Они используются для разных целей.
Несколько интерпретаторов у одного языка
Чаще всего бывает так, что у одного языка есть несколько интерпретаторов, или, как говорят, несколько реализаций или движков. Яркий пример — JavaScript: там основных интерпретаторов три, причем одним пользуются браузеры на базе Chromium, другим — Mozilla, а третьим — браузеры Apple.
Разные реализации могут чуть-чуть отличаться в исполнении или в обработке некоторых сложных запросов. У каждого движка свои механизмы оптимизации, свои особенности. Если предполагается, что код будет запускаться из-под разных движков, желательно учесть их все. Например, тот же JavaScript: фронтенд-разработчики, работающие с «браузерной» частью сайта, должны учитывать и тестировать поведение кода в разных браузерах. Но общий принцип работы похож у всех.
Как именно должен работать код, определяет спецификация — большой документ, обычно от разработчиков языка, который подробно описывает все возможные команды и принципы. Создатели интерпретаторов ориентируются именно на этот документ.
Достоинства интерпретируемых языков
Интерактивность. Код выполняется построчно, и это удобно. Если нужно протестировать какой-то простой запрос, можно просто включить интерпретатор и отдать ему команду в консоли — все выполнится тут же без временных затрат на компиляцию и передачу системе. А еще так можно выполнять простые действия, например для автоматизации рутины.
Скорость освоения и написания кода. Благодаря уже упомянутой интерактивности язык проще учить. Во-первых, сразу видно, к какому результату приведет то или иное действие, во-вторых, если появится ошибка, интерпретатор укажет где. Да и код пишется быстрее — по этой же причине.
Удобство отладки. Отлаживать код, написанный на интерпретируемом языке, проще по той же причине: он исполняется сразу. В случае с компилируемыми языками ошибка может возникнуть в одном месте, а заметной для компилятора стать в другом. Это затрудняет отладку. С интерпретируемыми языками в этом смысле проще: интерпретатор начинает «ругаться», как только доходит до ошибочного места.
Более того: если компилируемая программа обнаружит ошибку, она не выполнится совсем. А интерпретируемая — выполнится до места сбоя. Это тоже удобно при поиске ошибок, а еще помогает решить хотя бы часть задачи, прежде чем программа «упадет».
Понятность для человека. Большинство интерпретируемых языков — высокоуровневые, близкие к человеку. Поэтому у них понятный синтаксис, код на них легко читается, а сами языки освоить относительно просто. И уж точно проще, чем писать на низкоуровневых языках или вообще на машинных кодах.
Независимость от платформы. Исполнение берет на себя интерпретатор, а не система. Поэтому язык не зависит от ОС, то есть по умолчанию кроссплатформенный. Это выгодно отличает интерпретируемые языки от компилируемых: в них исполнение напрямую зависит от системы. Ведь разные ОС воспринимают машинные коды по-разному, поэтому программы-компиляторы — свои для каждой системы . И код, скомпилированный для Windows, не запустится в Linux или на Mac.
Механизмы оптимизации. На руку программисту играют и сами интерпретаторы: в них встраивают механизмы, которые оптимизируют код сами. Это значит, что, если код написан не самым оптимальным способом, интерпретатор сгладит недостаток оптимизации за счет встроенных механизмов.
Конечно, это не означает, что нужно забыть о качестве кода. Оно все еще важно. Но такая возможность позволяет сконцентрироваться на том, чтобы код был чистым и понятным для человека, и не жертвовать понятностью ради нескольких миллисекунд.
Недостатки интерпретируемых языков
Низкая скорость программ. Сами программы на интерпретируемых языках работают медленнее, чем на компилируемых, потому что код выполняет не система напрямую. Но разница в скорости сглаживается за счет двух факторов: оптимизация самих интерпретаторов и отсутствие промежуточного шага — компиляции, перевода в машинный код. Хотя в критичных по быстродействию процессах предпочитают использовать компилируемые языки, для большинства задач интерпретируемых вполне достаточно.
Риск ошибки. Выше мы говорили, что отлаживать код на интерпретируемых языках легче. Это так, но у преимущества есть оборотная сторона. Так как программа запускается сразу и выполняется до ошибочного места, какую-то ошибку в редко выполняемых блоках кода легко пропустить. Есть риск, что программа выполнится, а разработчик так и не узнает, что в ней ошибка. Компилируемые языки от этого защищены: если ошибка есть хоть где-то, программа просто не запустится. Проблемы, правда, можно избежать за счет качественного тестирования.
Зависимость от установленного интерпретатора. Интерпретируемые языки не зависят от операционной системы, но зависят от интерпретатора. Чтобы код запустился, нужно не только скачать файл с ним, но и установить интерпретатор. Если тот не установлен, компьютер просто не сможет выполнить программу — нечему будет понимать ее. А вот исполняемый файл, созданный через компилятор, можно запустить где угодно, но только в нужной ОС.
Открытость исходного кода. При компиляции в машинные коды «исходники» скрываются, и никто не может посмотреть, как была реализована та или иная функция. Интерпретируемые языки такого достоинства лишены: код не переводится в исполняемый файл, а выполняется интерпретатором сразу. Поэтому исходники остаются открытыми и их может посмотреть кто угодно. Это минус, если код программы — коммерческая тайна или разработчик просто по какой-то причине хочет его скрыть. Придется пользоваться дополнительными инструментами, чтобы «спрятать» написанное.
Какие языки лучше учить: компилируемые или интерпретируемые
Нет однозначного ответа, что лучше: у обоих типов свои сферы применения, достоинства и недостатки. Поэтому мы рекомендуем ориентироваться на область, в которой вы хотите работать, и на языки, которые в ней принято использовать. Например, в геймдеве и при создании приложений под разные ОС обычно применяют компилируемые языки. В вебе и для автоматизации — интерпретируемые. А еще вы можете попробовать разные варианты и разобраться, какой подход вам ближе.
Узнайте больше о программировании и информационных технологиях на наших курсах. Мы поможем получить новую высокооплачиваемую профессию.
IT-специалист с нуля
Наш лучший курс для старта в IT. За 2 месяца вы пробуете себя в девяти разных профессиях: мобильной и веб-разработке, тестировании, аналитике и даже Data Science — выберите подходящую и сразу освойте ее.
Какой тег нужно использовать чтобы интерпретатор не игнорировал пробелы между словами
Пробел в html имеет большое значение для форматирования текста. Зачастую происходит так: пользователь ищет какую-то информацию, переходит на сайт и видит там нужную ему, но плохо отформатированную страницу нечитабельного текста – скорее всего, он не станет себя мучить и « ломать глаза », а найдет ресурс, на котором та же информация будет удобна для чтения:

Следствием такой недоработки, а именно отталкивающего вида текста, является плохая посещаемость сайта.
Ставим пробел в html
В том, как поставить пробел в html , не должно возникнуть проблем, так как браузер интерпретирует его и выводит на экран, но только в том случае, если пробел между словами единичный. Сразу же следует вопрос: « Как сделать пробел в html, когда между словами необходимо поставить более одного пробела? ».
В языке гипертекстовой разметки предусмотрены три варианта, как прописать пробел в html :
- &ensp – узкий пробел;
- &emsp – широкий пробел;
-   – неразрывный пробел.
Функциональность узкого и широкого пробела ясна из их названия. Рассмотрим html код пробела, который называют неразрывным.
Неразрывный пробел
Неразрывный пробел является одним из списка специальных символов html :
Привет тебе я шлю!
С неразрывным пробелом!
В названии специального символа содержится сокращение nbsp – от английского non-breaking space . Из названия следует, что это непереносимый пробел. Он не позволяет браузеру перенести (разделить) строку в месте применения. Отсюда и третье название – неделимый пробел:

Ниже приведены случаи, когда необходимо применять данную разновидность пробела:
- При написании инициалов и фамилии ( Н.   В.   Гоголь );
- При сокращенном обращении ( г-жа   Петрова );
- При обозначении параграфов ( №  9 );
- В написании версии программного продукта ( android   4.4   kitkat );
- При написании многозначных чисел ( 3   231   821   байт ).
Это далеко не полный список применения неразрывного пробела в местах, где того требует типография, а лишь основные пункты.
Длинный пробел в html
Что же делать, когда необходимо отобразить символ пробела в html длиной более одного стандартного?

Для решения данной задачи можно использовать длинный пробел ( &emsp ), о котором было упомянуто ранее. Но что, если нужно поставить пробел еще длиннее? Для этого используется неразрывный пробел html . Вставив специальный символ   несколько раз, друг за другом, можно получить пробел нужной длины.
Исходя из изложенного выше, можно сделать следующий вывод: чтобы отобразить знак пробела в html нужной длины, используются специальные символы: &ensp, &emsp,   . Нужно понимать, что для создания длинного неразрывного пробела между символами необходимо использовать   , иначе браузер может перенести символ, стоящий после пробела, на следующую строку.
Какой тег нужно использовать чтобы интерпретатор не игнорировал пробелы между словами
В CSS есть такое полезное свойство, как white-space, которое остаётся без внимания у начинающих верстальщиков. Возможно, вы обходились без него довольно долго, но однажды узнав, что это такое, и как его использовать, вы поймёте как много вы потеряли.
В этой статье я постараюсь описать, в чём разница между различными значениями свойства и как они могут быть использованы.
Немного об HTML.
В HTML, всякий раз когда вы оставляете подряд несколько пробелов (табов или переводов строки), браузер, по умолчанию, будет выводить их как один единственный пробел. Такое поведение позволяет браузеру отделять и размещать элементы наиболее удобным для прочтения способом.
Если вы хотите чтобы все ваши пробелы и переводы строк отображались как в исходном HTML, то вам необходимо использовать тег pre, всё содержимое которого будет отображаться в соответствии с исходным кодом страницы.
Кроме того, можно воспользоваться неразрывным пробелом ( ), в случае, если вам необходимо, чтобы строки не «схлопывались». Также, в предыдущих версиях HTML был тег nobr для таких целей. Сейчас этот тег не рекомендуется к использованию.
Свойство white-space — это шаг к семантически чистому HTML. Вы можете настроить обработку браузером пробелов, используя CSS.
Определение и возможные значения.
Свойство white-space предназначено для определения поведения браузера при обработке множественных пробелов и переводов строк. Конечно, обрабатываемая часть документа ограничивается CSS-селектором.
Ниже перечислены допустимые значения свойства с описанием каждого из них:
white-space: normal
Значение по умолчанию. Если оно установлено явно, то результатом будет обычный вывод, без использования тега pre. Как и в случае с большинством CSS-свойств, существует только одна причина использовать это значение, когда вы установили это свойство где-либо выше по иерархии свойств или элементов, для того чтобы вернуть обычное поведение элемента.

white-space: nowrap
Это наиболее используемое значение свойства, поскольку оно делает поведение браузера точно таким же, как и в случае со значением normal, за исключением того, что подавляются разрывы строк, даже в тех случаях когда выводимый текст получается шире чем контейнер для вывода.
Элемент, для которого значение свойства установлено как nowrap, не позволяет тексту и другим inline-элементам переносится естественным образом на новую строку. Вместо этого он продолжает вывод за своими границами, до тех пор, пока текст не закончится, оставляя его на одной линии. Это значение не оказывает никакого эффекта на повторяющиеся пробелы между словами, они по-прежнему «схлопываются» в один, как обычно.

white-space: pre
Это значение работает именно так, как ожидается: точно также, как и содержимое тега pre. Все пробелы и переводы строк выводятся точно также как и в исходном HTML. Если какая-нибудь строка шире, чем её родитель, то она не будет разрываться, а будет выводится как одна строка.

white-space: pre-line
Это свойство работает также как и normal, за исключением одного момента: переводы строк в исходной разметке являются значимыми. Таким образом, если в разметке между словами несколько пробелом, они будут проигнорированы как обычно, однако, если в разметке встречается перевод строки, при выводе, текст также будет перенесён на новую строку. Это значение не поддерживается в Internet Explorer до 7-ой версии, FireFox до 3-ей версии и Opera до версии 9.2.

white-space: pre-wrap
Это значение определяет такое же поведение как и значение pre, за тем исключением что строка переносится в соответствии с границами родительского элемента. Таким образом, текст будет переносится на новую строку, как это было бы при значении normal, а также будут считываться множественные пробелы и переводы строк исходного HTML. Это свойство не поддерживается в Internet Explorer до версии 7, а также FireFox до версии 3.

Варианты использования
Возможно, наиболее частый случай использования свойства white-space — это применение его к ссылке, которую вы не хотите переносить на новую строку.

На показанном скриншоте, ссылка «Read more »» кавычка (») перенеслась на новую строку, поскольку ей не хватило места. Этого можно избежать применив к ссылке значение nowrap. В этом случае ссылка будет перенесена на новую строку целиком, как неразрывный элемент. Обратите внимание, что свойство white-space было применено только к содержимому элемента. Поэтому ссылка и была перенесена на новую строку целиком. Текст внутри неё — неразрывен.
Заблуждения
У новичков вёрстки часто возникает недопонимание при использовании white-space: nowrap, в случае если они применяют его к inline-элементу и ожидают что он не будет переносится на новую строку. Стоит запомнить, что свойство применяется только к inline-элементам, которые находятся внутри элемента, к которому его применили, а также не оказывают никакого эффекта на блочные элементы и отступы между ними.
Пробел в html
В этой инструкции вы узнаете о том как вставить неразрывный пробел в HTML. Расскажу об особенностях и нюансах вставки длинных пробелов в код.
Самое главное — не стоит путать неразрывный пробел с обычным!
Что такое неразрывный пробел в HTML?
Неразрывный пробел — спецсимвол в HTML (см. также что такое HTML), благодаря которому браузер будет воспринимать каждый из этих пробелов как те, которые нужно отобразить, а не игнорировать.
Пример предельно простой. Многие из вас наверное помнят, как осуществляли «форматирование пробелами» какого-нибудь важного документа, будучи начинающими пользователями Ворда. Поставишь 30 пробелов подряд и, что называется, будет выравнено как надо!
В HTML такие фокусы не прокатят. Если я сейчас напишу 30 пробелов при написании поста в HTML-редакторе, он просто проигнорирует их, отображая лишь один.
И, соответственно, не проигнорирует неразрывный пробел как элемент форматирования вашего HTML-кода.
Еще одна фишка длинного неразрывного пробела в том, что использование его в длинной строке «запрещает» ее разбиение в местах использования. Что это значит? Это значит, что такой пробел будет уместно использовать в тех местах, где перенос словосочетания на новую строку нужно осуществить полностью, не разбивая его на отдельные слова.
Как в HTML сделать неразрывный пробел?
Приведу пример для интернет-магазинов. В длинной строке использование пробела в тексте или таблице обычным текстом «25 000 долларов» может привести к тому что в отдельных случаях у вас «25» будет на одной строке, а «000 долларов» — на другой. Сходные ситуации действительно возможны и неразрывный пробел страхует от этого. Код:
При использовании этого кода, запись «25 000 долларов» будет целиком на новой строке, если браузер вдруг захочет ее перенести (в случае если не влезает).
Пример кода неразрывного пробела (non-breaking space) позволит вам сделать пробел в любом месте HTML-страницы:
Неразрывный пробел, это не тег, а спецсимвол. Поэтому его нужно сопровождать сначала амперсандом, а в конце — точкой с запятой. Что это дает? Браузер видит где у спецсимвола начало и конец, а значит вы сможете вставить несколько пробелов подряд:
Обычно конечно это не требуется. Все гораздо приземленнее, например, если нужно перенести на новую строку ФИО целиком, а не делая инициалы «повисшими» на предыдущей строке (или если речь идет о большом и длинном числе вида 900 000 000). Код:
Кстати, в Ворде неразрывный пробел вставляется CTRL+SHIFT+SPACE.
Распространено некорректное применение неразрывного пробела в верстке — для задания отступов между элементами навигации или абзацем и картинкой. Вы должны понимать, что не надо так. Лучшее решение подобных задач — тегом или .
- Если у вас встречается потребность в нескольких пробелах подряд, используйте спецсимвол nbsp между словами столько раз, сколько нужно.
- Помните, что длинный пробел не позволяет разорвать строку на месте словосочетания — используйте этот спецсимвол с умом, не злоупотребляя и не применяя в качестве «напильника» для того, чтобы поправить немного верстку.
- Для регулирования длины пробела используйте ensp и emsp. Ensp — длинный пробел, равный длине двух обычных пробелов (длина «N»). Emsp — самый длинный пробел, по длине равный четырем обычным пробелам (длина «M»).
Есть также принудительный разрыв строки br и предварительное форматирование текста тегами , но это тема отдельной статьи и поговорим мы об этом как-нибудь потом.
Памятка: не забудьте о том, что набор HTML-кода нужно осуществлять в редакторе для обработки кода (в WordPress есть специальная вкладка для этого).
Похожие публикации:
- Какое действие не характеризует первоклассное выражение javascript
- Какое приложение позволяет открывать файлы обозначенные следующим значком
- Какой бензонасос лучше поставить на заз 968м
- Какой карбюратор лучше поставить на уаз
Найти пробелы между словами внутри ковычек
Не могу до конца решить вопрос с regex. Мне нужно найти пробелы в словах между ковычками (чтобы потом заменить их на подчеркивание). Как правильно написать регулярное выражение? Пробовал ([«]*\w)(\s) но это не совсем то, потому что попадают пробелы находящиеся после ковычек. Спасибо. Пример текста, на котором тестирую (пробелы внутри ковычек): test «test1 test2» test3 «t4 t5 test6» test7 Хочу потом получить: test «test1_test2» test3 «t4_t5_test6» test7
Отслеживать
задан 19 июн 2020 в 8:36
Kirill Volodin Kirill Volodin
9 1 1 бронзовый знак
Без языка программирования никак. Потому что с одной и с другой стороны у вас одинаковые разделители ( » ). Есть, конечно, один вариант, но очень неэффективный. Очень популярный, но, к сожелению, всё может повиснуть, если текст очень длинный.
19 июн 2020 в 8:40
Split по кавычке, Replace в чётных, Join обратно.
19 июн 2020 в 9:04
Да, тут придется использовать что-то вроде replace для замены пробелов. Не знаю вашей цели. Но получить фрагменты кода в кавычках можно таким выражением : [«][0-9 \w]*[«] . чистый split вместо regex добавит работы
19 июн 2020 в 9:05
получить фрагменты кода в кавычках какой смысл их получать-то? конечная цель вроде чётко обозначена — заменить. А получение не даёт для её достижения ничего — более того, только создаст дополнительные проблемы в случае какого-нибудь a a»a a»a a»a a»a a .
19 июн 2020 в 9:44
Если подробнее про задачу, то есть такие строки описания полей БД: «Location Code» character varying(10), И есть задача поменять в названии полей пробелы на подчеркивания. Я думал это сделать с помощью регулярного выражения, но возникли сложности с ним.
