...

суббота, 12 сентября 2015 г.

Snaql. Raw SQL в Python-проектах

В последний год у меня появилось новое правило — каждые 3 месяца изучать новый язык программирования и его экосистему. На это есть несколько причин: новые парадигмы, концепции, инструменты, да и просто интересно что там, по ту сторону набившего с годами оскомину Python. Это простое правило позволило изучить за текущий год современные хипстерские Go, Clojure и Rust, проникнуться их идеями и best practices, что, кстати, очень положительно влияет на стиль и качество кода, когда я пишу на своём основном языке.

Рассматривая стек Luminus, я наткнулся на простую и в то же время шикарную, на мой вкус, библиотеку Yesql для организации SQL-запросов в проекте на Clojure и я не увидел чего-то похожего для Python (может плохо искал). Идея этой библиотеки простая — не морочьте себе голову, используйте обычные SQL-запросы, у вас есть возможность именования этих запросов и мапинга на соответствующие динамические функции. Всё это выглядит как набор микро-шаблонов с SQL и их рендер по какому-то контексту. Просто, эффективно, хочу такое у себя в проекте на Python.

Вообще в последнее время мне импонирует мысль, что ORM не нужны. Они переусложняют, на самом деле, работу с реляционными БД, скрывают «адский» SQL за ширмой сложных конструкций собственных объектов, а зачастую выдают и крайне неэффективный результат. Наверняка кто-то поспорит с этим выводом, но моя практика показала, что Django ORM ужасающе простой чуть более чем всегда (и доступен только если вы используете Django, конечно), SQLAlchemy ужасающе сложный, Peewee — ни разу не встречал в дикой природе, к тому же ещё немного и он станет как Alchemy по своему порогу вхождения. SQL — сам по себе мощный и выразительный DSL, вам не нужен ещё один уровень абстракции над ним, серьёзно. Под другим углом я задумался о целесообразности ORM во время очередного проекта на Tornado. Алхимия чудесным алхимическим образом убивает всю асинхронность выполнения обработчика блокирующими вызовами в базу. И вариантов кроме как использовать тот же Momoko с сырыми запросами я не увидел.

Всё, что нам нужно для полного счастья — это разведение SQL-строк и Python-кода по разным углам и некоторая гибкость в построении конструкций по условиям или контексту. Ну и перестать бояться писать SQL, конечно. Изучить SQL до необходимого уровня реально проще чем все нюансы Алхимии для того же результата.

Попробовав и немного переосмыслив Yesql у меня родилась крохотная библиотека Snaql, которая решает описанную выше проблему, хоть и немного по-своему. Я решил вообще не завязываться на клиенты к базам и использовать Jinja2 в качестве движка для парсинга и рендеринга шаблонов с SQL-блоками (со всеми вытекающими возможностями использовать её шаблонную логику). Вот как это выглядит.

1. Ставим Snaql.

$ pip install snaql

2. Создаём в своём проекте папку, куда будем складывать файлы с SQL-блоками. Или несколько таких папок.

/queries
    users.sql

3. В users.sql у нас, например, все запросы, связанные с сущностью пользователя.

{% sql 'users_by_country', note='counts users' %}
    SELECT count(*) AS count
    FROM user
    WHERE country_code = ?
{% endsql %}

Как можно догадаться, SQL помещается внутри блока {%sql%}{%endsql%}, «users_by_country» это название функции, на которую навешивается данный SQL (создаётся динамически), а «note» — это docstring к этой функции, он опционален.

Таких блоков в одном файле может быть сколь угодно много. Главное, чтобы их имена были уникальны.

4. Теперь нам нужна фабрика, которая распарсит такие файлы и создаст набор одноимённых функций.

from snaql.factory import Snaql

# корень проекта
root_location = os.path.abspath(os.path.dirname(__file__))

# регистрация директории с шаблонами
snaql_factory = Snaql(root_location, 'queries')

# регистрация шаблона с SQL-блоками
# users_queries = snaql_factory.load_queries('users.sql')

Извлечь в коде необходимый SQL теперь можно просто вызвав

your_sql = users_queries.users_by_country()

# SELECT count(*) AS count
# FROM user
# WHERE country_code = ?

На самом деле уже этого может быть достаточно. Но не в случае с генерируемыми условиями запроса. В этом случае можно добавить в шаблон всю то логику, которую предоставляет Jinja. Например:

{% sql 'users_select_cond', note='select users with condition' %}
    SELECT *
    FROM user
    {% if users_ids %}
        WHERE user_id IN ({{ users_ids|join(', ') }})
    {% endif %}
{% endsql %}

Если вызвать функцию без контекста:

your_sql = users_queries.users_select_cond()

# SELECT *
# FROM user 

И если с контекстом:

your_sql = users_queries.users_select_cond(users_ids=[1, 2, 3])

# SELECT *
# FROM user 
# WHERE user_id IN (1, 2, 3)

Получив сформированный SQL, остальное — дело техники. Вроде неплохо, да? В любом случае пишите свои «за» и «против» в комментариях, мне интересно мнение сообщества, насколько это может быть удобным кому-то кроме меня.

GitHub, PyPi

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Логируем контекст исключений

В преддверии Дня программиста и по следам Дня тестировщика хочу рассказать о том как упростить жизнь и тем и другим в их общем деле — разработке и отладке ПО.
А именно — расскажу о том как сделать исключения С++ более информативными, а логирование ошибок — более компактным.
После полутора лет работы с Java я привык к исключениям содержащим в себе StackTrace наподобие
Exception in thread "main" java.lang.IllegalStateException: A book has a null property
        at com.example.myproject.Author.getBookIds(Author.java:38)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
        at com.example.myproject.Book.getId(Book.java:22)
        at com.example.myproject.Author.getBookIds(Author.java:35)
        ... 1 more

Переключившись на С++ я через какое-то время был поражён неинформативностью исключений в плане выявления причин ошибки и дальнейшей отладки. Пришлось логировать прохождение всех важных точек программы, чтобы по логам понять где же именно что-то пошло не так и привело к исключению.
Далее я представлю упрощённую эволюцию логирования, через которую прошёл сам.
Исходная программа, которую мы будем отлаживать и улучшать
void foo(int a)
{
        if (a == 0)
        {
                throw runtime_error("foo throw because zero argument");
        }
}

void bar(int a)
{
        foo(a - 10);
}

int main()
{
        try
        {
                for (int i = 0; i < 100; i++)
                {
                        bar(i);
                }
        }
        catch (const exception &e)
        {
                cerr << "Caught exception: " << e.what() << endl;
        }
        return 0;
}



В данном виде не зная ничего о пути вызова foo и функции bar очень сложно понять что делать с полученным исключением

Caught exception: foo throw because zero argument


Добавляем чуть-чуть логов
void bar(int a)
{
        cerr << "Calling foo(" << a - 10 << ")" << endl;
        foo(a - 10);
}

int main()
{
        try
        {
                for (int i = 0; i < 100; i++)
                {
                        cerr << "Calling bar(" << i << ")" << endl;
                        bar(i);
                }
        }
        catch (const exception &e)
        {
                cerr << "Caught exception: " << e.what() << endl;
        }
        return 0;
}



Результат выполнения:

Calling bar(0)
Calling foo(-10)
Calling bar(1)
Calling foo(-9)
Calling bar(2)
Calling foo(-8)
Calling bar(3)
Calling foo(-7)
Calling bar(4)
Calling foo(-6)
Calling bar(5)
Calling foo(-5)
Calling bar(6)
Calling foo(-4)
Calling bar(7)
Calling foo(-3)
Calling bar(8)
Calling foo(-2)
Calling bar(9)
Calling foo(-1)
Calling bar(10)
Calling foo(0)
Caught exception: foo throw because zero argument


Теперь понятно что произошло, но лог оказался чересчур захламлённым. А представим кошмар, в который это превратиться, если программа должна обработать все файлы в директории, на каждый файл приходится 5 строчек лога и исключение возникло через 1000 файлов. Итого — 5000 строк лога о том, как всё хорошо, и 10 строк лога об ошибке.
За время чтения этого лога проклятия коллег-разработчиков и тестировщиков, которые вместо праздника читают мой лог и пишут баг-репорт, загонят мою карму в минус бесконечность.
Значит придётся логировать только «ошибочные» ветви исполнения.
Очевидно что на момент вызова функции неизвестно завершится она нормально или выкинет исключение. Значит запись в лог придётся отложить до выхода из функции и анализировать ход выполнения программы.
Например так
void bar(int a)
{
        try
        {
                foo(a - 10);
        }
        catch (const exception &e)
        {
                string msg = string("calling foo(") + to_string(a - 10) + ") failed";
                throw runtime_error(string(e.what()) + "\n" + msg);
        }
}

int main()
{
        try
        {
                int i;
                try
                {
                        for (i = 0; i < 100; i++)
                        {
                                bar(i);
                        }
                }
                catch (const exception &e)
                {
                        string msg = string("calling bar(") + to_string(i) + ") failed";
                        throw runtime_error(string(e.what()) + "\n" + msg);
                }
        }
        catch (const exception &e)
        {
                cerr << "Caught exception: " << e.what() << endl;
        }
        return 0;
}


Caught exception: foo throw because zero argument
calling foo(0) failed
calling bar(10) failed


Теперь тестировщики легко напишут баг-репорт и приложат к нему красивый, информативный и чистый лог. А вот программа стала уродливой и потеряла всю прелесть исключений — возможность разнести рабочий код и обработку ошибок. Фактически мы вернулись чуть ли не к проверке кодов возврата функции и жестоким временам чистого C. А хочется красивого решения, которое позволит использовать создавать чистый лог и не уродовать программу. Т.е. кто-то должен за нас при выходе из функции проанализировать и залогировать происходящее. Тут нам на помощь приходит подход, уже описаный на Хабре, а именно — запись логов при вызове деструктора.

Итак требования к классу логирования:

  1. Задание сообщения для лога
  2. Вывод сообщения, только в случае исключения

Тут нам на помощь приходит функция bool uncaught_exception(), которая как раз и говорит есть ли необработанное исключение.
Класс ExceptionContext
class ExceptionContext
{
public:
        ExceptionContext(const std::string &msg);
        ~ExceptionContext();

private:
        std::string message;
};

ExceptionContext::ExceptionContext(const std::string &msg): message(msg)
{}

ExceptionContext::~ExceptionContext()
{
        if (std::uncaught_exception())
        {
                std::cerr << message << std::endl;
        }
}



Пример использования
void bar(int a)
{
        ExceptionContext e(string("calling foo(") + to_string(a - 10) + ")");
        foo(a - 10);
}

