...

суббота, 15 августа 2020 г.

[Из песочницы] Почему налоговая не верит в айтишников-индивидуальных предпринимателей?

Сколько может зарабатывать айтишник на ИП? У ФНС свое мнение


Когда один человек много зарабатывает и честно платит налоги, налоговики смотрят со стороны и думают: «Как такое возможно? Наверняка у него масса помощников! Пусть и за них налоги заплатит!»

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

Может ли IT-специалист зарабатывать 400к в месяц?


Почему нет? Медианная ЗП в ИТ-отрасли сейчас 108 тыс, а у «сеньора» мобильного – разработчика или архитектора – даже официальная ЗП может приближаться к этой цифре. К тому же, почти всегда есть какая-то подработка. Многие вообще уходят во фриланс и работают только на заказы: регистрируют ИП, выписывают инвойсы – самостоятельная единица в мире западного бизнеса. Там потолок зарплаты ограничен только твоей смелостью и уровнем заказчиков. Из Красногорска ты или из Междуреченска, становится уже не важно.

И вот ты ИП с заказчиками из Европы и США, ведешь параллельно несколько проектов: один в активной разработке, один на этапе согласования ТЗ и парочка в пассивной поддержке… Зарабатываешь пресловутые 400к в месяц. Все под контролем.

Но налоговая думает иначе...


Приходит внезапное требование от ФНС: «Просим предоставить пояснения по тому, каким образом ИП получен такой доход без наличия сотрудников? Либо СРОЧНО представить расчет по страховым взносам за 2019 год, с указанием физических лиц, получивших доход от ИП и сумм полученного дохода! Явитесь в ФНС с… по…»

А что ждёт в налоговой? Множество серьезных людей в форме, которые будут утверждать, что не может один человек столько заработать. И не будут они смотреть ни инвойсы, ни переписки в чатах, ни подписанные сканы в дропбоксе, им даже договор в «Диадоке» не указ! Это чисто психологическое мероприятие, на которое лучше вообще не ходить.

Что же тогда делать?


Есть простое решение: написать мотивированный ответ на такое требование и отправить его по ТКС. Если есть электронная подпись и программа для отправки – дело пары кликов.

Как отвечать?


image

Как выглядит требование?


Это может быть требование или уведомление о вызове. Выглядит вот так:

image

image

Какие суммы дохода ИП-одиночки приемлемы для ФНС?


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

Баг или фича?


Получив подобное требование, бессмысленно рассуждать о правомерности запроса. Если он выражен в виде требования, то ответить все равно придется. Иначе дело может и до блокировки расчетного счета дойти.

Чего еще ждать от ФНС?


Пока ясно одно: автотребований будет все больше. ФНС автоматизирует ручной труд и придумывает все новые и новые проверки, призванные увеличить собираемость налогов. Бэклог разработки у ФНС огромен: служба делает большие успехи в цифровизации, а когда их ПО наконец станет работать стабильно хорошо, работы у бухгалтеров прибавится. Может быть, именно это требование они шлют по просьбе своих HR-ов, налоговой «сеньоры» в разработке тоже нужны, не все же за счет «джунов» выезжать.

Let's block ads! (Why?)

Попытка определить язык манускрипта Войнича, Random Forest Classifier

Пытаемся определить язык таинственной рукописи — манускрипта Войнича — простыми методами обработки естественных языков на Python.

1 Что это — манускрипт Войнича?


Манускрипт Войнича — таинственная рукопись (кодекс, манускрипт или просто книга) в добрых 240 страниц пришедшая к нам, предположительно, из XV века. Рукопись была случайно приобретена у антиквара мужем знаменитой писательницы-карбонария Этель Войнич — Уилфредом Войничем — в 1912 году и скоро стала достоянием широкой общественности.

Язык рукописи не определен до сих пор. Ряд исследователей манускрипта предполагают, что текст рукописи — шифровка. Иные уверены, что манускрипт написан на языке, не сохранившемся в известных нам сегодня текстах. Третьи и вовсе считают манускрипт Войнича бессмыслицей (см современный гимн абсурдизму Codex Seraphinianus).

В качестве примера приведу сканированный фрагмент сабжа с текстом и нимфами:

2 Чем так интересен диковинный манускрипт?


Может быть, это — поздняя подделка? По-видимому, нет. В отличие от Туринской плащаницы, ни радиоуглеродный анализ, ни прочие попытки оспорить древность пергамента пока не дали однозначного ответа. А ведь Войнич не мог предвидеть изотопный анализ в самом начале XX века…

Но если рукопись — бессмысленный набор букв пера шаловливого монаха, дворянина в измененном сознании? Нет, однозначно нет. Бездумно шлепая по клавишам, я, например, изображу всем привычный модулированный QWERTY-клавиатурой белый шум наподобие “asfds dsf”. Графологическая экспертиза показывает: автор писал твердой рукой набитые “в подкорку” символы хорошо известного ему алфавита. Плюс корреляции распределения букв и слов в тексте рукописи соответствуют “живому” тексту. К примеру, в рукописи, разделенной условно на 6 разделов, есть слова — “эндемики”, часто встречающиеся в каком-нибудь одном из разделов, но отсутствующие в прочих.

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

Несмотря на колоссальные успехи в переводе древних письменностей, в том числе, с применением современных вычислительных ресурсов, рукопись Войнича до сих пор не поддается ни профессиональным языковедам со стажем, ни молодым амбициозным data-scientist-ам.

3 Но что если язык манускрипта нам известен


… но написание отличается? Кто, например, распознает в этом тексте латынь?

А вот другой пример — транслитерация английского текста на греческий:

in one of the many little suburbs which cling to the outskirts of london
ιν ονε οφ θε μανυ λιττλε συμπυρμπσ whιχ cλιγγ το θε ουτσκιρτσ οφ λονδον

пайтоновской библиотекой Transliterate. NB: это уже не шифр подстановки — некоторые многобуквенные сочетания передаются одной буквой и наоборот.

Я попробую опознать (классифицировать) язык рукописи или же найти наиболее близкого родственника ему из известных языков, выделив характерные черты (features) и обучив на них модель:

На первом этапе — featurization — мы превращаем тексты в feature-вектора: фиксированного размера массивы вещественных чисел, где каждая размерность вектора отвечает за свою особую черту (feature) исходного текста. Например, условимся в 15-м измерении вектора сохранять частоту употребления в тексте самого распространенного слова, 16-м измерением — второго по популярности слова … в N-м измерении — наибольшую длину последовательности из одного и того же повторяющегося слова и т.д.

На втором шаге — обучение — мы подбираем коэффициенты классификатора исходя из априорного знания языка каждого из текстов.

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

4 На картинке всё так просто — в чем подвох?


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

Но алфавит манускрипта “не имеет аналогов ”. Более того, мы не можем полностью положиться на закономерности в распределении букв. Теоретически возможен и вариант передачи фонетики одного языка правилами другого (язык эльфийский — а руны мордорские).

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

Значит, поднимемся на уровень выше относительно букв и будем опираться на слова. Составим на основе текста рукописи словарь и проследим закономерности уже на уровне слов.

5 Исходный текст манускрипта


Разумеется, кодировать замысловатые символы манускрипта Войнича в их Unicode-эквиваленты и обратно самостоятельно вовсе не требуется — эту работу уже проделали за нас, например, здесь. С опциями по-умолчанию я получу следующий эквивалент первой строки манускрипта:
fachys.ykal.ar.ataiin.shol.shory.cth!res.y.kor.sholdy!-

Точки и восклицательные знаки (а также ряд других символов алфавита EVA) — всего лишь разделители, которые для наших целей вполне можно заменить пробелами. Знаки вопроса и звездочки — нераспознанные слова / буквы.

Для проверки подставим текст сюда и получим фрагмент рукописи:

6 Программа — классификатор текстов (Python)


Вот ссылка на репозитарий кода с необходимым минимумом подсказок в README, чтобы протестировать код в работе.

Я собрал по 20+ текстов на латыни, русском, английском, польском, и греческом языках стараясь выдерживать объем каждого текста в ± 35 000 слов (объем рукописи Войнича). Тексты старался подбирать близких датировок, в одном написании — например, в русскоязычных текстах избегал буквы Ѣ, а варианты написания греческих букв с различными диакритическими знаками приводил к единому знаменателю. Также убрал из текстов цифры, спец. символы, лишние пробелы, привел буквы к одному регистру.

Следующий шаг — построить “словарь”, содержащий такую информацию как:

  • частота употребления каждого слова в тексте (текстах),
  • “корень” слова — а точнее, неизменяемая, общая часть для множества слов,
  • распространенные “приставки” и “окончания” — а точнее, начала и окончания слов, вместе с “корнем” составляющие собственно слова,
  • распространенные последовательности из 2-х и 3-х одинаковых слов и частота их появления.

“Корень” слова я забрал в кавычки — простой алгоритм (да и я сам иногда) не в состоянии определить, к примеру, какой корень у слова подставка? Подставка? Подставка?

Вообще говоря, этот словарь — наполовину подготовленные данные для построения feature-вектора. Почему я выделил этот этап — составление и кэширование словарей по отдельным текстам и по совокупности текстов для каждого из языков? Дело в том, что такой словарь строится довольно долго, порядка полуминуты на каждый текстовый файл. А текстовых файлов у меня набралось уже более 120.