int main()
{
        try
        {
                for (int i = 0; i < 100; i++)
                {
                        ExceptionContext e(string("calling bar(") + to_string(i) + ")");
                        bar(i);
                }
        }
        catch (const exception &e)
        {
                cerr << "Caught exception: " << e.what() << endl;http://ift.tt/1Q8GwkW
        }
        return 0;
}


calling foo(0)
calling bar(10)
Caught exception: foo throw because zero argument


Видно, что данный вариант сочетает компактность лога (логируем только упавшую ветку исполнения) с компактностью программы (основной код и обработка исключений разнесены, в основной код логирование вставляется одной строчкой). Теперь и разработчики, и тестировщики перестали меня проклинать.
Вообще-то основная цель уже достигнута, но можно пойти по пути множества дальнейших улучшений, в том числе описанных в конце уже упомянутого поста.
Я же рассмотрю всего два момента:
  1. Взаимодействие с другими логгерами
  2. Потокобезопасность

Распечатывание лога напрямую в cerr может быть неудобно отсутствием возможности получить с таким трудом накопленый контекст для дублирования его ещё куда-нибудь (да хоть на почту разработчику, раз уж мы существенно сократили его объём!). Опять же, при наличии других инструментов логирования или многопоточного исполнения, возможны неприятные спецэффекты вроде строк лога вперемешку. Поэтому теперь класс ExceptionContext будет логи хранить внутри себя, а наружу выдавать по запросу, на манер printStackTrace из Java.
Потокобезопасный вариант (с использованием С++11)
class ExceptionContext
{
public:
        ExceptionContext(const std::string &msg);
        ~ExceptionContext();

        static void Print(); //! Вывод контекста в cerr с последующей очисткой.
private:
        std::string message;
        static thread_local std::string global_context; //! Хранилище контекста последнего исключения данного потока.
};

ExceptionContext::ExceptionContext(const std::string &msg): message(msg)
{}

ExceptionContext::~ExceptionContext()
{
        if (std::uncaught_exception())
        {
                global_context += message + std::string("\n");
        }
}

void ExceptionContext::Print()
{
        std::cerr << global_context << std::endl;
        global_context.clear();
}

thread_local std::string ExceptionContext::global_context = std::string("");

А catch-блок в основной программе теперь выглядит так:

     catch (const exception &e)
        {
                cerr << "Caught exception: " << e.what() << endl;
                ExceptionContext::Print();
        }



От С++11 здесь используется модификатор thread_local, который гарантирует что в каждом потоке исполнения объект global_context будет собственный, несмотря на то, что он является статическим членом класса и должен быть един для всех экземпляров.

Хороших всем выходных, чистого кода, читаемых логов и остальных успехов в работе!

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Документация всего и вся в одном месте

Stackoverflow объявили о запуске нового амбициозного проекта: документационном хабе для всех существующих технологий. Предполагается, что для каждого существующего на Стэке тэга можно будет создать раздел документации, и в этом разделе постить топики, похожие на существующую парадигму вопрос-ответ, но являющиеся разделами документации.
По задумке Джеффа, Кевина и других основателей сайта, это позволит пользователям самим создавать более качественную документацию, чем зачастую предоставляют авторы той или иной технологии. Тогда программисты смогут вовсе не покидать сайт Стека, где они и так проводят много времени решая свои ежедневные проблемы. Пользовательская вычитка, голосование, примеры кода, единый поиск, мотивация репутацией — есть надежда, что всё это поможет создать на порядок более качественную и востребованную документацию, причём для всех технологий сразу.

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

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

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

На самом деле люди в Стеке очень практичные, у них есть соображения совершенно другого порядка. Постоянно мучающая пользователей и владельцев Стэка проблема это too broad и офф-топик. В комментариях годами идут баталии потому, что людям хочется задавать воопросы «вообще», например «что посоветуете почитать». Был создан отдельный хаб programmers, но он не особо популярен и поток «офф-топиков» и «ту-броадов» не ослабевает. Так вот: есть надежда, что большая часть таких вопросов уйдёт в документацию вместо того чтобы с ними бороться они станут полезной частью сети.

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

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

Ещё важнейший вопрос, который возможно улучшится в результате — это возможность приносить пользу. Сейчас очень много людей пользующихся Стэком хотят написать что-то полезное для других, в чём они класно разбираются, но не могут, потому, что не могут найти подходящих вопросов или не желают учавствовать в гонке ответов. Знаете, на Стэке это есть, в погоне за репутацией люди бросаются отвечать с максимальной скоростью применяя зачастую разные трюки для выведения своего ответа в топ. Теперь можно будет не торопясь создавать топики для документации по огромному количеству тем и, кстати, получать голоса в репутацию.

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

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

Примеры можно «схлопывать» (коллапсить) глобальные ссылки могут вести не только на весь топик, но и на отдельный пример. Вообще редактор будет новый, хотя и похож на редактор вопросника-ответника. Макрдаун, подсветка, увеличенное окно. Кроме топиков, кстати, можно будет создавать запросы (реквесты), за них можно голосовать и топовые реквесты должны вызывать желание писать по ним топики у посетителей. Как это всё будет конкретно сказываться не репутации ещё не ясно. Однако уже ясно: репутация вопросной части сайта и репутация в доках будет общей!

Конечно, имеется путаница в мотивах. С одной стороны основатели бросаются фразами в духе «one ring(docs) to rule them all», с другой стороны говорят, что не надо соревноваться с хорошими существующими документациями, а только работать там, где нет качественных документаций. Всё это вызывает у народа немало вопросов.

Ещё вызывает вопрос, куда постить теперь свои вопросы и проблемы, в новый реквест топика или старый вопросник? Это может вызвать немалую путаницу, например «как сложить две строки на Джаве» это куда, в вопросы-ответы или это реквест на новую статью в доке? Пока не ясно, хотя пушутся гайды (правила) на эту тему.

Импортирование статей из существующих документаций запрещено. Есть лицензионная политика, но здесь я её не буду обсуждать, поскольку статья вводная.

Развернулась широкая дискуссия, приведу ряд вопросов котоые получили широкий резонанс.

Спрашивают: вы говорите, что если у проекта есть хорошая документация, то не надо делать доки для этого проекта на стэке, но как быть в случае если доки были созданы на стэке, а потом у проекта появилась своя крутая документация? Будет ли в таком случае стёрта документация стэка?

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

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

Самую большую кучу плюсов получил комментарий (хотя технически это ответ) человека который запостил комикс из трёх скриншотов, на первом он набрал в гугле «node.js write file», и первая ссылка сверху ведёт на Стэк, на втором страничка открытая по этой ссылке, где сразу бросается искомый пример записи в файл, а на третьем страничка официальной документации по Node.js, где типичный для этого ресурса список поддерживаемых функций и совершенно не понятно что читать дальше. То есть люди ищут примеры. Впрочем само по себе это не доказывает необходимости нового сайта.

Люди очень переживают, что возникнет путаница между официальными документациями и документацией на Стэке. Некоторые считают, что это будет «выживет сильнейший», другие думают, что официальная документация будет только для хардкорщиков и тех, кто пишет доки для Стэка, а большинство будет пользоваться последней, а сами основатели Стэка полагают, что лучше всего официально мигрировать официальные доки на платформу Стэка! Иметь единый, глобальный, обкатанный пользовательский интерфейс и гигантскую общественную вычитку и правку должно быть удобно создателям технологий, разве нет? Создатели open-source проектов, наверняка первыми воспользуются такой возможностью.

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

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

Предлагают ещё сделать сайт для туториалов и хауту.

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

Много плюсов получил комментарий о том, что куча девелоперов ползающих по сайту (мне вспомнились пресловутые воображаемые обезьянки у пишущей машинки пишущие «Войну и мир») никогда не создадут качественной документации, потому что этим должны заниматься профессионалы, которые знают, как это всё правильно структурировать, оформлять, осознающие всю сложность и ответственность задачи. Кроме прочего, документация является источником программерского стиля и паттернов решения проблем, и создать такие доки которые не просто показывают, как можно решить проблему, но объясняющие как её решать положено с точки зрения разработчиков технологии могут только профессиональные докописатели работающие в тесном контакте с разрабами данной технологии. Хотя популярного ответа на этот вопрос не нашлось, но некоторые указали, что когда тысячи программистов пользуются технологией и решают всякие вопросы с ней связанные, то они создают доки как раз такие как и нужны другим пользователям данной технологии и совершенно не факт, что «эти ваши профессиональные писатели в тесном контакте с разработчиками» действительно необходимы. Возможно этот вопрос больше беспокоит самих профессиональных докописателей, ведь их очень много и они получают деньги. Кстати, их работа может не исчезнуть, им просто будут платить за «сидение на Стэке».

Один человек меня очень порадовал приведя примеры офф-топиков которые постоянно убиваются на Стэке, но которые «должен знать каждый», и которым надо-бы найти место на сайте, и которые, кстати, приводят к множеству вопросов которых бы могло и не быть, знай люди эти элементарные вещи: «никогда не используйте scanf», «the type-based aliasing rules are asymmetric» (затрудняюсь перевести) и «зря вы пытаетесь решить эту проблему шелл-скриптом».

Люди, писавшие доки для открытых проектов высказывались в духе, писать официальные доки гораздо приятнее, чем для «каких-то там форумов и вики», пытаясь приводить разные квази-рациональные доводы, например, что авторы открытых проектов любят, когда с ними связываются для уточнения вопросов. Но как-то это меня не очень убеждает. Ведь получается, что Стэк предлагает опен-сорсному комьюнити такую-же площадку для документации, как, скажем Гитхаб для исходников.

Официальная ветка обсуждения на английском языке находится на Мете, все желающие приглашаются принять участие: http://ift.tt/1NSNBY6

Вот такая ситуация складывается, ждём развития. Сам для себя я пока не решил хорошо это или плохо, но одно точно могу сказать, мне очень интересно, что из этого (не)выйдет. Интересно было бы сравнить приведённые мнения англоязычного сообщества, с тем, что думает наше, русскоязычное, не стесняйтесь, оставляйте свои комментарии! И ещё, в заключение, дорогие читатели, вам предлагается маленький опрос:

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Об одной задаче Data Science

Привет, хабр!

Как и обещал, продолжаю публикацию статей, в которой описываю свой опыт после прохождения обучения по Data Science от ребят из MLClass.ru (кстати, кто еще не успел — рекомендую зарегистрироваться). В этот раз мы на примере задачи Digit Recognizer изучим влияние размера обучающей выборки на качество алгоритма машинного обучения. Это один из самых первых и основных вопросов, которые возникают при построении предиктивной модели

Вступление


В процессе работы над анализом данных встречаются ситуации, когда размер выборки доступной для исследования является препятствием. С таким примером я встретился при участии в соревновании Digit Recognizer проводимого на сайте Kaggle. В качестве объекта соревнования выбрана база изображений вручную написанных цифр — The MNIST database of handwritten digits. Изображения были отцентрованы и приведены к одинаковому размеру. В качестве обучающей выборки предлагается выборка состоящая из 42000 таких цифр. Каждая цифра разложена в строку из 784 признаков, значение в каждом является его яркостью.

Для начала, загрузим полную тренировочную выборку в R

library(readr)
require(magrittr)
require(dplyr)
require(caret)
data_train <- read_csv("train.csv")


Теперь, для получения представления о предоставленных данных, изобразим цифры в привычном для человеческого глаза виде.
colors<-c('white','black')
cus_col<-colorRampPalette(colors=colors)

default_par <- par()
par(mfrow=c(6,6),pty='s',mar=c(1,1,1,1),xaxt='n',yaxt='n')

for(i in 1:36)
{
        z<-array(as.matrix(data_train)[i,-1],dim=c(28,28))
        z<-z[,28:1] 
        image(1:28,1:28,z,main=data_train[i,1],col=cus_col(256))
}

par(default_par)

Дальше можно было бы приступить к построению различных моделей, выбору параметров и т.д. Но, давайте посмотрим на данные. 42000 объектов и 784 признака. При попытке построения более комплексных моделей, таких как Random Forest или Support Vector Machine я получил ошибку о нехватке памяти, а обучение даже на небольшой части от полной выборки уже происходит далеко не минуты. Один из вариантов борьбы с этим — это использование существенно более мощной машины для вычисления, либо создание кластеров из нескольких компьтеров. Но в данной работе я решил исследовать, как влияет на качество модели использование для обучение части от всех предоставленных данных.

Теория обучающей кривой


В качестве инструмента для исследования я использую Learning Curve или обучающую кривую, которая представляет собой график, состоящий из зависимости средней ошибки модели на данных использованных для обучения и зависимости средней ошибки на тестовых данных. В теории существуют два основных варианта, которые получатся при построении данного графика.

Первый вариант — когда модель недообучена или имеет высокое смещение (High bias). Основной признак такой ситуации — это высокая средняя ошибка как для тренировочных данных так и для тестовых. В этом случае привлечение дополнительных данных не улучшит качество модели. Второй вариант — когда модель переобучена или имеет большую вариативность (High variance). Визуально можно определить по наличию большого разрыва между тестовой и тренировочной кривыми и низкой тренировочной ошибкой. Тут наоборот больше данных может привести к улучшению тестовой ошибки и, соответственно, к улучшению модели.

Обработка данных


Разобъём выборку на тренировочную и тестовую в соотношении 60/40
data_train$label <- as.factor(data_train$label)
set.seed(111)
split <- createDataPartition(data_train$label, p = 0.6, list = FALSE)
train <- slice(data_train, split)
test <- slice(data_train, -split)


Если посмотреть на изображения цифр, приведённые выше, то можно увидеть, что, т.к. они отцентрованы, то по краям много пространства, на котором никогда не бывает самой цифры. То есть, в данных эта особенность будет выражена в признаках, которые имеют постоянное значение для всех объектов. Во-первых, такие признаки не несут никакой информации для модели и, во-вторых, для многих моделей, за исключением основанных на деревьях, могут приводить к ошибкам при обучении. Поэтому, можно удалить эти признаки из данных.
zero_var_col <- nearZeroVar(train, saveMetrics = T)
sum(zero_var_col$nzv)

## [1] 532

train_nzv <- train[, !zero_var_col$nzv]
test_nzv <- test[, !zero_var_col$nzv]


Таких признаков оказалось 532 из 784. Чтобы проверить как повлияло это существенное изменение на качество моделей, проведём обучение простой CART модели (на которую не должно отрицательно влиять наличие постоянных признаков) на данных до изменения и после. В качестве оценки приведено средний процент ошибки на тестовых данных.
library(rpart)
model_tree <- rpart(label ~ ., data = train, method="class" )
predict_data_test <- predict(model_tree, newdata = test, type = "class")
sum(test$label != predict_data_test)/nrow(test)

## [1] 0.383507

model_tree_nzv <- rpart(label ~ ., data = train_nzv, method="class" )
predict_data_test_nzv <- predict(model_tree_nzv, newdata = test_nzv, type = "class")
sum(test_nzv$label != predict_data_test_nzv)/nrow(test_nzv)

## [1] 0.3838642


Т.к. изменения затронули сотую часть процента, то можно в дальнейшем использовать данные с удалёнными признаками
train <- train[, !zero_var_col$nzv]
test <- test[, !zero_var_col$nzv]


CART


Построим, наконец, саму обучающую кривую. Была применена простая CART модель без изменения параметров по умолчанию. Для получения статистически значимых результатов, каждая оценка проводилась на каждом значении размера выборки пять раз.
learn_curve_data <- data.frame(integer(),
                               double(),
                               double())
for (n in 1:5 )
{
        for (i in seq(1, 2000, by = 200))
        {
                train_learn <- train[sample(nrow(train), size = i),]
                test_learn <- test[sample(nrow(test), size = i),]
                model_tree_learn <- rpart(label ~ ., data = train_learn, method="class" )
                predict_train_learn <- predict(model_tree_learn, type = "class")
                error_rate_train_rpart <- sum(train_learn$label != predict_train_learn)/i
                predict_test_learn <- predict(model_tree_learn, newdata = test_learn, type = "class")
                error_rate_test_rpart <- sum(test_learn$label != predict_test_learn)/i
                learn_curve_data <- rbind(learn_curve_data, c(i, error_rate_train_rpart, error_rate_test_rpart))
        }
}

Усреднение проводилось при помощи модели GAM

colnames(learn_curve_data) <- c("Size", "Train_Error_Rate", "Test_Error_Rate")
library(reshape2)
library(ggplot2)
learn_curve_data_long <- melt(learn_curve_data, id = "Size")
ggplot(data=learn_curve_data_long, aes(x=Size, y=value, colour=variable)) + 
        geom_point() + stat_smooth(method = "gam", formula = y ~ s(x), size = 1)

Что же мы видим?

  • Изменение среднего процента ошибки происходит монотонно, начиная с 500 объектов в выборке.
  • Ошибка как для тренировочных, так и для тестовых данных достаточно высока.
  • Разрыв между тестовыми и тренировочными данными мал.
  • Тестовая ошибка не уменьшается.

Если суммировать — то CART модель явно недообучена, т.е. имеет постоянное высокое смещение. Увеличение выборки для обучения не приведёт к улучшению качества предсказания на тестовых данных. Для того, чтобу улучшить результаты этой модели необходимо улучшать саму модель, например вводом дополнительных значимых признаков.

Random Forest


Теперь, проведём оценку Random Forest модели. Опять же модель применялась «как есть», никакие параметры не изменялись. Начальный размер выборки изменён на 100, т.к. модель не может быть построена, если признаков существенно больше, чем объектов.
library(randomForest)
learn_curve_data <- data.frame(integer(),
                               double(),
                               double())
for (n in 1:5 )
{
        for (i in seq(100, 5100, by = 1000))
        {
                train_learn <- train[sample(nrow(train), size = i),]
                test_learn <- test[sample(nrow(test), size = i),]
                model_learn <- randomForest(label ~ ., data = train_learn)
                predict_train_learn <- predict(model_learn)
                error_rate_train <- sum(train_learn$label != predict_train_learn)/i
                predict_test_learn <- predict(model_learn, newdata = test_learn)
                error_rate_test <- sum(test_learn$label != predict_test_learn)/i
                learn_curve_data <- rbind(learn_curve_data, c(i, error_rate_train, error_rate_test))
        }
}

colnames(learn_curve_data) <- c("Size", "Train_Error_Rate", "Test_Error_Rate")
learn_curve_data_long <- melt(learn_curve_data, id = "Size")
ggplot(data=learn_curve_data_long, aes(x=Size, y=value, colour=variable)) + 
        geom_point() + stat_smooth()

Тут мы видим другую ситуацию.

  • Изменение среднего процента ошибки также происходит монотонно.
  • Тестовая и тренировочная ошибка малы и продолжают уменьшаться.
  • Разрыв между тестовыми и тренировочными данными мал.

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

Support Vector Machine


Прежде чем приступить к исследованию третьей модели — Support Vector Machine, необходимо ещё раз обработать данные. Проведём их стандартизацию, т.к. это необходимо для «сходимости» алгоритма.
library("e1071")
scale_model <- preProcess(train[, -1], method = c("center", "scale"))
train_scale <- predict(scale_model, train[, -1])
train_scale <- cbind(train[, 1], train_scale)
test_scale <- predict(scale_model, test[, -1])
test_scale <- cbind(test[, 1], test_scale)


Теперь построим график.
learn_curve_data <- data.frame(integer(),
                               double(),
                               double())
for (n in 1:5 )
{
        for (i in seq(10, 2010, by = 100))
        {
                train_learn <- train_scale[sample(nrow(train_scale), size = i),]
                test_learn <- test_scale[sample(nrow(test_scale), size = i),]
                model_learn <- svm(label ~ ., data = train_learn, kernel = "radial", scale = F)
                predict_train_learn <- predict(model_learn)
                error_rate_train <- sum(train_learn$label != predict_train_learn)/i
                predict_test_learn <- predict(model_learn, newdata = test_learn)
                error_rate_test <- sum(test_learn$label != predict_test_learn)/i
                learn_curve_data <- rbind(learn_curve_data, c(i, error_rate_train, error_rate_test))
        }
}
colnames(learn_curve_data) <- c("Size", "Train_Error_Rate", "Test_Error_Rate")
learn_curve_data_long <- melt(learn_curve_data, id = "Size")
ggplot(data=learn_curve_data_long, aes(x=Size, y=value, colour=variable)) + 
        geom_point() + stat_smooth(method = "gam", formula = y ~ s(x), size = 1)

  • Тренировочная ошибка очень мала.
  • Наблюдается существенный разрыв между тестовой и тренировочной кривой, который монотонно уменьшается.
  • Тестовая ошибка достаточно мала и продолжает уменьшаться.

Я думаю, что перед нами, как раз, второй вариант из теории, т.е. модель переобучена или имеет высокую вариативность. Исходя их этого вывода, можно уверенно сказать, что увеличение размера обучающей выборки приведёт к существенному улучшению качества модели.

Выводы


Данная работа показала, что обучающая кривая (Learning Curve) является хорошим инструментом в арсенале исследователя данных как для оценки используемых моделей, так и для оценки необходимости в увеличении выборки используемых данных.