7 Featurization


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

А еще класс-наследник должен давать каждой отдельной feature (i-координате feature-вектора) имя. Это пригодится, если мы решим визуализировать машинную логику классификации. Например, 0-е измерение вектора будет помечено как CRw1 — автокорреляция частоты употребления слов, взятых из текста на соседней позиции (с лагом 1).

От класса BaseFeaturizer я унаследовал класс WordMorphFeaturizer, логика которого базируется на частоте употребления слов во всем тексте и в рамках скользящего окна из 12 слов.

Важный аспект — код конкретного наследника BaseFeaturizer помимо собственно текстов нуждается еще в подготовленных на их основе словарях (класс CorpusFeatures), которые уже скорее всего закэшированы на диске на момент старта обучения и тестирования модели.

8 Классификация


Следующий абстрактный класс — BaseClassifier. Этот объект может обучаться, а затем классифицировать тексты по их feature-векторам.

Для реализации (класс RandomForestLangClassifier) я выбрал алгоритм Random Forest Classifier из библиотеки sklearn. Почему именно этот классификатор?

  • Random Forest Classifier мне подошел со своими параметрами по-умолчанию,
  • он не требует нормализации входных данных,
  • предлагает простую и наглядную визуализацию алгоритма принятия решения.

Так как, на мой взгляд, Random Forest Classifier вполне справился со своей задачей, других реализаций я уже не писал.

9 Обучение и тестирование


80% файлов — большие фрагменты из опусов Байрона, Аксакова, Апулея, Павсания и прочих авторов, чьи тексты я смог найти в формате txt — были отобраны случайным образом для тренировки классификатора. Оставшиеся 20% (28 файлов) определены для вневыборочного тестирования.

Пока я тестировал классификатор на ~30 английских и 20 русских текстах, классификатор давал большой процент ошибок: почти в половине случаев язык текста определялся неверно. Но когда я завел ~120 текстовых файлов на 5 языках (русский, английский, латынь, староэллинский и польский) классификатор перестал ошибаться и начал распознавать корректно язык 27 — 28 файлов из 28 тестовых примеров.

Затем я несколько усложнил задачу: ирландский роман XIX века “Rachel Gray” записал транслитом на греческий и подал на вход обученному классификатору. Язык текста в транслите снова был определен корректно.

10 Алгоритм классификации наглядно


Вот так выглядит одно из 100 деревьев в составе обученного Random Forest Classifier (чтобы изображение было более читаемым, я отрезал 3 узла правого поддерева):

На примере корневого узла поясню значение каждой подписи:

  • DGram3 <= 0.28 — критерий классификации. В данном случае DGram3 — конкретное именованное классом WordMorphFeaturizer измерение feature-вектора, а именно, частота третьего по распространенности слова в скользящем окне из 12 слов,
  • gini = 0.76 — коэффициент, известный как Gini impurity, объясняется, например, в этой статье. Не вдаваясь в подробности, можно сказать, что этот коэффициент характеризует степень неопределенности относительно принадлежности входных данных какому-либо конкретному классу. Продвигаясь от корня в сторону листьев мы наблюдаем уменьшение коэффициента. Наконец, для листа gini, закономерно, равен 0 (жребий брошен),
  • samples = 92 — количество текстов, на которых построено поддерево,
  • value = [46, 17, 45, 12, 29] — количество текстов в поддереве, попавших в ту или иную категорию (46 английских, 17 греческих, 45 латинских и т.д.),
  • class = en (английский текст) — определяется по наиболее заполненному поддереву.