В следующей раз я расскажу о применении метода главных компонент (PCA) к данной задаче.
Оставатесь на связи!)

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Обзор релиз-кандидата React v0.14

Мы рады представить вам наш первый релиз-кандидат версии React 0.14! Мы опубликовали в июле анонс предстоящих изменениях, но сейчас мы еще больше стабилизировали релиз и нам бы хотелось, чтобы вы попробовали его до того, как мы выпустим финальную версию.

Сообщите нам, если у вас возникли любые проблемы, создав задачу в нашем GitHub репозитории

Инсталляция


Мы рекомендуем использовать React через npm и использовать утилиты, типа browserify или webpack для сборки вашего кода в один пакет:
npm install --save react@0.14.0-rc1
npm install --save react-dom@0.14.0-rc1


Помните, что по умолчанию, в режиме разработки React запускает дополнительные проверки и предоставляет полезные предупреждения. Поэтому, при развертывания вашего приложения установите переменную окружения NODE_ENV в production для использования production-режима, при котором React не включает служебные предупреждения и работает значительно быстрее.

Если вы не можете использовать npm, мы также предоставляем скомпилированные сборки для браузеров для вашего удобства:

Эти сборки также доступны в качестве bower-пакетов: react и react-dom

Основные изменения

Два пакета: React и React DOM


Когда мы смотрим на такие модули, как react-native, react-art, react-canvas и react-three, становится понятно, что красота и сущность React не имеет ничего общего с браузерами или DOM.

Чтобы сделать это более прозрачным и проще для использования в большем количестве окружений, где React может рендерить, мы разделили главный пакет react на два: react и react-dom.
Это открывает путь для написания компонентов, которые могут совместно использоваться в веб-версиях React и React Native. Мы не предполагаем, что весь код в приложении должен быть расшарен, но мы хотим иметь возможность совместного использования компонентов, которые ведут себя одинаково на разных платформах.

Пакет react содержит React.createElement, .createClass, .Component, .PropTypes, .Children и другие хелперы ориентированные на элементы и компонентные классы. Мы думаем о них, как о изоморфных или универсальных хелперах, которые необходимы вам для построения компонентов.

Пакет react-dom содержит ReactDOM.render, .unmountComponentAtNode и .findDOMNode. В react-dom/server у нас есть поддержка рендера на сервере при помощи ReactDOMServer.renderToString и .renderToStaticMarkup.

var React = require('react');
var ReactDOM = require('react-dom');</p>

<p>var MyComponent = React.createClass({
  render: function() {
    return <div>Hello World</div>;
  }
});

ReactDOM.render(<MyComponent />, node);

Мы опубликовали автоматизированный codemod скрипт, который мы используем в Facebook для этого перехода

Дополнения (Add-ons) были перемещены в отдельные пакеты: react-addons-clone-with-props, react-addons-create-fragment, react-addons-css-transition-group, react-addons-linked-state-mixin, react-addons-perf, react-addons-pure-render-mixin, react-addons-shallow-compare, react-addons-test-utils, react-addons-transition-group и react-addons-update, а также ReactDOM.unstable_batchedUpdates в react-dom

На данный момент, используйте пожалуйста определенные версии react и react-dom в ваших приложениях, для предотвращения проблем с версионностью.

Ссылки на DOM узлы


Другим важным изменением, которое мы сделали в этом релизе является то, что ссылки (refs) на компоненты DOM, теперь ссылаются на сам DOM узел.
Что это значит: мы посмотрели на то, что вы делаете с ссылкой (ref) на React DOM компоненты и поняли, что единственная полезная вещь, которую вы можете сделать с ним — это вызов this.refs.giraffe.getDOMNode() для получения DOM узла. В этом релизе, this.refs.giraffe это текущая DOM нода.
Обратите внимание, что ссылки на кастомные (заданные пользователем) компоненты, работают также, как и раньше; только нативные DOM компоненты подпадают под это изменение.
var Zoo = React.createClass({
  render: function() {
    return <div>Giraffe name: <input ref="giraffe" /></div>;
  },
  showName: function() {
    // Previously: var input = this.refs.giraffe.getDOMNode();
    var input = this.refs.giraffe;
    alert(input.value);
  }
});

Это изменение также относится и к возвращаемому результату ReactDOM.render когда мы передаем DOM узел, как корневой компонент. Как и в случаи с refs, эти изменения не распространяются на кастомные компоненты. C этими изменениями, мы объявляем метод .getDOMNode() неподдерживаемым и заменяем его методом ReactDOM.findDOMNode (см. далее).

«Глупые» компоненты


В характерном для React коде, большинство компонентов, которые вы пишете, не должны иметь собственного состояния, т.н. stateless. Мы представляем новый, более простой синтаксис для таких компонентов, в котором вы можете передать props, как аргумент и вернуть элемент, который вы хотите отрисовать:
// Используя ES2015 (ES6) стрелочную функцию:
var Aquarium = (props) => {
  var fish = getFish(props.species);
  return <Tank>{fish}</Tank>;
};

// Или с деструкцией и неявным возвращением:
var Aquarium = ({species}) => (
  <Tank>
    {getFish(species)}
  </Tank>
);

// Далее используем: <Aquarium species="rainbowfish" />

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

React-tools больше не поддерживается


Пакет react-tools и браузерный вариант JSXTransformer.js — нежелательны к применению. Вы можете продолжить их использование в версии 0.13.3, но мы больше их не поддерживаем и рекомендуем перейти на использование Babel, который имеет встроенную поддержку React и JSX.

Оптимизация компилятора


React теперь поддерживает две оптимизации компилятора, которые могут быть включены в Babel 5.8.23 и выше. Обе эти трансформации должны быть включены только в продакшене (например, до минификации) потому что хотя они и улучшают производительность выполнения, но делают служебные предупреждения более скрытыми и пропускают важные проверки, которые происходят в режиме разработки, в том числе propTypes.

Встраивание React элементов: optimisation.react.inlineElements конвертирует JSX элементы в объекты, типа {type: 'div', props: ...} вместо вызова React.createElement.

Постоянное «всплытие» React элементов: optimisation.react.constantElements «поднимает» создание элементов на верхний уровень поддеревьев, который полностью статичен, что уменьшает вызовы React.createElement и перемещения. Важнее то, что это информирует React, что поддерево не изменилось, поэтому React может полностью пропустить его при согласовании.

Критические изменения


Как обычно, у нас есть несколько критических изменений в этом релизе. Всякий раз, когда мы вносим большие изменения, мы предупреждаем, как минимум за один релиз, т.ч. у вас есть время обновить ваш код. Кодовая база Facebook составляет более 15,000 React компонентов, поэтому в команде React, мы всегда стараемся свести к минимуму последствия от критических изменений.

Эти три критические изменения выводили предупреждения в версии 0.13, поэтому вам нет необходимости что-то делать, если в вашем коде нет предупреждений:

  • Объект props теперь фиксирован, поэтому изменение props после создания компонента больше не поддерживается. В большинстве случае, взамен необходимо использовать React.cloneElement. Эти изменения делают ваши компоненты более легкими для понимания и включают оптимизацию компиляции, описанную выше.
  • Простые объекты больше не поддерживаются в качестве потомков React, вместо этого вы должны использовать массивы. Вы можете использовать хелпер createFragment для миграции, который сейчас возвращает массив.
  • Add-Ons: classSet был удален. Вместо этого используйте classnames

А эти два изменения не диагностировались в 0.13, но их легко будет найти и исправить:

  • React.initializeTouchEvents больше не нужно и может быть полностью удалено. Тач-эвенты теперь работают автоматически.
  • Add-Ons: В связи с тем, что ссылки на DOM ноды изменились, как упоминалось выше, TestUtils.findAllInRenderedTree и связанные с ним хелперы больше недоступны для получения DOM компонентов, а только для кастомных компонентов.

Новые исключения, выводимые в предупреждениях


  • В связи с тем, что ссылки на DOM ноды изменились, как упоминалось выше, this.getDOMNode() теперь исключен и взамен вы можете использовать ReactDOM.findDOMNode(this). Заметьте, что в большинстве случаев, вызов findDOMNode теперь не нужен – смотрите пример указанный выше в секции “Ссылки на DOM узлы".

    Если у вас большая кодовая база, вы можете использовать наш automated codemod script для автоматического исправления вашего кода.

  • setProps и replaceProps теперь запрещены. Вместо этого снова вызовите ReactDOM.render на верхнем уровне с новыми props.
  • Классы компонентов ES6 теперь должны расширить React.Component надлежащим образом для поддержки «глупых» компонентов. Шаблон ES3 модуля продолжает работать.
  • Переиспользование и изменение style объекта между рендерами теперь запрещена. Это отражает суть нашего изменения, заморозить props объекта.
  • Add-Ons: cloneWithProps теперь запрещено. Взамен используйте React.cloneElement (в отличии от cloneWithProps, cloneElement не объединяет className или style автоматически, вы можете объединять их вручную, если надо).
  • Add-Ons: Для повышения надежности, CSSTransitionGroup больше не будет слушать transition события. Вместо этого, вы должны вручную устанавливать продолжительность transition используя свойства, такие как transitionEnterTimeout={500}.

Значимые улучшения


  • Добавлен React.Children.toArray который берет объект вложенных потомков и возвращает плоский массив с ключами, назначаемые каждому ребенку. Этот хелпер упрощает управление коллекциями детей в ваших render методах, особенно, если вы хотите перераспределить или извлечь this.props.children перед передачей их далее. В дополнение, React.Children.map также теперь возвращает простой массив.
  • Для предупреждений React теперь использует console.error вместо console.warn, т.ч. браузеры показывают полный стек в консоли. (Наши предупреждения появляются при использовании шаблонов, которые будут изменены в будущих версиях и для кода, который, скорее всего, ведет себя неожиданно, поэтому мы считаем, что наши предупреждения должны быть «must-fix» ошибками.)
  • Ранее, использование ненадежных объектов, в качестве React потомков могло привести к XSS-уязвимости. Эта проблема должна быть должным образом исключена, путем валидирования входа на уровне приложений и не использования ненадежных объектов по всему коду приложения. В качестве дополнительного уровня защиты, React теперь помечает элементы определенным ES2015 (ES6) типом Symbol, в браузерах, которые его поддерживают, для того, чтобы гарантировать, что React никогда не посчитает ненадежный JSON валидным элементом. Если это дополнительная защита безопасности является важной для вас, вы должны добавить Symbol полифил для старых браузеров, например Babel’s polyfill.
  • Когда это возможно, React DOM теперь генерирует XHTML-совместимую разметку.
  • React DOM теперь поддерживает такие стандартные HTML атрибуты: capture, challenge, inputMode, is, keyParams, keyType, minLength, summary, wrap. Он также теперь поддерживает и эти нестандартные атрибуты: autoSave, results, security.
  • React DOM теперь поддерживает следующие SVG атрибуты, которые могут рендерится в namespaced атрибуты: xlinkActuate, xlinkArcrole, xlinkHref, xlinkRole, xlinkShow, xlinkTitle, xlinkType, xmlBase, xmlLang, xmlSpace.
  • SVG тэг image теперь поддерживается в React DOM.
  • В React DOM, произвольные атрибуты поддерживаются на пользовательских элеменах (тех, с дефисом в имени тега или атрибутом is="...").
  • React DOM теперь поддерживает следующие медиа-события на audio и video тэгах: onAbort, onCanPlay, onCanPlayThrough, onDurationChange, onEmptied, onEncrypted, onEnded, onError, onLoadedData, onLoadedMetadata, onLoadStart, onPause, onPlay, onPlaying, onProgress, onRateChange, onSeeked, onSeeking, onStalled, onSuspend, onTimeUpdate, onVolumeChange, onWaiting.
  • Было сделано множество небольших улучшений производительности.
  • Многие предупреждения показывают больше информации, чем раньше.
  • Add-Ons: Дополнение shallowCompare было добавлено в качестве пути миграции для PureRenderMixin для ES6 классов.
  • Add-Ons: CSSTransitionGroup может теперь использовать произвольные имена классов вместо добавления -enter-active или схожего с именем transition.

Новые полезные предупреждения


  • React DOM теперь предупреждает вас, когда вложенные HTML элементы невалидны, что помогает вам избежать удивительных ошибок во время обновления.
  • Подключение document.body напрямую в качестве контейнера для ReactDOM.render теперь выдает предупреждение, т.к. это может вызывать проблемы с расширениями браузера, которые изменяют DOM.
  • Совместное использование нескольких инстансов React не поддерживается, поэтому мы теперь выводим предупреждение, когда обнаруживаем этот случай, чтобы помочь вам избежать проблем.

Значимые исправления


  • Click-события обрабатываются React DOM более надежно в мобильных браузерах, в частности в Mobile Safari.
  • SVG элементы создаются с правильными неймспейсами в большинстве случаев.
  • React DOM теперь правильно рендерит <option> элементы с несколькими текстовыми детьми и рендерит <select> элементы на сервере, с правильно выбранной опцией.
  • Когда два отдельных экземпляра React добавляют узлы в один и тот же документ (в том числе, когда расширение для браузера использует React), React DOM упорно пытается не бросать исключения во время обработки событий.
  • Использование HTML тэгов не в нижнем регистре в React DOM (например, React.createElement('DIV')) теперь не вызывает проблем, однако мы продолжаем рекомендовать использование нижнего регистра для соответствия с конвенцией наименования JSX тэгов (нижний регистр для встроенных компонентов, заглавное наименование для кастомных компонентов).
  • React DOM понимает, что эти свойства CSS являются безразмерными и не добавляет «px» для их значений: animationIterationCount, boxOrdinalGroup, flexOrder, tabSize, stopOpacity.
  • Add-Ons: При использовании тестовых утилит, Simulate.mouseEnter и Simulate.mouseLeave теперь работает
  • Add-Ons: ReactTransitionGroup теперь корректно обрабатывает множественные узлы, удаляя одновременно.

Автор: Ben Alpert

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

[Перевод] Screentendo — генерация уровней для Super Mario Bros на основе содержимого экрана

image

Screentendo – это приложение для настольных компьютеров, которое превращает выделенную область экрана в играбельный уровень для игры Super Mario Bros.

Я раньше не делал приложений для Cocoa app и не использовал Sprite Kit, и это стало для меня отличным упражнением. Исходный код доступен по ссылке.

Как это работает?


При запуске приложения появляется полупрозрачное окно, которое можно двигать и изменять его размер. После размещения окошка над какой-то областью экрана клик внутри него приведёт к тому, что приложение создаст уровень на базе контента, находящегося под ним.

У Screentendo есть два основных этапа работы: обработка картинки для определения структуры выбранного района, и создание уровня.

Обработка картинки


Первый шаг – получить выбранный район экрана под окном. Следующее описание – пример выбора графика внутри Google Sheets внутри Safari:

Приложение использует API CGWindowListCopyWindowInfo API (из Quartz Window Services) для получения списка окон в текущей сессии, в той последовательности, в которой они появляются на экране.
Из этого списка берутся данные окон (id, размеры, и т.п.) для окна Screentendo, и для следующего окна в иерархии (у нас это окно Safari).
Используя информацию об окне, CGWindowListCreateImage делает скриншот из Safari
Скриншот обрезается по границам окна Screentendo, используя разницу между окном Safari и окном Screentendo, и высоту и ширину окна Screentendo.

image
Screentendo находится над целевым окном (график в Google Sheets)

image
Обрезанная картинка

Обрезанная картинка проходит несколько фильтров, перед тем, как её сконвертируют в формат, который Screentendo может использовать для создания игрового уровня.

Motion blur – уменьшает общий шум и визуальные артефакты.
image

Luminance filter – вычисляется средний порог яркости изображения, и уменьшается до двух цветов.
image

Pixellation filter – фильтр пикселизации упрощает детализацию картинки и подготавливает её к разделению на подблоки.
image

Sub-blocking – картинка разбивается на подблоки, по умолчанию размером 10х10 пикселей.
image

image

Average block colour to array – для каждого подблока вычисляется средний цвет. Создаётся двумерный массив, и каждый подблок, примущественно состоящий из чёрного цвета, принимает значение 1, а каждый подблок преимущественно белого цвета, принимает значение 0.
image

Создание уровня и игровая логика


Представление картинки в виде двумерного массива передаётся в класс GameScene, создающий игровой уровень. Он проходит по массиву, и создаёт блоки из значений 1, игнорируя значения 0.

image

image

Спрайты блоков, облаков, фона и игрока


Когда массив обработан, к сцене добавляются фон, облака и игрок. Остаток игры основан на основных физических принципах, представленных в физическом движке Sprite Kit (физика игрока, обработка столкновений, анимация летящих остатков блоков, и т.п.).

У приложения есть меню для изменения размера блока. Уменьшение размеров увеличивает разрешение и время на обработку.

Ограничения


Приложение является доказательством концепции, и у него есть несколько недостатков. Обработка картинки пока происходит очень медленно, разбиение картинки на подблоки медленное (каждый подблок превращается в NSImage, что является не очень эффективным способом решения, но зато быстрым в реализации). Также требуется достаточно сильный контраст в цветах исходной картинки. Физика немного кривая – я не писал эмулятор Super Mario Bros, а просто нечто, что будет работать «достаточно хорошо». Поэтому иногда наблюдаются некие призрачные вершины, которые я пока не поборол.

Код доступен в репозитории github.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

[Из песочницы] Подпольный рынок кардеров. Перевод книги «KingPIN». Глава 10. «Cris Aragon»

Кевин Поулсен, редактор журнала WIRED, а в детстве blackhat хакер Dark Dante, написал книгу про «одного своего знакомого».

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

Начало и план перевода тут: «Шкворень: школьники переводят книгу про хакеров».

Логика выбора книги для работы со школьниками у меня следующая:

  • книг про хакеров на русском языке мало (полторы)
  • книг про кардинг на русском нет вообще (UPD нашлась одна)
  • Кевин Поулсен — редактор WIRED, не глупый товарищ, авторитетный
  • приобщить молодежь к переводу и творчеству на Хабре и получить обратную связь от старших
  • работать в спайке школьники-студенты-специалисты очень эффективно для обучения и показывает значимость работы
  • текст не сильно хардкорный и доступен широкому кругу, но затрагивает вопросы информационной безопасности, уязвимости платежных систем, структуру кардингового подполья, базовые понятия инфраструктуры интернет
  • книга иллюстрирует, что «кормиться» на подпольных форумах — плохо заканчивается

Кто хочет помочь с переводом других глав пишите в личку magisterludi.

(По поводу очередности мне задают много вопросов и советуют публиковать главы по очереди. Я бы тоже так хотел, но увы, так как работаю с с множеством людей, которые, например, уже перевели 80% главы, а потом у них случается форсмажор на 2 недели. С одной стороны на них давить не хочется, с другой стороны откладывать публикацию тех людей, которые перевели уже следующую главу — не совсем честно по отношению к ним. Поэтому, то что есть, публикую.)

Глава 10. «Cris Aragon»

(за помощь с переводом спасибо хабраюзеру Find_The_Truth)

Крис Арагон
imageМакс встретил своего будущего друга и напарника по криминалу, Криса Арагона, в маленькой Италии Сан-Франциско — Норт Биче, где обшарпанные стрип-бары и гадалки сосуществовали с приятными, безвкусными пекарнями и летниками с горячей пастой. Встреча была назначена в кафе неподалеку от книжного магазина Сити Лайтс, колыбели поколения битников в 50х годах, по направлению к кафе Везувио, стены которого украшали росписи с винными бутылками и символами мира. Ниже по холму, над финансовым районом, упираясь в небо, стояла Пирамида Трансамерика.

Норминтон представил Криса Максу под приглушенные стуки кофейных чашек и блюдец. Эти двое поладили сразу. Сорокаоднолетний Крис был студентом восточной духовной школы, вегетарианцем, который занимался йогой для концентрации ума. Макс с его замашками хиппи, казалось, нашел родственную душу. Они даже читали общие книги. И, как и Макс, Крис неоднократно имел проблемы с полицией.
Все началось в Колорадо, когда Крису был двадцать один год. Он работал массажистом на курорте, получая достаточно, чтобы платить за аренду жилья и поддерживать кокаиновую зависимость. Однажды он познакомился с буйным ветераном, Альбертом Си, которого он встретил в кабаке, когда тот отбывал наказание. Си был в бегах и ему были нужны деньги, чтобы уехать из страны. Крис был из привилегированной семьи — его мать, Марлен Арагон, работала в Голливуде талантливой певицей. Не так давно она имела радость участвовать в детском утреннем мультфильме Вызов СуперДрузей на ЭйБиСи, озвучивая мстящую кошку Волшебницы Читу. Однако, он также любил романтические образы преступников — в его квартире на стене висел плакат, представляющий собой обложку альбома Вэйлон Дженнингс Леди Любят Беглецов. Крис взял Альберта в дело и предпринял рад смелых, хоть и неудачных, ограблений банков в курортных городах Колорадо.