Если критерий (DGram3

Окончательное же решение принимает ансамбль из 100 подобных деревьев, построенных в ходе обучения классификатора.

11 И как же определила программа язык манускрипта?


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

древнегреческого и русского языков:

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

Очевидного соответствия вроде

с распространением правил замены букв на остальные часто употребляемые слова мне найти не удалось. Можно лишь делать предположения — например, самое часто употребляемое слово — это союз “и” — как и во всех остальных рассмотренных языках за исключением английского, в котором союз “and” был задвинут на второе место определенным артиклем “the”.

Что дальше?


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

Более творческая задача — попытаться определить часть речи для каждого слова. Для ряда языков (разумеется, прежде всего — английского) с этой задачей хорошо справляются PoS (Part of Speech) токенизаторы в составе доступных для скачивания пакетов. Но как определить роли слов неизвестного языка?

Схожие задачи решал советский лингвист Б.В. Сухотин — например, он описал алгоритмы:

  • разделения символов неизвестного алфавита на гласные и согласные — к сожалению, не 100% надежного, в особенности для языков с нетривиальной передачей фонетики, вроде французского,
  • выделение морфем в тексте без пробелов.

Для PoS-токенизации мы можем отталкиваться от частоты употребления слов, вхождения в сочетания из 2 / 3 слов, распределения слов по разделам текста: союзы и частицы должны быть распределены более равномерно, чем существительные.

Литература


Не буду оставлять здесь ссылки на книги и руководства по NLP — этого достаточно в сети. Вместо этого перечислю художественные произведения, которые стали для меня большой находкой еще в детстве, где героям пришлось потрудиться над разгадкой зашифрованных текстов:
  1. Э. А. По: “Золотой жук” — нестареющая классика,
  2. В. Бабенко: “Встреча” — лихо закрученная, в чем-то провидческая детективная повесть конца 80-х,
  3. К. Кирицэ: “Рыцари с Черешневой улицы, или Замок девушки в белом” — увлекательный подростковый роман, написанный без скидки на возраст читателя.

Let's block ads! (Why?)

Опыт конвертирования кода C# в код Rust

Постановка задачи

Код на языке C# нужно перевести в код на Rust. Точнее, требуется такая процедура перевода (разработка продолжается на C#), чтобы в любой момент можно было получить работающий код на Rust. Эту задачу я решал для языков Java, Python, JavaScript и PHP, написав конвертер из C# в эти языки. Концепция такого конвертирования была изложена в статье UniSharping пару лет назад. Я разрабатывал этот конвертер, чтобы переводить код своего проекта SDK Pullenti (лингвистический анализ текста). И подумалось мне: а не замахнуться ли на Rust? Да, слышал разные отзывы, что язык необычный и пр., но попытка же не пытка… Тем более, что у одного из заказчиков группа программистов увлечённо пишет на нём.

Сразу скажу, что в полном объёме, как для других языков, этого сделать не получилось — не хватило сил. Может, ещё вернусь к этой задаче. Полтора месяца было потрачено на борьбу с собой и языком, удалось довести конвертер до состояния, что морфологический блок начал переводиться и даже компилироваться (значит, и работать) на Rust-е. Разумеется, за это время модуль морфологии можно было написать с нуля, но за ним маячили ещё около 500 классов C#, создаваемые и отлаживаемые почти 10 лет, а их переписать не так просто. В этой статье хочу поделиться своими впечатлениями от языка Rust, а также описать приёмы, которые я использовал для конвертирования.


Впечатление от языка Rust

Говорят, что мастер не ищет лёгкий путей. Это в полной мере относится к Rust, так как многое простое и привычное на других языках становится сложным, при этом сложное не становится простым. Вы как бы попадаете в другой мир с абсурдной на первый взгляд логикой, которая становится далеко не сразу понятна после освоения базовых концепций. Неважно, на чём вы писали до сих пор: Си++, Java, Python и пр., но когда оказывается, что после добавления в список объект нельзя использовать: it = new ...(); list.add(it); it.val = ..., а вот так можно: it = new ...(); it.val = ...; list.add(it);, то это обескураживает. Или чтобы реализовать перекрёстные ссылки между объектами класса Foo, нужно использовать конструкцию Option<Rc<RefCell<Foo>>>, а для доступа к полю val этого класса вызывать foo.unwrap().borrow().val.
Выскажу своё личное мнение: мне представляется, что прогресс в области программирования идёт в направлении оптимизации труда программиста, чтобы меньшими усилиями достигать большего эффекта. Случай Rust уникален тем, что не вписывается в эту тенденцию. Удивительным для меня фактом является рост его популярности (сейчас Rust вошёл в 20-ку). Но почему? Вот вопрос.
По производительности на моей задаче Rust не произвёл впечатления — выигрыш по сравнению с C# получился всего в 2 раза. Подчеркну, что это на моей частной задаче морфологического анализа, которую удалось перевести в эквивалентный код (наверняка человек написал бы оптимальнее). В разных статьях сравнения производительности приводятся разные данные, и в целом создаётся впечатление, что Rust и C/C++ близки по скорости. Но существенно разнятся по сложности написания кода. Утверждается, что Rust сильно уменьшает вероятность утечек памяти по сравнению с С/C++, доступа за границу массива и прочее, но какой ценой…
Единственным разумным для меня объяснением роста популярности Rust является то, что Си "поднадоел" за почти 50 лет, и молодое поколение программистов желает чего-то нового, не обязательно лучшего. Как молодёжь 80-х добровольно ехала из обжитых городов строить БАМ (такая железная дорога на Дальнем Востоке), стойко перенося трудности и лишения, застревая потом в таёжных городках и посёлках. Подобно и здесь. Си-шника я ещё могу понять, так как он получает trait-ы (типа interface в Java и C#), на которых можно худо-бедно реализовывать ООП, и ещё некоторые полезные штучки. Но вот что здесь искать программистам других языков, кроме романтики новой экосистемы, в создании которой можно поучаствовать? Мне показалось, что при переходе на Rust большее теряешь, чем находишь.


Работа с кучей

Базовое отличие Rust от других языков — в парадигме работы с кучей (heap). В древних языках всё просто — выделение и освобождение памяти в куче происходит явно операторами типа new/delete. Это С\С++, Паскаль, Фортран и др. Возникает утечка памяти, если delete не вызвать. Потом решили упростить программистам жизнь, избавив от необходимости явно освобождать память. Этот процесс перенесли на уровень так называемых сборщиков мусора, которые сами заботятся об освобождении в нужный момент, программист же только выделяет через new. Это имеет место во всех известных мне современных языках: Java, C#, Python, JavaScritp, PHP и пр.
В Rust освобождение памяти происходит тоже автоматически, но сразу при окончании жизни объекта, например, когда он выходит из области видимости. Скажем, если внутри блока создать объект { ... let x = Foo {...это конструктор}; ... }, то память автоматически освободится при выходе управления из этого блока. И вот весь геморрой — из-за этой парадигмы. Приходится вводить понятие владельца объекта, ссылки на владельца, изменяемой (mut) ссылки на владельца, время жизни и другие понятия, порождающие ограничения языка. Скажем, изменяемая ссылка может быть только одна, и вот аналог C# чтения в буфер buf из потока stream.Read(buf, 0, buf.Length) не будет компилироваться, поскольку в первом аргументе buf перезаписывается и должен быть mut-ссылкой, поэтому в третьем аргументе уже никак этот buf использовать нельзя. А вот так можно: int len = buf.Length; stream.Read(buf, 0, len);.
Далее опишу решения, которые я использовал в конвертере для перевода кода C# в Rust. Напомню, что именно это и было исходной задачей — конвертирование существующего кода.


Ограничения кода C#

Несколько лет назад я озаботился тем, чтобы перевести свой SDK на C# в язык Java. Опробованные конвертеры не подошли, так как выдавали на выходе грязный код, который ещё править и править. Да, они заточены на задачу разовой миграции, а мне хотелось иного — продолжать разработку на C# и получать на выходе сразу исполняемый код, работающий эквивалентно. Пришлось писать самому. Этому была посвящена статья UniSharping. Если кратко, то в общем случае невозможно решить эту задачу. Однако, если придерживаться некоторых ограничений при написании кода C#, то невозможное становится возможным. Например, в Java отсутствует аналог оператора yield, так давайте избавимся от него в исходном коде C# — невелика потеря!
С каждым новым поддержанным языком исходный код на C# слегка корректировался. Для Java пришлось отказаться от плагинной техники динамической загрузки DLL, поскольку в Java и других языках понятие сборки просто отсутствует. Для Python пришлось убрать одинаковые названия методов в классе, поскольку сигнатура Питона включает только имя, а типы аргументов у него не указываются. У JavaScript обнаружилось отсутствие типа long (есть byte, short, int, float, double, а вот на long-е разработчики сэкономили), пришлось мне в коде SDK C# заменить все long на int, благо их оказалось немного. В PHP ждала засада в виде представления string как последовательность байт кодировки utf-8 с невозможностью быстрого доступа к i-му символу строки без перебора. Тут я уже ничего у себя переделывать не стал, а использовал их штатные функции mb_, из-за чего производительность получилась чудовищно низкой. В Rust со строками такая же ситуация, но тут я поступил по-другому.
Также плодотворной оказалась идея использования директив препроцессора с коде C#, когда нужно в зависимости от языка слегка что-нибудь подправить: #if JAVA || PYTHON… #else… #endif — конвертер понимает такие конструкции.
Огромная сложность автоматического перевода — это библиотечные классы и методы. Хорошо, когда есть полный аналог в другом языке, а если нет? Тогда приходится реализовывать на конечном языке эту функциональность и добавлять её в файлы генерируемого кода. Для Rust это пришлось делать более, чем для других языков.
Итак, приступим.


Стандартные конструкции

Выражения C#, операторы ветвления, циклы, функции — для всего этого Rust имеет эквиваленты, тут всё просто. Только циклы for(...; ...; ...) в большинстве случаев приходится разворачивать в while — к этому я был хорошо подготовлен Питоном. С простыми типами byte, int, float и пр. тоже просто, на то они и простые. А вот с непростыми сложнее.
Для непростого типа T из C# в Rust следует использовать три разных типа: T (для владельца объекта), &T (неизменяемая ссылка) и &mut T (изменяемая ссылка). Можно считать, что в C# произошло как бы слияние в один тип трёх разных типов — с C# он один, а в Rust это три типа, причём в нужных местах этот тип должен быть одним из этих трёх.

var obj = new T(); // создали объект класса T
FuncNotModif(obj); // передали аргументом в функцию
FuncModif(obj); // здесь объект модифицируется
list.Add(obj); // добавили в список List<T>
var obj2 = obj; // другая ссылка на тот же объект
var obj3 = obj; // другая ссылка на тот же объект

Вот как этот фрагмент можно представить в Rust:

let obj = T { }; // создали объект класса T (о классах чуть ниже)
func_not_modif(&obj); // передаём неизменяемую ссылку, иначе дальше obj нельзя будет использовать
func_modif(&mut obj); // а здесь модифицирующая ссылка
list.push(&obj); // можно добавлять только в вектор ссылок Vec<&T>, иначе потом obj недоступен
let obj2 : &T = &obj; // другая ссылка на объект
let obj3 : T = obj; // а здесь владение переходит к obj3, и obj с obj2 больше нельзя использовать

Итак, принципиальным моментом в Rust является владение экземпляром, которое переходит при присваивании значения: в операторе =, return и передаче аргументом без указания &. После этого предыдущая переменная и все ссылки становятся недействительными, эстафета как бы передаётся другой переменной.
А как в коде C# понять в каждом случае, какую из этих трёх разновидностей использовать: T, &T или &mut T? Хороший вопрос, и я его решил так:

По умолчанию аргументы методов являются &T или &mut T в зависимости от того, модифицируются ли они или нет в самом методе (это автоматом определяет конвертер), у методов, реализующих property { get; set; } возвращаемые значения &T, всё остальное — T. В коде C# сразу за типом можно указать через комментарий /*&*/ или /*&mut*/ нужный вариант. Например, для списка ссылок подсказка конвертеру будет List<T/*&*/>, а если и сам список является ссылкой, то List<T/*&*/>/*&*/.
Здесь некоторые из дочитавших до этого места разочарованно произнесут: мы то думали, что конвертер сам понимает, а ему приходится указывать, ещё и коверкать исходный код нелепыми вставками. Согласен, после разоблачения фокус воспринимается уже не так. Но это — решение, и лучше мне не удалось найти. К тому же оказалось, что в моём случае морфологического блока таких вставок получилось не так уж много.


Строки

Строки в Rust представляются последовательностью байт кодировки utf-8 (как и в PHP). Думаю, здесь 2 причины. В C#, Java и др. строки есть последовательность символов char размером 16 бит (в Си 8 бит, в Си++ 8 и 16), что есть ограничение с точки зрения разработчиков Rust. Сейчас Unicode уже 32-битный, а вдруг в будущем он вообще станет 64-битным? А они будут к этому готовы. Другая причина субъективная — основатель англоязычный, и его слабо волнуют проблемы за пределами 7-битной ASCII.
А в моей переводимой библиотеке идёт интенсивная работа со строками и доступом к её элементам str[i]. Как быть?
Решение — реализовать класс (struct в терминологии Rust), содержащий как вектор символов, так и сам string.

#[derive(Clone)]
pub struct NString {
    pub chars : Vec<char>,
    pub string : String,
    _is_null : bool
}
impl NString {
    pub fn from_string(s : &String) -> NString {
        NString { chars : s.chars().collect(), string : s.clone(), _is_null : false }
    }
    pub fn from_str(s : &str) -> NString {
        NString { chars : s.chars().collect(), string : s.to_string(), _is_null : false }
    }
    pub fn from_chars(s : &Vec<char>) -> NString {
        NString { chars : s.clone(), string : s.into_iter().collect(), _is_null : false }
    }
...
}

Когда нужно работать как массивом элементов, то используется поле chars, иначе — штатный String. Для этой структуры реализованы различные стандартные методы C# работы со строками, если аналогов не находилось в Rust. Например, вот метод Substring(int start, int len) для получения подстроки:

    pub fn substring(&self, pos : i32, len : i32) -> NString {
        let length : i32 = if len <= 0 { self.chars.len() as i32 - pos } else { len };
        let sub = self.chars[pos as usize .. (pos + length) as usize].to_vec();
        NString::from_chars(&sub)
    }

А строки-лексемы конвертер представляет так, ссылаясь в коде &STR_HELLO для ссылки или STR_HELLO.clone() для владения:

static STR_HELLO : Lazy<NString> = Lazy::new(|| { NString::from_str("Hello!") }); 
use once_cell::sync::Lazy;

Коллекции

Разумеется, в Rust есть множество типов для работы с коллекциями, но они по ряду причин не подошли. Если сразу писать на Расте, то может и ничего, но транслировать из кода C# оказалось затруднительно, поэтому пришлось как и для строк написать обёртки над Vec и HashMap и использовать их. Причём получилось 3 обёртки для каждого типа в зависимости от типа элементов: для простых типов, для ссылок &T и для владений T. Массивы array[] я транслировал в Rust так же, как и List.

Object

В Rust нет привычных всем null и базового класса object, практически отсутствует и приведение типов. То, что в C# любой тип можно "упаковать" в object и затем "распаковать" его — для Rust это за пределами возможного. Я не придумал лучшего решения, чем следующее.
Если в существующем коде используется object, то как правило в реальности в конкретном месте фигурирует ограниченный набор типов значений для этого object. Поэтому можно создать служебный класс, содержащий в отдельных полях значения этих типов, и использовать его, указав конвертеру в виде подсказки /*=имя*/ после object.

            object/*=ObjValue*/ obj = "Hello";
            Console.WriteLine(obj);
            obj = 10;
            if (obj is int)
            {
                int ii = (int)obj;
                Console.WriteLine(ii);
            }
            obj = cnt.First; // объект класса Item
            if(obj is Item)
                Console.WriteLine((obj as Item).Str);

#if RUST  // компилятор C# игнорирует этот фрагмент
        //RUST object_class
        class ObjValue
        {
            public string Str;
            public int Int;
            public Item/*&*/ Item;
        }
#endif

Здесь мы знаем, что object принимает значения только int, string и Item, причём это именно ссылка, а не владение Item — им владеют в другом месте.
Создаём класс ObjValue, который игнорируется компилятором C#, но который воспринимается конвертером.

        let mut obj : ObjValue = ObjValue::from_str_(STR_HELLO.clone());
        println!("{}", &obj.to_nstring());
        obj = ObjValue::from_int(10);
        if obj.is_class("i32") {
            let mut ii : i32 = obj.int;
            println!("{}", &NString::from_string(&ii.to_string()));
        }
        obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap())));
        if obj.is_class("Item") {
            println!("{}", obj.item.as_ref().unwrap().borrow().get_str());
        }