Первое ограбление в Аспен Сейвингс энд Лоан начиналось неплохо: было утро, когда Крис в бело-синей бандане, прикрывавшей его брекеты, направил армейский автомат 11мм калибра на менеджера банка, чтобы тот открыл сейф. Он и Альберт затолкали менеджера внутрь, где они обнаружили уборщицу, спрятавшуюся под столом, которая вызывала полицию. Горе-преступники в спешке скрылись. Второе ограбление, в окружном банке Питкина, закончилось, не успев начаться. Партнер Криса спрятался в мусорном баке во дворе, планируя выскочить оттуда с оружием, когда на работу начнут приходить первые сотрудники. План был разрушен, когда Крис, наблюдая с другой стороны улицы, увидел мусоровоз, подъезжающий к баку. Третье ограбление было спланировано лучше. 22 июля 1981 года Крис и Альберт посетили салон Шевроле в Рифле и заявили, что хотели бы попробовать прокатиться на новом Шевроле Камаро. Незадачливый продавец настоял на том, что он поедет с ними, однако, как только они выехали за город, Крис остановился на обочине, а Альберт вышвырнул продавца из машины, угрожая пистолетом. Связав беднягу веревкой, заткнув рот кляпом, грабители бросили его в поле, умчавшись в серебристой спортивной машине.

На следующий день, в 4:50 по полудню, Крис вел украденный Камаро вверх к Вэлли Банк энд Траст в Гленвуд Спрингс, где городские жители тратили свои деньги, которые они зарабатывали в процветающем туристическом бизнесе. Сам Крис был клиентом этого банка. Он ожидал за рулем авто, пока Альберт, в темных очках и с кожаным портфелем, входил в банк. Через несколько минут Альберт выбежал с 10 000 долларов и прыгнул в Камаро, на котором они помчались прочь.

Крис гнал к югу города по грунтовой дороге, которая вилась через скалистые красные холма, вокруг Гленвуд Спринг. Затем они съехали на тропинку, где его подружка ждала с другой машиной. Торжествуя и радуясь, Крис остановился с заносом и поднял столб пыли. Он прыгал и кричал: «Мы сделали это!», когда полицейская машина, едущая на облако пыли, нашла грабителей. Крис и Альберт рванули вверх по скалистым и заросшим горам. Когда Крис споткнулся и упал на кактус, двое полицейских догнали и поймали его. Крис бросил ружье и сдался. Из этой истории Крис вынес урок: самым глупым в этом ограблении было оружие и угнанный автомобиль. Когда, в 1986 году, Крис досрочно вышел после пяти лет в федеральной тюрьме, он увлекся махинациями с кредитными картами и даже имел небольшие успехи. После этого он познакомился с барыгой из Мексики, которого он встретил в кабаке. Крис помог ему с доставкой двух тысяч фунтов (907 кг) марихуаны в двадцатиакровое ранчо, недалеко от Риверсайд, Калифорния, однако сразу же был арестован во время тайной операции отдела по борьбе с наркотиками. В сентябре 1991 года Крис снова попал в тюрьму.

Когда Крис вышел в 1996 году, ему было тридцать пять лет — часть детства и больше половины сознательной жизни он оставил за решеткой. Он пообещал себе не нарушать закон впредь. С помощью мамы он основал бизнес под названием Мишн Пацифик Кэпитал, занимающийся лизингом компьютерной и бизнес-техники для стартап компаний, которые старались быть на волне дот-ком гонки.

Аккуратно подстриженный, приятный, с чарующим взглядом, Крис легко вписался в роль бизнесмена Южной Калифорнии. После жизни полной проблем и неопределенностей, прелести обычной жизни, доступной среднему классу, казались экзотикой. Он любил ездить по конференциям, встречаться с сотрудниками, нанимать новых людей, болтать с коллегами. На одной из маркетинговых конференций Крис встретил Клару Шао Йен, стильную женщину с китайскими корнями, которая эмигрировала из Бразилии. Очарованный красотой и умом Клары, вскоре он женился на ней. Под руководством Криса Мишн Пацифик заработала репутацию инновационной компании, одной из первых предложив мгновенные контракты через интернет, которые помогали фирме получать десятки тысяч клиентов по всей стране. Грабитель банков и торговец наркотиками в прошлом, Крис имел двух видных бизнесменов Орандж Каунти в партнерах и двадцать одного сотрудника, которые работали в просторном офисе, недалеко от Пацифик Коаст Хайвэй. Клара периодически «светилась» в рекламных журналах и на сайте компании, чтобы помогать компании с продвижением. К 2000-м годам, пара отметилась на всех фронтах — купили роскошный дом в Ньюпорт Бич, родили сына и сделали ставку на улучшение бизнеса до огромных масштабов.

Этой весной мечта умерла. Пузырь интернет-компаний лопнул и поток новых компаний, который был основой Мишн Пацифик, стал иссякать. А после, крупные компании, как Американ Экспресс, вошли в сферу лизинга, вытесняя малые фирмы. Компания Криса была одной из десятков лизинговых контор, которым было суждено рухнуть и прогореть. Он начал сокращать сотрудников, но этого было мало, и ему пришлось признаться оставшимся, что компания больше не сможет оплатить их работу. Крис ушел работать в другую лизинговую компанию, где вскоре он был сокращен, когда прокатилась волна увольнений в связи с продажей фирмы крупному банку. Тем временем его жена родила второго мальчика. Поэтому, когда Джефф Норминтон появился, чтобы обсудить суперхакера, которого он встретил в Тафте, Крис был готов к встрече.

К тому моменту, когда он и Макс встретились в ресторане Норт Бич, Крис уже спонсировал схему Норминтона, обеспечивая того специфическим оборудованием, которое, как сказал Норминтон, было нужно хакеру. Теперь, когда Крис встретил Макса вживую, ему не терпелось увидеть его в работе. После нескольких часов разговоров, трое мужчин вышли из кафе, чтобы найти место, откуда можно было провести взлом. Они поднялись на двадцать седьмой этаж Холидэй Инн в Чайнатауне, в нескольких кварталах от отеля. Они спросили Макса выбрать какую-нибудь комнату повыше дороги. Макс нацелился на окно, включил ноутбук, подключил антенну и начал сканировать Wi-Fi сети.

В 2003 году мир только начинал большое путешествие в беспроводной мир, неся с собой большую дыру в защите. Революция началась с беспроводной точки доступа AirPort от Apple, позже к ней присоединились производители железа Linksys и Netgear. По мере падения цен на железо, все больше и больше компаний и обычных пользователей избавлялись от паутины голубых Ethernet кабелей. Тем не менее, создание и объединение компаний по всей стране в беспроводную сеть было мечтой хакеров. В большинстве случаев, эти сети использовали беспроводной стандарт 802.11b, включавший в себя схему шифрования, которая, теоретически, была защищена от взлома, прослушки и подключения к чужой сети. В 2011 году, исследователи из Университета Калифорнии в Беркли осветили ряд серьезных недостатков этой схемы, которые позволяли взломать ее доступным оборудованием и нужным софтом. С практической точки зрения, не нужно было прибегать к какой-либо технической черной магии. Чтобы ускорить переход на новое оборудование, производители точек доступа отключали шифрование по умолчанию. Компании просто использовали оборудование с завода, не ковырясь в настройках, наивно предполагая, что стены офиса спасут их сеть от взлома с улицы.

За несколько месяцев до того, как Макс попал в тюрьму, whitehat-хакер придумал вид спорта, названный вардрайвингом, чтобы показать масштабы дырявых сетей Сан Франциско. После установки антенны на крыше своего Сатурна, whitehat-хакер катался по улицам города, пока его ноутбук сканировал точки доступа Wi-Fi. После часа в финансовом районе города, его установка нашла около восьмидесяти сетей. С тех пор прошло полтора года, и Сан Франциско, как и любой другой большой город, погряз в невидимой сети интернет трафика, доступной любому, кто захотел нырнуть в нее.

Взлом из дома — для идиотов и подростков, — Макс узнал это на своей шкуре. Благодаря Wi-Fi, теперь он мог работать практически из любого места, оставаясь при этом анонимным. В этот раз, если полиция попадет на след Макса, то всё, что они получат — одного из бедных провайдеров, что использовал Макс для подключения.

Антенна, которую использовал Макс была огромной — параболическая сетка из проволоки шириной в два фута, которая мгновенно отслеживала десятки сетей из эфира, окружающего Холидэй Инн. Он выбрал одну из сетей и показал Крису, как это работает. Используя сканер уязвимостей, — такой же софт, что он использовал тестах на проникновение, — он мог сканировать большие фрагменты интернет адресов в поисках известных уязвимостей, как будто закинув сеть в море интернета. Дыры в безопасности были повсюду. Для Макса не было проблемой проникнуть в сети финансового института или торговой корпорации. Дело было в Норминтоне и Крисе, чтобы решить, какие данные им были нужны и как они хотели использовать их. Крис вымотался. Этот хакер шести с половиной футов ростом, полу-вегетарианец, знал свое дело, даже если был гнилой до костей.

Крис представил Макса одному из своих тюремных знакомых, мошеннику по недвижимости Вернеру Джанеру, которого Крис встретил в Терминал Айлэнд в 92ом году. Джанер предложил Максу 5000 долларов, если он взломает компьютер личного врага. Он выписал чек на Чарити, поэтому Максу не пришлось объяснять этот доход своему курирующему офицеру. Полученые деньги дали Максу небольшую передышку. Он начал летать в Орандж Каунти, делая ошибку в имени на билете, чтобы у офицеров не было данных о том, что Крис нарушил границы Бэй Эрия, дозволенные ему нахождением под надзором. Макс и Норминтон проводили взлом в течение недели, засев в гараже Криса.

Он скачал список небольших финансовых компаний с сайта Федеральной Корпорации Страхования Вкладов, предполагая, что они были наиболее уязвимы для атак, и запустил скрипт для сканирования каждого банка на наличие известных дыр в безопасности. Электронный звонок прозвенел на весь гараж, оповещая о завершении сканирования. Червь прошерстил все банки и вытащил имена клиентов, финансовые данные и проверочные номера чеков. Обобщенный подход означал, что Макса снова ждет разочарование, как это было при последнем легальном тесте на проникновение. Взлом одной единственной цели может быть сложен, в зависимости от цели, это может быть даже невозможно. Но сканирование сотен и тысяч систем дает гарантию, что можно найти что-то слабое. Это было игрой чисел, как пытаться открыть случайно оставленную открытой дверь машины на огромной стоянке.