pub struct ObjValue {
    pub str_ : NString, 
    pub int : i32, 
    pub item : Option<Rc<RefCell<dyn IItem>>>, 
    _typ : &'static str
}

impl ObjValue {
    pub fn from_str_(val : NString) -> ObjValue {
        ObjValue { str_ : val, int : 0, item : None, _typ : "NString" }
    }
    pub fn from_int(val : i32) -> ObjValue {
        ObjValue { str_ : NString::null(), int : val, item : None, _typ : "i32" }
    }
    pub fn from_item(val : Option<Rc<RefCell<dyn IItem>>>) -> ObjValue {
        ObjValue { str_ : NString::null(), int : 0, item : val, _typ : "Item" }
    }
    pub fn null() -> ObjValue {
        ObjValue { str_ : NString::null(), int : 0, item : None, _typ : "" }
    }
    pub fn is_null(&self) -> bool { self._typ.len() == 0 }
    pub fn is_class(&self, typ : &str) -> bool { self._typ == typ }
    pub fn to_nstring(&self) -> NString {
        if self._typ == "NString" { return self.str_.clone(); }
        if self._typ == "i32" { return NString::from_string(&self.int.to_string()); }
        if self._typ == "Item" { return NString::from_str("Option<Rc<RefCell<dyn IItem>>>"); }
        NString::null()
    }
}

Да, громоздко. Но ведь это же конвертер генерирует! Главное, что работает.
Обратим внимание: для шарпового obj = cnt.First на Rust получается obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap()))). Что говорите, это жесть? Нет, это Раст! Разумеется, человек напишет короче, здесь лишь попытка дать универсальное решение доступа к члену класса.


Классы и наследование

Аналогом класса C# в Rust выступает struct, аналогом интерфейса — trait. Трейты множественно наследуются, структуры — вообще не наследуются. Структура может реализовывать любое число трейтов. То есть как бы в C# убрали наследование от другого класса, но оставили интерфейсы: из ООП полноценной осталась только инкапсуляция. Ну и на том спасибо.
Есть какой-то хитрый способ через виртуальные таблицы как-то моделировать наследование и полиморфизм, разобраться в котором мне попросту не хватило мозгов.
Я придумал следующий способ. Для класса A, от которого идёт наследование, всегда генерируется один trait, в который переносятся все методы, а для публичных полей генерируется функции get и set (как для property). В struct наследного класса B добавляется экземпляр класса A (struct B { base : A, другие поля }), и этот B также реализует этот trait от A. Причём если нужен доступ к полю или методу A, то используется self.base.x.
Приведу пример из кода реализации двунаправленного списка.

    //RUST RefCell
    class Item
    {
        public Item(int val) { Val = val; }
        public int Val { get; set; }
        public string Str;
        public Item/*&*/ Prev { get; set; }
        public Item/*&*/ Next { get; set; }
        public virtual void Inc() { Val += 1; }
    }
    //RUST RefCell
    class ItemChild : Item
    {
        public ItemChild(int val) : base(val) { }
        public override void Inc() { Val *= 2; }
    }

Вот работа конвертера (некоторые фрагменты будут удалены для краткости). Это сгенерированный базовый trait.

pub trait IItem {
    fn get_val(&self) -> i32;
    fn set_val(&mut self, value : i32) -> i32;
    fn get_str(&self) -> &NString;
    fn set_str(&mut self, value : NString) -> &NString;
    fn get_prev(&self) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn set_prev(&mut self, value : Option<Rc<RefCell<dyn IItem>>>) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn get_next(&self) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn set_next(&mut self, value : Option<Rc<RefCell<dyn IItem>>>) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn inc(&mut self);
    fn get_base_class(&self) -> &dyn IItem;
    fn is_class(&self, name : &str) -> bool;
    fn as_item(&self) -> &dyn IItem;
    fn as_mut_item(&mut self) -> &mut dyn IItem;
}

Это реализация базового класса.

pub struct Item {
    pub _val : i32, 
    pub m_str : NString, 
    pub _prev : Option<Rc<RefCell<dyn IItem>>>, 
    pub _next : Option<Rc<RefCell<dyn IItem>>>, 
}

impl IItem for Item {
    fn get_val(&self) -> i32 {
        return self._val;
    }
    fn set_val(&mut self, mut value : i32) -> i32 {
        self._val = value;
        return self._val;
    }
    fn get_prev(&self) -> &Option<Rc<RefCell<dyn IItem>>> {
        return &self._prev;
    }
    fn set_prev(&mut self, mut value : Option<Rc<RefCell<dyn IItem>>>) -> &Option<Rc<RefCell<dyn IItem>>> {
        self._prev = utils::clone_opt_ref(&value);
        return &self._prev;
    }
...
    fn inc(&mut self) {
        self.set_val(self.get_val() + 1);
    }
    fn as_item(&self) -> &dyn IItem { self }
    fn as_mut_item(&mut self) -> &mut dyn IItem { self }
    fn get_base_class(&self) -> &dyn IItem { self }
    fn is_class(&self, name : &str) -> bool { name == "Item" }
}

impl Item {
    pub fn new(mut __val : i32) -> Item {
        let mut self_result = Item {  _val : 0,  _prev : None,  _next : None,  m_str : NString::null() };
        self_result.set_val(__val);
        self_result
    }
}

А вот наследный класс:

pub struct ItemChild {
    pub base : Item, // экземпляр базового класса
}
impl IItem for ItemChild {
    fn get_val(&self) -> i32 {
        self.base.get_val()  // вот здесь работа через экземпляр base
    }
    fn set_val(&mut self, value : i32) -> i32 {
        self.base.set_val(value)
    }
    // а это - переопределённая как бы виртуальная функция
    fn inc(&mut self) {
        self.base.set_val(self.get_val() * 2);
    }
    ....
}

impl ItemChild {
    pub fn new(mut __val : i32) -> ItemChild {
        ItemChild {  base : Item::new(__val) };
    }
}

Обращение к объектам Item и ItemChild везде идёт через ITrait, так что вызов inc() будет именно той функции, объект которой находится за этим trait — а это и есть полиморфизм! Что и требовалось доказать.


Ссылки

У каждой ссылки &T должно быть явно или неявно задано так называемое время жизни (lifetime), чтобы не оказалось так, чтобы ссылка жила дольше самого объекта, на который ссылается. Если использовать ссылки в полях структур, то для самой структуры тоже нужно указывать время жизни: struct A<'a> { ref : &'a Item, ... }. При этом получается как бы новый тип, при использовании которого нужно учитывать это 'a. Это должно ещё коррелировать со временем жизни самого объекта. Короче, когда таких ссылок становится много, наступает lifetime-hell, как я его назвал. Да, Rust тоже внёс свой вклад в коллекцию этих хеллов!
Решение подсказали опытные товарищи: использовать конструкцию Option<Rc<RefCell<T>>>. Выше в примерах кода она уже встречалась. И применять правило — если на объекты класса ссылаются в нескольких местах, то использовать только эту конструкцию для ссылок. Хотя и это не гарантирует корректного освобождения памяти, так как при циклической зависимости обратная ссылка должна быть Option<Weak<RefCell<T>>>. "Вот тут, Василий Иванович, я и сломался! — Дурак ты, Петька..."


Послесловие

Не стану описывать все нюансы конвертации, но признаюсь, что задачу удалось решить лишь частично. Из всего SDK получилось перевести только блоки морфологии и онтологии, что составляет примерно 10% от планируемого. Остальное оказалось непосильным, да и время закончилось, пора возвращаться к своим лингвистическим задачам. Первый "подход к снаряду" показал, что задача в принципе решается, но требует относительно большой корректировки исходного кода C# — это подсказки конвертеру и переписывание недопустимых для Rust фрагментов. А выигрыш производительности получился у меня всего в два раза.
Как язык, Rust не прост, совсем не прост… Советских людей учили, что в жизни всегда есть место подвигу. Программирование на Rust — это, конечно, не подвиг, но что-то героическое в этом есть! Я это оценил на собственной шкуре и приветствую героев!

Let's block ads! (Why?)

[Из песочницы] Command Line Habr

Это пост выходного дня про то как сделать command line версию Хабра.

image

Если ты поклонник минимализма и командной строки, то добро пожаловать под кат.

Идея для создания command line версии Хабра была навеяна комментариями к статье об оптимизации статей с большим количеством комментариев.

И что может быть минимальнее чем командная строка? Так родился скрипт для превращения десктопной версии Хабра в некое подобие терминала с управлением командами.

DISCLAIMER: Хочу сразу предупредить что я не js-ninja и джаваскрипт не мой основной язык разработки, так что не судите строго из-за неоптимального использования конструкций языка.

Также, этот проект создавался как MVP, PoC, JfF и etc. и не ставилось цели реализовать весь возможный спектр функциональности предоставляемый Хабром.

В качестве терминального контрола была использована open-source библиотека JqueryTerminal от Jakub T. Jankiewicz поставляемая по лицензии MIT.

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

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

image

ls – просмотр списка статей.

Например:

image

cd – изменение текущей директории

Список поддерживаемых директорий:

image

pwd – вывод пути текущей директории

whoami – имя текущего пользователя

more – вывод текстовой версии статьи

Например:

image

open – открыть статью в новом окне браузера

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

Для того чтобы самому испробовать скрипт в действии надо:

1. Открыть исходный код скрипта и скопировать его в буфер обмена (Ctrl-a/Ctrl-c)

2. Открыть Хабр (еще раз повторюсь что этот скрипт работает только для десктоп версии сайта)

3. Находясь на странице Хабра, открыть инструменты разработчика (Ctrl-Shift-I для Хрома) и вставить скрипт из буфера обмена в командную строку консоли и нажать Enter. Скрипт автоматически подгрузит необходимые библиотеки и начнёт исполнение.

Если всё сделано правильно, в том окне где у вас был открыт Хабр, вы увидите нечто подобное:

image

Есть еще вариант сконфигурировать подгрузку из GreaseMonkey, но этим расширением пользуется достаточно ограниченное количество пользователей и если понадобится, то они вполне смогут разобраться самостоятельно.

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

Enjoy.

Let's block ads! (Why?)

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

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

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

Примечание: индикаторы – это лишь один из вспомогательных инструментов для работы с биржевыми активами, они не гарантируют положительных результатов торговли. Для того, чтобы разобраться с использованием индикаторов, вам понадобится торговый терминал (например, SMARTx), а также брокерский счет – открыть его можно онлайн. Потренироваться можно с помощью тестового счета с виртуальными деньгами.

Технический анализ: краткая история


Понятие технического анализа акций и трендов существует на протяжение сотен лет. В Европе, купец Джозеф де ла Вега использовал практики технического анализа для предсказания поведения рынков в Голландии в 17 веке.

В современном виде технический анализ был сформирован Чарльзом Доу (Charles Dow), Уильямом Гамильтоном (William P. Hamilton), Робертом Реа (Robert Rhea, автор теории Доу) и другими финансистами, в том числе непрофессиональными, вроде Николаса Дарваса (Nicolas Darvas).

Эти люди представили рынок, состоящий из волн, которые соответствуют на графиках максимумам и минимумам стоимости и объема торгов определенным активом. Все представления о техническом анализе были собраны воедино и обобщены Робертом Эдвардсом (Robert D. Edwards) и Джоном Маги (John Magee) в книге «Технический анализ трендов на фондовом рынке», которая вышла в 1948 году.

В 1990-е в США получила популярность техника, основанная на анализе японских свечей – этот метод сотни лет назад использовали японские купцы для определения трендов в торговле рисом. Инвесторы анализировали графики акций в стремлении увидеть новые паттерны движения цен, которые возможны в будущем, предсказать развороты рынка.

Для чего нужен технический анализ


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

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

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

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

Индикатор «скользящие средние» с отмеченными точками входа и выхода из сделки

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

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

Ограничения технического анализа


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

И еще один важный фактор: чем популярнее становится технический анализ и конкретные его техники, индикаторы, тем труднее становится понять – цена сама сформировала определенный паттерн на графике, или тысячи инвесторов видят одну и ту же картину, верят, что рынок упадет и начинают продавать, усиливая его падение?

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

Читайте обзоры, аналитику рынков и инвестидеи в Telegram-канале ITI Capital

Полезные ссылки по теме инвестиций и биржевой торговли:


Let's block ads! (Why?)

[Перевод] Структуры данных и алгоритмы, которыми я пользовался, работая в технологических компаниях

Пользуетесь ли вы структурами данных и алгоритмами в повседневной работе? Я обратил внимание на то, что всё больше и больше людей считает алгоритмы чем-то таким, чем, без особой связи с реальностью, технические компании, лишь по собственной прихоти, интересуются на собеседованиях. Многие жалуются на то, что задачи на алгоритмы — это нечто из области теории, имеющей слабое отношение к настоящей работе. Такой взгляд на вещи, определённо, распространился после того, как Макс Хауэлл, автор Homebrew, опубликовал твит о том, что произошло с ним на собеседовании в Google:

Google: 90% наших инженеров пользуются программой, которую вы написали (Homebrew), но вы не можете инвертировать бинарное дерево на доске, поэтому — прощайте.

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

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

А теперь давайте перейдём к примерам использования структур данных и алгоритмов в реальной жизни.

Деревья и обход деревьев: Skype, Uber и UI-фреймворки


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

Мы создали базовый навигационный фреймворк, основанный на WinJS. Для того чтобы это сделать, нам понадобилось поддерживать граф, похожий на DOM, что требовалось для организации наблюдения за элементами, с которыми мог взаимодействовать пользователь. Для поиска таких элементов мы выполняли обход DOM, который сводился к обходу дерева, то есть — существующей структуры DOM-элементов. Это — классический пример BFS или DFS (breadth-first search или depth-first search — поиск в ширину или поиск в глубину).

Если вы занимаетесь веб-разработкой — это значит, что вы уже работаете с древовидной структурой, а именно — с DOM. У всех узлов DOM могут быть дочерние узлы. Браузер выводит узлы на экран после обхода дерева DOM. Если вам надо найти конкретный элемент, то для решения этой задачи можно воспользоваться встроенными методами DOM. Например — методом getElementById. Альтернативой этого может стать разработка собственного BFS- или DFS-решения для обхода узлов и поиска того, что нужно. Например, нечто подобное сделано здесь.

Многие фреймворки, которые рендерят элементы пользовательского интерфейса, используют, в своих недрах, древовидные структуры. Так, React поддерживает виртуальную DOM и использует интеллектуальный алгоритм согласования (сравнения). Это позволяет добиваться высокой производительности за счёт того, что повторному рендерингу подвергаются только изменённые части интерфейса. Здесь можно найти визуализацию этого процесса.

В мобильной архитектуре Uber, RIBs, тоже используются деревья. Это роднит данную архитектуру с большинством других UI-фреймворков, которые выводят на экраны иерархические структуры элементов. В архитектуре RIBs поддерживается, с целью управления состоянием, дерево RIB. Прикрепляя к нему RIBs и открепляя их от него, управляют их рендерингом. Работая с RIBs, мы иногда делали наброски, пытаясь понять, вписывается ли RIBs в существующую иерархию, и обсуждали то, должны ли у RIBs, о котором идёт речь, быть видимые элементы. То есть — говорили о том, будет ли эта структура принимать участие в формировании визуального представления интерфейса, или она будет использоваться лишь для управления состоянием.


Переходы между состояниями при использовании RIBs. Здесь можно найти подробности о RIBs

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

Взвешенные графы и поиск кратчайшего пути: Skyscanner


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

Планирование путешествий с посещением нескольких городов стало одной из возможностей, на реализацию которой у Skyscanner ушло довольно много времени. При этом сложности, в основном, относились к самой разрабатываемой системе. Лучшие предложения такого рода находят, используя алгоритмы поиска кратчайшего пути — вроде алгоритма Дейкстры или A*. Маршруты полётов представляют в виде ориентированного графа. Каждому его ребру назначен вес в виде стоимости билета. При поиске наилучшего маршрута нахождение самого дешёвого пути между двумя городами выполняется с использованием реализации модифицированного алгоритма A*. Если вам интересна тема подбора авиабилетов и поиска кратчайших маршрутов, то вот хорошая статья, посвящённая использованию BFS для решения подобных задач.

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