Только Чарити имела полное представление о том, чем занимается Макс, и ей это не нравилось. Пытаясь добиться ее расположения, Крис и Норминтон пригласили молодую пару в Орандж Каунти на выходные, собираясь отправиться в Диснейлэнд. Чарити видела, что Макс и Крис ладят, но что-то не давало ей покоя. Он был слишком скользким и слащавым. Макс переключился на небольшие сайты интернет-торговли, где он собирал историю транзакций, в которых иногда попадались номера кредитных карт. Однако этот взлом не был направленным, ни Крис, ни Норминтон не знали точно, что они будут делать с добытыми данными. К счастью, у Криса были деньги. Вернер Джанер задолжал ему 50 000 долларов и был готов перевести деньги на любой, удобный Крису, счет. Ожидая получить на руки законные, холодные, осязаемые наличные, Крис спросил Норминтона, как бы поступил он, на что Норминтон поддержал мнение о том, что лучше сделать перевод на кого-то из друзей, а затем, в течение нескольких дней, уже обналичить его.

Первая часть перевода прошла как и ожидалось, и Норминтон, и его друг пришли с Крисом и получили 30 000 долларов в 100 долларовых купюрах. Следующий день, однако, не заладился, — Норминтон сообщил, что его друг заболел и ему нужно отлежаться денек.
По правде говоря, Норминтон узнал источник денег — это был кусок Криса в деле Джанера, где он помог в махинациях с недвижимостью. Это были грязные деньги, и теперь Норминтон был в этой схеме. На следующее утро Крис нашел Хонду, которую он одолжил Норминтону, припаркованную недалеко от своего офиса, — одно колесо было пробито, а на крыле виднелась свежая вмятина. В салоне была записка от Норминтона: «ФБР гонится за мной. Я сваливаю из города». Крис позвонил другу Норминтона, догадываясь, что ему скажут: «Самочувствие в порядке, а оставшиеся 20 000 долларов мы уже сняли. Я отдал их Норминтону. Разве ты не получил их?» Крис разыскал Макса через Чарити и обрушил на него шквал вопросов: что Максу известно о местонахождении Норминтона? Где его деньги? Макс был удивлен не меньше Криса исчезновением Норминтона. Обсудив детали, дальше они решили действовать без Норминтона.

Макс и Крис погрязли в рутине. Раз в месяц Крис прилетал или приезжал на север и встречался с Максом в Сан Франциско, где они заседали в отеле. Они приносили громадную антенну Макса по ступеням в их комнату и устанавливали на треноге, направляя в окно. Затем Макс проводил настройку, отыскивая точку доступа с сильным приемом и высокой скоростью. Они заметили, что при взломе Wi-Fi высота была не столько важна, сколько обзор из окна,. Если у них что-то не получалось, Крис всегда мог попросить другую комнату, объясняя это тем, что он не может поймать сигнал сотовой сети или боязнью высоты.

Для Макса это было работой, прощаясь с Чарити, он исчезал на неделю в одном из лучших отелей города, в Хилтоне, в Вестине, W, или Хайятте. Под лязг трамваев, которые ездили по улицам, Макс сканировал сети, выхватывая любую информацию, что ему попадалась, не представляя, что именно с ней делать. Ему пришло в голову взломать компьютер Кими и ее парня, с которым она уехала. Макс задумывал взломать ее адресную книгу, чтобы разослать письмо от ее имени, в котором он расскажет, как она его предала. Он думал, все должны знать, что новая жизнь Кими основана на предательстве.

Он не сделал этого. У него была Чарити. Кими переехала, так что ничего не изменить, если пытаться пристыдить ее. Вскоре после этого Крис подписал бумаги о разводе. Вернувшись к работе, он начал поиски в Гугле, чтобы определиться с дальнейшими действиями по взлому — «Что делают другие мошенники?», «Как можно использовать украденные данные?» Он был удивлен, когда нашел ответы на свои вопросы на двух сайтах: CarderPlanet и ShadowCrew.

Продолжение следует

Готовые переводы и план (состояние на 11 сентября)
PROLOGUE (Школьники лагеря GoTo)
1. The Key (Гриша, Саша, Катя, Алена, Соня)
2. Deadly Weapons (Юные программисты ФСБ РФ, 23 авг)
3. The Hungry Programmers (Юные программисты ФСБ РФ)
4. The White Hat (Саша К, ShiawasenaHoshi)
5. Cyberwar! ( ShiawasenaHoshi)
6. I Miss Crime (Валентин)
7. Max Vision (Валентин, 14 авг)
8. Welcome to America (Alexander Ivanov, 16 авг)
9. Opportunities (jellyprol)
10. Chris Aragon (jorj)
11. Script’s Twenty-Dollar Dumps (Жорж)
12. Free Amex! (Теплица социальных технологий)
13. Villa Siena (Lorian_Grace)
14. The Raid (Жорж)
15. UBuyWeRush (Ungswar)
16. Operation Firewall (Жорж)
17. Pizza and Plastic (готово)
18. The Briefing ()
19. Carders Market (Ungswar)
20. The Starlight Room (Ungswar)
21. Master Splyntr (Ungswar)
22. Enemies (Alexander Ivanov)
23. Anglerphish (Жорж)
24. Exposure (Mekan)
25. Hostile Takeover (Фанур)
26. What’s in Your Wallet? (al_undefined)
27. Web War One (Lorian_Grace)
28. Carder Court (drak0sha)
29. One Plat and Six Classics (Бильбо)
30. Maksik (workinspace)
31. The Trial (Forever 4apple)
32. The Mall (Shuflin)
33. Exit Strategy
34. DarkMarket (Валера ака Дима)
35. Sentencing
36. Aftermath
EPILOGUE

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

пятница, 11 сентября 2015 г.

[Из песочницы] Заимствование и время существования в Rust

Представляю вашему вниманию перевод статьи «Rust Borrow and Lifetimes» из блога Артура Ляо (Arthur Liao), инженера Yahoo!

Rust — это новый язык программирования, находящийся в активной разработке с версии 1.0. Я могу написать другой блог о Rust и том, почему он крут, но сегодня я сфокусируюсь на его системе заимствования и времени существования, которая запутывает многих новичков, в том числе и меня самого. Данный пост предполагает, что у вас есть базовое понимание Rust. Если же нет, вы можете сперва прочитать само Руководство и Руководство по указателям.

Владение ресурсами и заимствование


В Rust безопасность по памяти обеспечивается без сбора мусора путём использования усложнённой системы заимствования. Имеется как минимум один владелец (owner) для любого ресурса, который занимается освобождением своих ресурсов. Вы можете создать новые биндинги, чтобы обратиться к ресурсу, использующему & или &mut, которые называются заимствованием (borrow) и изменяемым заимствованием (mutable borrow). Компилятор следит за должным поведением всех владельцев (owners) и заёмщиков (borrowers).

Копирование и перемещение


Перед тем, как перейти к системе заимствования, нам нужно знать, как методы copy и move обрабатываются в Rust. Этот ответ из StackOverflow просто необходимо прочитать. В целом, в присваиваниях и в функции они вызываются:

1. Если значение копируемо ( с участием лишь примитивных типов, без участия ресурсов, например обработки памяти или файла), компилятор по умолчанию копирует.
2. В противном случае, компилятор перемещает (передает) владение (ownership) и делает недействительным оригинальный биндинг.

Вкратце, pod (читаемые старые данные) => copy, non-pod (линейные типы) => move.

Тут есть несколько дополнительных замечаний для справки:

* Метод Rust copy похож на Си. Каждое использование по значению является побайтовым копированием (теневой метод memcpy copy) вместо семантического копирования или клонирования.
* Чтобы сделать структуру pod некопируемой, вы можете использовать поле маркера NoCopy или реализовать типаж Drop.

После перемещения, владение передаётся следующему владельцу.

Освобождение ресурса


В Rust любой объект освобождается как только его владение исчезнет, например когда:

1. Владелец окажется вне области или
2. Владение биндингом изменяется (тем самым, оригинальный биндинг становится void)

Привилегии и ограничения владельца (owner) и заёмщика (borrower)


Этот раздел основывается на Руководстве по Rust с упоминанием методов copy и move в части привилегий.

Владелец имеет некоторые привилегии. И может:

1. Контролировать деаллокацию ресурса
2. Занимать ресурс неизменяемо (множественные заимствования) или изменяемо (эксклюзивно) и
3. Передавать владение (с перемещением).

Владелец также имеет некоторые ограничения:

1. В процессе заимствования, владелец не может (а) изменять ресурс или (б) занимать его в измененном виде.
2. В процессе изменяемого заимствования, владелец не может (а) иметь доступ к ресурсу или (б) занимать его.

Заёмщик тоже имеет некоторые привилегии. В дополнение к получению доступа или изменению заимствованного ресурса, заёмщик также может делиться другим заёмщиком:

1. Заёмщик может распределить (копировать) указатель неизменяемое заимствование (immutable borrow)
2. Изменяемый заёмщик может передавать (перемещать) изменяемое заимствование. (Заметьте, что изменяемая ссылка (mutable reference) перемещена).

Примеры кода


Довольно разговоров. Давайте взглянем на какой-нибудь код (Вы можете запустить код Rust по адресу play.rust-lang.org). Во всех нижеследующих примерах мы используем «struct Foo», структуру Foo, которая не является копируемой, поскольку содержит упакованное (динамически распределяемое) значение. Использование некопируемых ресурсов ограничивает возможности операций, что является хорошей идеей на этапе изучения.

К каждому образцу кода также даётся «диаграмма области» для иллюстрации областей владельца, заёмщиков и т. д. Фигурные скобки в строке заголовка совпадают с фигурными скобками в самом коде.

Владелец не может иметь доступ к ресурсу в процессе изменяемого заимствования

Нижеследующий код не скомпилируется, если мы не раскоментируем последнюю строку «println:»:
struct Foo {
    f: Box<int>,
}

fn main() {
    let mut a = Foo { f: box 0 };
    // изменяемое заимствование
    let x = &mut a;
    // ошибка: не могу заимствовать `a.f` как изменяемое, т. к. `a` уже заимствовано как изменямое
    // println!("{}", a.f);
}
           { a x * }
владелец a   |_____|
заёмщик  x     |___| x = &mut a
доступ a.f       |   ошибка


Это нарушает ограничение владельца #2 (а). Если мы поместим «let x = &mut a;» во вложенный блок, заимствование завершается перед строкой println! и это могло бы сработать:
fn main() {
    let mut a = Foo { f: box 0 };
    {
        // изменяемое заимствование 
        let x = &mut a;
        // здесь изменяемое заимствование завершается
    }
    println!("{}", a.f);
}
           { a { x } * }
владелец a   |_________|
заёмщик  x       |_|     x = &mut a
доступ a.f           |   OK


Заёмщик может перемещать изменяемое заимствование в новый заёмщик