Сортировка: Skype (или что-то вроде этого)


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

В Skype мне, правда, пришлось прибегнуть к практическому использованию моих знаний об алгоритмах сортировки. Один из наших программистов решил реализовать, для вывода контактов, сортировку вставками. В 2013 году, когда Skype подключался к сети, контакты загружались партиями. Для загрузки их всех требовалось некоторое время. В результате тот программист подумал, что лучше будет строить список контактов, упорядоченных по именам, используя сортировку вставками. Мы много обсуждали этот алгоритм, размышляли о том, почему бы просто не использовать что-то такое, что уже реализовано. В результате больше всего времени у нас ушло на то, чтобы как следует протестировать нашу реализацию алгоритма и проверить её производительность. Лично я не видел особого смысла в создании собственной реализации этого алгоритма. Но тогда проект был на такой стадии, на которой у нас было время на подобные дела.

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

Хеш-таблицы и хеширование: везде


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

Стеки и очереди: время от времени


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

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

Криптографические алгоритмы: Uber


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

Разбираться в криптографических алгоритмах — это очень интересно. При этом для решения неких задач не стоит предлагать собственные алгоритмы. Это — одна из худших идей, которая может прийти в голову программисту. Вместо этого берётся существующий, хорошо документированный стандарт и используются встроенные примитивы соответствующих фреймворков. Обычно в качестве стандарта, выбираемого при реализации криптографических решений, выступает AES. Он достаточно надёжен для того чтобы шифровать с его помощью секретную информацию в США. Не существует известных работающих атак на протокол. AES-192 и AES-256 обычно, для решения большинства практических задач, достаточно надёжны.

Когда я пришёл в Uber, мобильная система шифрования и система шифрования веб-приложения уже были реализованы, в их основе лежали вышеописанные механизмы, поэтому у меня был повод для изучения подробностей об AES (Advanced Encryption Standard), о HMAC (Hashed Message Authentication Codes), об алгоритме RSA и о прочих подобных вещах. Интересно было и дойти до понимания того, как доказывают криптостойкость последовательности действий, применяемой при шифровании. Например, если говорить об аутентифицированном шифровании с присоединёнными данными, оказывается, что анализируя режимы encrypt-and-MAC, MAC-then-encrypt и encrypt-then-MAC, можно доказать криптостойкость лишь одного из них, хотя это и не означает того, что остальные не являются криптостойкими.

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

Деревья решений: Uber


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

Программист, который приступил к решению этой задачи, сначала попробовал выразить все эти правила в виде инструкций if-else. Это привело к появлению чрезвычайно запутанного кода. В итоге решено было воспользоваться деревом решений. С его помощью было несложно произвести все необходимые проверки. Его было сравнительно просто реализовать. Кроме того, если нужно, его можно было, без особых проблем, изменить. Нам нужно было создать собственную реализацию дерева решений, такого, чтобы проверка условий осуществлялась бы на его рёбрах. Это — всё, что нам было нужно от этого дерева. Хотя мы могли бы сэкономить время, потраченное на реализацию дерева, воспользовавшись другим подходом, команда решила, что особое дерево будет легче поддерживать, и приступила к работе. Выглядит это дерево примерно так: рёбра символизируют результаты проверки условий (это — бинарные результаты, или результаты, представленные диапазонами значений), а листовые узлы дерева описывают экраны, на которые нужно выполнять переходы.


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

В системе сборки мобильной версии приложения Uber, называемой SubmitQueue, тоже использовалось дерево решений, но оно формировалось динамически. Команде Developer Experience нужно было решить сложную проблему выполнения ежедневного слияния (merge) сотен веток-источников изменений с целевой веткой. При этом на выполнение каждой сборки нужно было около 30 минут. Сюда входили компиляция, выполнение модульных и интеграционных тестов, а также тестов интерфейса. Распараллеливание сборок было недостаточно хорошим решением, так как в различных сборках часто были перекрывающиеся изменения, что вызывало конфликты слияния. На практике это означало, что иногда программистам приходилось ждать 2-3 часа, прибегать к команде rebase, и снова запускать процесс слияния веток, надеясь, что в этот раз они не столкнутся с конфликтом.

Команда Developer Experience применила инновационный подход, который заключался в прогнозировании конфликтов слияния и в соответствующем размещении сборок в очереди. Делалось это с использованием особого бинарного дерева принятия решений (speculation tree).


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

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

Шестиугольные сетки, иерархические индексы: Uber


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

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


Разбиение карты на шестиугольные ячейки 

Эта структура данных отличается собственной системой индексирования, обхода, собственными определениями отдельных областей сетки, собственными функциями. Детальное описание этого всего можно найти в документации к соответствующему API. Подробности об H3 можно почитать здесь. Вот — исходный код. Здесь можно найти рассказ о том, как и почему была создана эта система.

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

Структуры данных и алгоритмы на собеседованиях


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

Я считаю, что знания о том, как работают популярные алгоритмы, или о том, как устроены экзотические структуры данных, это — не те знания, которые нужны для работы в технологической компании. Нужно знать о том, что такое алгоритм, нужно уметь самостоятельно придумывать простые алгоритмы, например, работающие по «жадному» принципу. Ещё нужно обладать знаниями о самых распространённых структурах данных, вроде хеш-таблиц, очередей и стеков. Но что-то достаточно специфическое, вроде алгоритма Дейкстры или A*, или чего-то ещё более сложного, это — не то, что нужно заучивать наизусть. Если вам эти алгоритмы реально понадобятся — вы легко сможете найти справочные материалы по ним. Я, например, если говорить об алгоритмах, не относящихся к алгоритмам сортировки, обычно стремился разобраться с ними в общих чертах и понять их сущность. То же касается и экзотических структур данных вроде красно-чёрных деревьев и АВЛ-деревьев. У меня никогда не возникало нужды в их использовании. А если бы они мне и понадобились — я всегда мог бы освежить знания о них, прибегнув к соответствующим публикациям. Я, проводя собеседования, как я уже говорил, никогда не задаю подобные вопросы и не планирую их задавать.

Я за практические задачи, имеющие отношение к программированию, при решении которых можно применить множество подходов: от методов «грубой силы» и «жадных» вариантов алгоритмов до более продвинутых алгоритмических идей. Например, я полагаю, что вполне нормальной является задача, касающаяся выравнивания текста по ширине, вроде этой. Мне, например, пришлось решать эту задачу при создании компонента для рендеринга текста на Windows Phone. Решить эту задачу можно, просто воспользовавшись массивом и несколькими инструкциями if-else, не прибегая к каким-то мудрёным структурам данным.

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

Если вы — из компании, которая нанимает только тех, кто едва ли не с рождения обладает глубокими знаниями сложных алгоритмов, я предлагаю вам хорошо подумать о том, те ли это люди, что вам нужны. Я, например, нанимал прекрасные команды в Skyscanner (Лондон) и в Uber (Амстердам), не задавая соискателям хитрых вопросов по алгоритмам. Я ограничивался самыми обычными структурами данных и проверкой возможностей собеседуемых, имеющих отношение к решению задач. То есть — им нужно было знать о распространённых структурах данных и уметь придумывать простые алгоритмы для решения стоящих перед ними задач. Структуры данных и алгоритмы — это всего лишь инструменты.

Итоги: структуры данных и алгоритмы — это инструменты


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

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

Если вы хотите лучше изучить структуры данных и алгоритмы — вот несколько советов и ресурсов:

  • Почитайте о хеш-таблицах, связных списках, деревьях, графах, кучах, очередях, стеках. Поэкспериментируйте с ними, используя тот язык, на котором пишете. Вот хороший обзор, посвящённый структурам данных. А вот — место, где можно попрактиковаться.
  • Почитайте книгу «Грокаем алгоритмы». Это, как мне кажется, лучший путеводитель по алгоритмам, предназначенный для самых разных категорий программистов. Это — доступное иллюстрированное руководство, в котором содержится всё, что большинству специалистов нужно знать по данной теме. Я уверен в том, что вам не понадобится больше знаний об алгоритмах, чем те, что вы сможете найти в этой книге.
  • Вот ещё пара книг: «Алгоритмы. Руководство по разработке» и «Алгоритмы на Java, 4-е издание». Я воспользовался ими для того чтобы освежить в памяти университетские знания об алгоритмах. Я их, правда, до конца не дочитал. Они показались мне достаточно сухими и неприменимыми к моей повседневной работе.

А с какими структурами данных и алгоритмами сталкивались на практике вы?

Let's block ads! (Why?)

[Из песочницы] Boost.Compute или параллельные вычисления на GPU/CPU. Часть 1

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

По моим меркам я уже достаточно давно пишу код на C++, но до этого времени ещё не сталкивался с задачами, связанными с параллельными вычислениями. Я не увидел ни одной статьи о библиотеке Boost.Compute, поэтому эта статья будет именно о ней.

Содержание


  • Что такое boost.compute
  • Проблемы с подключением boost.compute к проекту
  • Введение в boost.compute
  • Основные классы compute
  • Приступаем к работе
  • Заключение

Что такое boost.compute


Данная c++ библиотека предоставляет простой высокоуровневый интерфейс для взаимодействия с многоядерными CPU и GPU вычислительными устройствами. Эта библиотека была впервые добавлена в boost в версии 1.61.0 и поддерживается до сих пор.

Проблемы с подключением boost.compute к проекту


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

image

После подключения всё должно скомпилироваться корректно.

На счёт библиотеки boost, её можно скачать и подключить к проекту Visual Studio с помощью менеджера пакетов NuGet.

Введение в boost.compute


После установки всех необходимых компонентов можно рассмотреть простые куски кода. Для корректной работы достаточно включить модуль compute таким образом:
#include <boost/compute.hpp>
using namespace boost;

Стоит подметить, что обычные контейнеры из stl не подойдут для использования в алгоритмах пространства имён compute. Вместо них существуют специально созданные контейнеры которые не конфликтуют с стандартными. Пример кода:
std::vector<float> std_vector(10);
compute::vector<float> compute_vector(std_vector.begin(), std_vector.end(), queue); 
// пока не обращайте внимания на третий аргумент, к нему мы вернёмся позже.

Для конвертации обратно в std::vector можно использовать функцию copy():
compute::copy(compute_vector.begin(), compute_vector.end(), std_vector.begin(), queue);

Основные классы compute


Библиотека насчитывает в себе три вспомогательных класса, которых для начала хватит для вычислений на видеокарте и/или процессоре:
  • compute::device (будет определять с каким именно устройством мы будем работать)
  • compute::context (объект данного класса хранит в себе ресурсы OpenCL, включая буферы памяти и другие объекты)
  • compute::command_queue (предоставляет интерфейс для взаимодействия с вычислительным устройством)

Объявить это всё дело можно таким образом:
auto device = compute::system::default_device(); // устройство по умолчанию это видеокарта
auto context = compute::context::context(device); // обычное объявление переменной
auto queue = compute::command_queue(context, device); // аналогично к предыдущему

Даже только с помощью первой строчки кода выше можно убедится что всё работает как нужно, запустив следующий код:
std::cout << device.name() << std::endl; 

Таким образом мы получили имя устройства, на котором будем производить вычисления. Результат (у вас может быть что-то другое):

image

Приступаем к работе


Рассмотрим функции trasform() и reduce() на примере:
std::vector<float> host_vec = {1, 4, 9};

compute::vector<float> com_vec(host_vec.begin(), host_vec.end(), queue);
// передавая в аргументы начальный и конечный указатель предыдущего вектора можно не
//использовать функцию copy()

compute::vector<float> buff_result(host_vec.size(), context);
transform(com_vec.begin(), com_vec.end(), buff_result.begin(), compute::sqrt<float>(), queue);

std::vector<float> transform_result(host_vec.size());
compute::copy(buff_result.begin(), buff_result.end(), transform_result.begin(), queue);
        
cout << "Transforming result: ";
for (size_t i = 0; i < transform_result.size(); i++)
{
        cout << transform_result[i] << " ";
}
cout << endl;

float reduce_result;
compute::reduce(com_vec.begin(), com_vec.end(), &reduce_result, compute::plus<float>(),queue);

cout << "Reducing result: " << reduce_result << endl;

При запуске приведённого выше кода, вы должны увидеть такой результат:

image

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

И так, функция transform() используется для того, чтобы изменить массив данных,(или два массива, если мы их передаём) применяя одну функцию ко всем значениям.

transform(com_vec.begin(), 
   com_vec.end(), 
   buff_result.begin(), 
   compute::sqrt<float>(), 
   queue);

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

Следующая функция reduce(), тут все немного интереснее. Этот метод возвращает результат применения четвёртого аргумента ко всем элементам вектора.

compute::reduce(com_vec.begin(), 
   com_vec.end(), 
   &reduce_result, 
   compute::plus<float>(),
   queue);

Сейчас поясню на примере, код выше можно сравнить с таким уравнением:
$inline$1 + 4 + 9$inline$
В нашем случае мы получаем суму всех элементов массива.

Заключение


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

Буду рад позитивному фидбэку. Спасибо за уделённое время.

Всем удачи!

Let's block ads! (Why?)

LINQ на JavaScript для самых маленьких

Шел очередной день самоизоляции, и я делал один из тех проектов для себя, которые мы забрасываем через пару дней после того как начали. Ну вы знаете, тот проект, который сделает вас знаменитым, позволит вам выучить новый ЯП, новый фреймворк и все такое. В общем, это был самый обычный день, самой обычной пандемии. Ничего не предвещало беды, пока одна библиотека, которой я пользовался для работы с массивами не выпала со stack overflow… И вот тут-то все и запузырилось.

В общем-то я прекрасно понимаю, что можно было использовать что-то готовое, но так не интересно.


Почему LINQ?

Вкратце для тех, кто не в курсе:


LINQ (Language-Integrated Query) представляет простой и удобный язык запросов к источнику данных. В качестве источника данных может выступать объект, реализующий интерфейс IEnumerable (например, стандартные коллекции, массивы), набор данных DataSet, документ XML.

И LINQ позволяет делать вот так:

string[] teams = { "Бавария", "Боруссия", "Реал Мадрид", "Манчестер Сити", "ПСЖ", "Барселона" };

var selectedTeams = teams.Where(t=>t.ToUpper().StartsWith("Б")).OrderBy(t => t);

Из особых преимуществ хотелось бы отметить:


  • Lazy-computation
  • Оптимизацию запросов
  • Удобная система методов расширения

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


Приступая к работе

Хотелось бы сразу, с шашкой наголо, броситься в дело, но мы так не поступим — мы вообще-то серьезные люди, которые пишут серьезные вещи.

Поэтому поставим для себя некоторые требования, помимо того, что указано в преимуществах LINQ:


  • Код должен быть легко-расширяемым
  • Код должен быть быстрым

Для оценки скорости возьмем Benchmark.js. А за эталон мы возьмем Lodash, потому что он написан серьезными большими мальчиками, а значит годится в качестве некоторой точки отсчета.

Если вам интересны исходники, то вот репозиторий, а если хочется просто поиграться с тем, что получилось, то вот npm-пакет.


В этой статье я расскажу как сделать несколько методов, потому что, если я буду описывать весь код, то эту портянку никто не будет читать.

Разберем мы:


  • Where
  • Sort
  • Min

Эти методы я, в общем-то, взял не с потолка:


  • Where мы рассмотрим как метод, который легко поддается оптимизации и его можно использовать для ленивых вычислений.
  • Sort как метод, который требует чтобы выражение до него было вычислено.
  • Min как пример агрегатной операции.

Приступим

Начнем с некоторых умозаключений:


  1. Мы хотим иметь методы расширения
  2. Методы расширения будут возвращать нам новые данные

На мой взгляд это очень похоже на паттерн Декоратор.

Если кратко, то схема классов должна быть такой:

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

export class Collection<T> implements ICollection<T> {
    protected inner: Collection<T>;
    private _computed: T[] | null = null; // Здесь мы с вами будем хранить уже посчитанный результат выражения, после того как коллекция "материализовалась"

    public where(condition: FilterCondition<T>): ICollection<T> {
        return new FilteringCollection<T>(this, condition);
    }

    public sort(condition?: CompareCondition<T> | undefined): ICollection<T> {
        return new SortingCollection<T>(this, {
            compare: condition
        })
    }

    public min(predicate?: CompareCondition<T> | undefined): T {
        return new MinAggregator(this, predicate).aggregate();
    }

    public toArray(): T[] {
        return this.computed;
    }

    public [Symbol.iterator](): IterableIterator<T> {
        return this.getIterator();
    }

    public getIterator(): IterableIterator<T> {
        return this.computed[Symbol.iterator](); // По задумке, коллекция материализуется еще и в цикле for - of
    }

    private get computed(): T[] { // Ленивые вычисления: материализуем коллекцию только при вызове геттера
        if (this._computed == null) {
            const result = this.materialize();

            Object.freeze(result);

            this._computed = result;
        }
        return this._computed
    }

    protected materialize(): T[] { // Метод материализации коллекции
        return this.inner.toArray();
    }
}

"В чем здесь декоратор?" — спросите вы, а я вам отвечу: "Учите матчасть".

Допустим есть выражение:

const collection = new Collection([6, 5, 4, 3, 2, 1]);

const result = collection.where(item => item % 2).sort(); // [1, 3, 5]

Разберем его по шагам, пусть:

        const collection = new Collection([6, 5, 4, 3, 2, 1]);
/* 1) */const filtered = collection.where(item => item % 2);
/* 2) */const sorted = filtered.sort();

1) Мы вызываем метод where у класса Collection, мы передаем ему ссылку на текущую коллекцию и записываем ее в поле inner.
2) Мы вызываем метод sort у класса FilteringCollection, мы передаем ему ссылку на текущую коллекцию и записываем ее в поле inner.

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

1) Вызовется materialize у FilteringCollection и вернется массив [5, 3, 1].
2) Вызовется materialize у SortingCollection и вернется массив [1, 3, 5].


Where


Реализация фильтрующего декоратора

export class FilteringCollection<T> extends Collection<T> {
    public constructor(iterable: Collection<T> | T[], private condition: FilterCondition<T>) {
        super(iterable);
    }