Этот код иллюстрирует привилегии заёмщика #2: изменяемый заёмщик x может передавать (перемещать) изменяемое заимствование в новый заёемщик y.
fn main() {
    let mut a = Foo { f: box 0 };
    // изменяемое заимствование
    let x = &mut a;
    // переместить изменяемое заимствование в новый заёмщик y
    let y = x;
    // ошибка: использование перемещённого значения: `x.f`
    // println!("{}", x.f);
}
           { a x y * }
владелец a   |_______|
заёмщик  x     |_|     x = &mut a
заёмщик  y       |___| y = x
доступ x.f         |   ошибка


После перемещения, оригинальный заёмщик x больше не имеет доступа к заимствованному ресурсу.

Область заимствования


Всё становится интересным, если мы передадим сюда ссылки (& и &mut), и тут многие новички начинают путаться.

Время существования

Во всей истории заимствования важно знать, где заимствование заёмщика начинается и где оканчивается. В Руководстве по времени существования это называется временем существования:

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

& = заимствование


Пару слов о заимствовании. Во-первых, просто запомните, что & = заимствование, а &mut = изменяемое заимствование. Где бы вы не увидели символ & — это заимствование.

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

В-третьих, для каждого заимствования имеется владелец и одиночный заёмщик или множественные заёмщики.

Расширение области займа


Несколько слов об области займа. Во первых, область займа:
— это область где заимствование эффективно, и
— заёмщик может расширить область займа (см ниже).

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

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

Формула займа


Теперь, у нас есть формула займа:
область ресурсов >= область займа = объединение областей всех заёмщиков

Пример кода


Давайте взглянем на некоторые примеры расширения области займа. Структура struct Foo та же самая как и прежде:
fn main() {
    let mut a = Foo { f: Box::new(0) };
    let y: &Foo;
    if false {
        // займ
        let x = &a;
        // поделиться займом с заемщиком y, следовательно расширив займы
        y = x;
    }
    // ошибка: не могу присвоить в `a.f`, потому, что он заимствован
    // a.f = Box::new(1);
}

               { a { x y } * }
    ресурс  a   |___________|
    заёмщик x       |___|     x = &a
    заёмщик y         |_____| y = x
область займа       |=======|
изменение a.f             |   ошибка


Даже несмотря на то, что заём происходит внутри if блока, и заёмщик х выходит за рамки после if блока, он расширил сферу заимствования через присваивания y = x;, так что есть два заёмщика: х и у. В соответствии с формулой заёма, область заёма является объединением заёмщика х и заёмщика у, которое находится между первым заёмом let x = &a; и до конца основного блока. (Обратите внимание, что связывание let y: &Foo; не заёмщик)

Вы, возможно, заметили, что, блок if никогда не будет выполнен, так как условие всегда ложно, но компилятор всё ещё запрещает владельцу ресурса `a` доступ к ресурсу. Это потому, что все проверки займа происходят во время компиляции, во время выполнения ничего не сделаешь.

Заимствование нескольких ресурсов


До сих пор мы сосредоточивались только на заимствованиях из одного ресурса. Может ли заёмщик брать несколько ресурсов? Конечно! Например, функция может принимать две ссылки и возвращать одного из них в зависимости от определенных критериев, например, кто из ссылок больше другого:
fn max(x: &Foo, y: &Foo) -> &Foo


Функция max возвращает указатель &, следовательно, это заемщик. Возвратный результат может быть любым из входящих ссылок, поэтому он заимствует два ресурса.

Именованная область займа


При наличии нескольких указателей & в качестве входящих, мы должны указать их отношения с помощью времени жизней с именем, определенным в руководстве времени существования. Но сейчас давайте просто называть их именованной областью займа.

Приведенный выше код не будет принят компилятором без указания отношения между заёмщиками, то есть, те заёмщики, которые сгруппированы в их области займа. Вот эта реализация будет правильная:

fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {
    if x.f > y.f { x } else { y }
}
  
(Все ресурсы и заёмщики сгруппированы в области займа 'a'.)
                  max( {   } ) 
       ресурс *x <-------------->
       ресурс *y <-------------->
область займа 'a <==============>
       заёмщик x        |___|
       заёмщик y        |___|
возвращаемое значение     |___|   обратно к вызываемому


В этой функции у нас есть одна область займа 'a' и три заемщика: два входных параметра и возвращаемый функцией результат. Вышеупомянутая формула заимствования всё ещё ​применяется, но теперь каждый заимствованый ресурс должен удовлетворять формуле. Смотрите пример ниже.

Пример кода


Давайте использовать функцию max в следующем коде, чтобы выбрать самое больше из a и b:
fn main() {
    let a = Foo { f: Box::new(1) };
    let y: &Foo;
    if false {
        let b = Foo { f: Box::new(0) };
        let x = max(&a, &b);
        // ошибка: `b` не живет достаточно долго
        // y = x;
    }
}

               { a { b x (  ) y } }
     ресурс a   |________________| успех
     ресурс b       |__________|   провал
область займа         |==========|
временный заёмщик        |_|       &a
временный заёмщик        |_|       &b
    заёмщик x         |________|   x = max(&a, &b)
    заёмщик y                |___| y = x


До let x = max(&a, &b); всё хорошо, потому, что &a и &b — это временные ссылки, которые действительны только в выражении, а третий заёмщик х заимствует два ресурса (либо a либо b но с проверкой заёмщика, если заимствованы оба) до конца блока if, таким образом, область займа находится в let x = max(&a, &b); до конца блока if. Ресурсы a и b действительны по всей области займа, следовательно, удовлетворяют формуле займа.

Теперь, если мы раскомментируем последнее значение y = x;, y станет четвертым заёмщиком, а область займа увеличится до конца основного блока, в результате чего ресурс b провалить тест формулы займа.

Структура как заёмщик


В дополнение к функциям и замыканиям, структура может также занимать несколько ресурсов, сохраняя несколько ссылок в своей области(ях). Посотрим на пример ниже, и как применяется формула займа. Давайте использовать структуру Link для хранения ссылки(неизменяемый займ(immutable borrow)):
struct Link<'a> {
    link: &'a Foo,
}


Структура заимствует несколько ресурсов


Даже только с одним полем, структура Link может занять несколько ресурсов:
fn main() {
    let a = Foo { f: Box::new(0) };
    let mut x = Link { link: &a };
    if false {
        let b = Foo { f: Box::new(1) };
        // ошибка: `b` не живет достаточно долго
        // x.link = &b;
    }
}

             { a x { b * } }
    ресурс a   |___________| успех
    ресурс b         |___|   провал
область займа    |=========|
   заёмщик x     |_________| x.link = &a
   заёмщик x           |___| x.link = &b


В приведенном выше примере, заёмщик х заимствовует ресурсы от владельца a, и область займа идет до конца основного блока. Всё идёт нормально. Если мы раскомментируем последнюю строку x.link = &b;, x также попытается заимствовать ресурс у владельца b, и тогда ресурс b провалит тест на формулу займа.

Функция для расширения области займа без возвращаемого значения


Функция без возвращаемого значения может также расширить область займа через его входные параметры. Например, функция store_foo принимает изменяемую ссылку на Link, и сохраняет в нее ссылку Foo(immutable borrow):
fn store_foo<'a>(x: &mut Link<'a>, y: &'a Foo) {
    x.link = y;
}


В следующем коде, заимствованные ресурсы овладели ресурсами; Структура Link изменяемо ссылается на заемщик х (т.е. *х является заемщиком); Область займа идет до конца основного блока.
fn main() {
    let a = Foo { f: Box::new(0) };
    let x = &mut Link { link: &a };
    if false {
        let b = Foo { f: Box::new(1) };
        // store_foo(x, &b);
    }
}

  
               { a x { b * } }
      ресурс a   |___________| успех
      ресурс b         |___|   провал
область займа      |=========|
   заёмщик *x     |_________| x.link = &a
   заёмщик *x           |___| x.link = &b


Если мы раскомментирем последнюю строку store_foo(x, &b); функция попытается хранить &b в x.link, делая ресурс b другим заимствованным ресурсом и провалит тест формулы займа, так как область ресурса b не покрывает всю область займа.

Несколько областей займа


Функция может иметь несколько именованных областей займа. Например:
fn superstore_foo<'a, 'b>(x: &mut Link<'a>, y: &'a Foo,
                          x2: &mut Link<'b>, y2: &'b Foo) {
    x.link = y;
    x2.link = y2;
}


В этой (вероятно, не очень полезной) функции участвуют две разрозненные области займа. Каждая область займа будет иметь свою собственную формулу заимствования.

Почему время существования сбивает с толку


Наконец, я хочу объяснить, почему я думаю, что термин время существования, используемый системой займа Rust’а вносит путаницу (и, таким образом, избежать использования термина в этом блоге).

Когда мы говорим о займе, есть три вида «времени существования»:

А: время существования владельца ресурса (или владеющий/заимствованный ресурс)
В: «время существования» всего займа, т.е. с первого займа и до возвращения
С: время существования отдельного заёмщика или заимствованного указателя

Когда кто-то говорит о термине «время существования», он может иметь ввиду любое из выше перечисленных. Если участвуют ещё и несколько ресурсов и заёмщиков, то всё становится еще более запутанным. Например, что делает «время жизни с именем» в объявлении функции или структуры? Означает ли это А, В или С?

В нашем предыдущей функции max:

fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {
    if x.f > y.f { x } else { y }
}


Что означает время существования 'a'? Это не должно быть А, поскольку два ресурса задействованы и имеют разные времена существования. Это не может быть и С, потому, что есть три заёмщика: х, y и возвращаемое значение функции и все они имеют разные времена жизни. Означает ли это B? Вероятно. Но вся область заёма не является конкретным объектом, как оно может иметь «время существования»? Назвать это временем существования — значит заблуждаться.

Кто-то может сказать, что это означает минимальные требования времени существования для заимствованных ресурсов. В некоторых случаях, это может иметь значение, однако как мы можем называть их «временем существования»?

Понятие собственности/заимствования само по себе сложное. Я бы сказал, что путаница в том, что даёт «время жизни», делает освоение еще более непонятным.

P.S. С использованием A, B и C, определенный выше, формула займа становится:

    A >= B = C1 U C2 U … U Cn


Изучение Rust стоит вашего времени!


Несмотря на то, что изучение займа и овладения занимает много времени, это интересно изучать. Rust попытается добиться безопасности без сбора мусора, и он до сих пор делает это очень хорошо. Некоторые люди говорят, что освоение Haskell изменяет стиль вашего программирования. Я думаю, что освоение Rust тоже стоит вашего времени.

Надеюсь, что этот пост вам поможет.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.