    public where(condition: FilterCondition<T>): ICollection<T> { // Перегрузка метода where
        const that = this;

        const result = new FilteringCollection<T>(this.inner, item => condition(item) && that.condition(item));

        return result;
    }

    protected materialize(): T[] { // перегрузка метода материализации
        return this.inner.toArray().filter(this.condition);
    }
}

Пояснение

В классе хранится ссылка на внутреннюю коллекцию и выражение для фильтрации. Все.
Это все что нам нужно, для того чтобы реализовать метод расширения where(). Красиво выглядит, неправда ли?

Отдельно хотелось бы отметить, что данная реализация оптимизирует проход по цепочке из where:

То есть

_(cats).where(cat => cat.age < 3).where(cat => cat.age > 1).toArray()

будет преобразовано в

_(cats).where(cat => cat.age < 3 && (function(item){
    return item.age > 1;
}(cat))).toArray()

происходит эта оптимизация вот тут:

public where(condition: FilterCondition<T>): ICollection<T> { // Перегрузка метода where
        const result = new FilteringCollection<T>(this.inner, item => condition(item) && this.condition(item)); // <-- тут

        return result;
    }

Мы просто отдаем в новый декоратор исходную коллекцию и подменяем выражение "сращенным".

В методе materialize у нас просто используется метод массива filter. Я выбрал его как самый простой и эффективный вариант для фильтрации. Все другое, что я пробовал проигрывало в производительности.


Поговорим о производительности
----------------------------------------------------
Filter for 1000000:

Where x 104 ops/sec ±14.73% (61 runs sampled)
Lodash filter x 609 ops/sec ±0.67% (88 runs sampled)
Native filter x 537 ops/sec ±1.69% (85 runs sampled)

Double where x 102 ops/sec ±11.51% (64 runs sampled)
Double lodash filter x 368 ops/sec ±1.00% (88 runs sampled)
Double native filter x 336 ops/sec ±1.08% (84 runs sampled)

10 where x 66.60 ops/sec ±9.15% (59 runs sampled)
10 lodash filter x 99.44 ops/sec ±1.20% (73 runs sampled)
10 native filter x 81.80 ops/sec ±1.33% (70 runs sampled)
----------------------------------------------------
Filter for 1000:

Where x 24,296 ops/sec ±0.90% (88 runs sampled)
Lodash filter x 60,927 ops/sec ±0.90% (89 runs sampled)
Native filter x 204,522 ops/sec ±6.76% (87 runs sampled)

Double where x 20,281 ops/sec ±0.86% (90 runs sampled)
Double lodash filter x 37,553 ops/sec ±0.97% (90 runs sampled)
Double native filter x 115,652 ops/sec ±6.12% (91 runs sampled)

10 where x 9,559 ops/sec ±1.09% (87 runs sampled)
10 lodash filter x 8,850 ops/sec ±0.80% (87 runs sampled)
10 native filter x 22,507 ops/sec ±9.22% (84 runs sampled)
----------------------------------------------------
Filter for 10:

Where x 1,788,009 ops/sec ±0.81% (87 runs sampled)
Lodash filter x 720,558 ops/sec ±0.80% (84 runs sampled)
Native filter x 14,917,151 ops/sec ±0.61% (85 runs sampled)

Double where x 1,257,163 ops/sec ±0.52% (95 runs sampled)
Double lodash filter x 456,365 ops/sec ±0.74% (76 runs sampled)
Double native filter x 8,262,940 ops/sec ±0.64% (90 runs sampled)

10 where x 489,733 ops/sec ±0.67% (94 runs sampled)
10 lodash filter x 135,275 ops/sec ±0.61% (94 runs sampled)
10 native filter x 1,350,316 ops/sec ±0.94% (90 runs sampled)
----------------------------------------------------

Коротко для ленивых: Наши побеждают, но только на коротких дистанциях.
Кстати такой расклад еще и с select (его реализацию вы можете увидеть в исходниках).


Sort


Реализация сортирующего декоратора

export class SortingCollection<T, V = T> extends Collection<T> implements ISortingCollection<T> {
    private sortSettings: SortSettings<T, V>[];

    public constructor(iterable: Collection<T>, ...sortSettings: SortSettings<T, V>[]) {
        super(iterable);
        this.sortSettings = _(sortSettings)
        .where(item => !!item.compare || !!item.mapping) // Получаем правила маппинга полей и сортировку по ним
        .toArray();
    }

    protected materialize(): T[] {
        const comparer = new Comparer(this.sortSettings, this.defaultCompare);

        return  Array.from(this.inner.toArray()).sort(this.sortSettings.length ? (first, second) => comparer.compare(first, second) : undefined);
    }

    private defaultCompare(first: V, second: V): number {
        if(first < second) {
            return -1
        } else if (second < first) {
            return 1
        } else {
            return 0;
        }
    }
}

Пояснение

В классе хранится ссылка на внутреннюю коллекцию и настройки сортировки. Переопределен метод материализации. За сравнение объектов отвечает специальный класс Comparer.

Отдельно бы хотелось отметить тот факт что Lodash, так же как и js изменяет исходный массив при сортировке.


Поговорим о производительности
----------------------------------------------------
Sort for 1000000:

Sort x 0.80 ops/sec ±3.59% (6 runs sampled)
Lodash sort x 0.97 ops/sec ±27.98% (7 runs sampled)
Native sort x 1.05 ops/sec ±14.71% (7 runs sampled)

SortBy x 0.19 ops/sec ±10.31% (5 runs sampled)
Lodash SortBy x 0.37 ops/sec ±7.21% (5 runs sampled)
Sort after map x 0.47 ops/sec ±8.67% (6 runs sampled)
----------------------------------------------------
Sort for 1000:

Sort x 1,121 ops/sec ±0.77% (85 runs sampled)
Lodash sort x 1,267 ops/sec ±0.77% (89 runs sampled)
Native sort x 1,274 ops/sec ±0.88% (86 runs sampled)

SortBy x 488 ops/sec ±1.45% (80 runs sampled)
Lodash SortBy x 549 ops/sec ±9.60% (70 runs sampled)
Sort after map x 954 ops/sec ±1.50% (83 runs sampled)
----------------------------------------------------
Sort for 10:

Sort x 171,700 ops/sec ±1.38% (85 runs sampled)
Lodash sort x 196,364 ops/sec ±2.01% (80 runs sampled)
Native sort x 250,820 ops/sec ±0.96% (85 runs sampled)

SortBy x 114,064 ops/sec ±0.90% (86 runs sampled)
Lodash SortBy x 86,370 ops/sec ±17.93% (67 runs sampled)
Sort after map x 221,034 ops/sec ±1.31% (87 runs sampled)
----------------------------------------------------

Все так же для ленивых:
± одинаково.


Min


Реализация агрератора поиска минимального элемента

export class MinAggregator<T> extends ReduceAggregator<T> {
    public constructor(collection: ICollection<T>, condition?: CompareCondition<T>) {
        super(collection, condition ? (first, second) => this.compare(first, second, condition) : (a, b) => a < b ? a : b)
    }

    private compare(first: T, second: T, func: CompareCondition<T>): T {
        return func(first, second) < 0 ? first : second;
    }
}

export class ReduceAggregator<T> extends Aggregator<T> {
    public constructor(private collection: ICollection<T>, protected predicate: ReduceCondition<T>){
        super();
    }

    public aggregate(): T {
        return this.collection.toArray().reduce((first, second) => this.predicate(first, second))
    }
}

Не ожидали? А декоратора не будет.

Вместо него у нас будет агрегатор.


Пояснение

Не уверен что оно вообще нужно, мы просто делаем свертку.


Поговорим о производительности
----------------------------------------------------
Aggregate for 1000000:
Min x 43.69 ops/sec ±28.48% (65 runs sampled)
Lodash Min x 117 ops/sec ±0.58% (73 runs sampled)
----------------------------------------------------
Aggregate for 1000:
Min x 61,220 ops/sec ±5.10% (87 runs sampled)
Lodash Min x 111,452 ops/sec ±0.72% (90 runs sampled)
----------------------------------------------------
Aggregate for 10:
Min x 3,502,264 ops/sec ±1.36% (86 runs sampled)
Lodash Min x 4,181,189 ops/sec ±1.48% (86 runs sampled)
----------------------------------------------------
Aggregate By for 1000000 disp 50:
Min By x 23.81 ops/sec ±2.02% (42 runs sampled)
Lodash Min By x 42.66 ops/sec ±2.46% (55 runs sampled)
----------------------------------------------------
Aggregate By for 1000 disp 50:
Min By x 43,064 ops/sec ±0.71% (86 runs sampled)
Lodash Min By x 60,212 ops/sec ±0.89% (87 runs sampled)
----------------------------------------------------
Aggregate By for 100 disp 50:
Min By x 351,098 ops/sec ±1.03% (81 runs sampled)
Lodash Min By x 382,302 ops/sec ±1.39% (76 runs sampled)
----------------------------------------------------

Lodash победил.


Итоги

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


Если кто знает как понизить стоимость итерации при фильтрации и маппинге, а именно вот тут:
this.inner.toArray().filter(this.condition);

то буду раз узнать ответ!

Открыт к критике, хочу сделать код лучше.

Всем спасибо за внимание.

Репозиторий
Npm-пакет

Let's block ads! (Why?)