Developer's notes
33 subscribers
67 photos
4 videos
74 links
Пишу обо всём и ни о чём, могу и о программировании
Download Telegram
Куда оно летит?

Часть 3.

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

Где-то в 2019ом году, состоялся первый релиз приложения. Как я уже упоминал, приложение – не настоящий стартап, иначе говоря, его функционал лежал вне основных интереса бизнеса, поэтому после релиза дальнейших задач по этому приложению не было (и не планировалось – это не было сюрпризом). Что несколько удивляло – так это реакция менеджмента: “Отлично, что вы с этим справились, мы подумаем, что дальше, а пока занимайтесь поддержкой своих проектов”.

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

Спустя несколько месяцев, мы вернулись к разработке этого приложения: кое-что писали уже на Kotlin, активно продвигаемый Google, на нас же свалили и поддержку backend для этого приложения, так же (коряво) написанный на Kotlin, но…объем фич был уже ни тот, и за это время технологии модные ещё вчера стали устаревать. Да, до сих пор некоторые технологии из моего первого поста используются в реальных проектах, но в тренде прежде всего Kotlin, Coroutines, Clean Architecture

В следующий раз, я точно закончу этот цикл! Будьте на связи

#job #IT #Android #Java #memories #ToBeContinued #working_days
Неожиданные проблемы нативных типов

Недавно попалась на глаза статья про редкие языки программирования и была там фраза, что мол в Cobol есть отдельный тип для представления денег…и я вспомнил, что когда-то, уже очень давно, я и мои коллеги, тоже сталкивались с проблемами представления денежных сумм в C++. Казалось бы – ерунда какая? В C++ (Java, C# – туда же) есть типы float и double для представления чисел с запятой, почему бы их не использовать? Потому что они не подходят: float уже на вот таком примере “сломается”:

    float a = 0.01;
float b = 999999.00;
float sum = a + b;
std::cout << sum << std::endl;


Выведя 999999.

С double надо искать пример посложнее, но, поверьте, когда его найдут у вас в программе – будет не до смеха. Тут можно отметить пару основных недостатков: большинство дробей типа 0.1, 0.2 и т.д. не имеют конечного представления в двоичной системе, при сложениях и вычитаниях эти числа “выравниваются” внутри что б быть представлены с той же самой степень, а число знаков, выделенных под мантиссу – конечно, можно тут потерять одно из чисел совсем, как в примере выше.

Как же хранить денежные величины в C++ – легко, храните их (в Сберегательной кассе) как целое – число копеек, используя long long int, перегрузите соответствующие операторы конструкторы и так далее. Забавно, что в Standard C++ library нет ничего подходящего для решения проблемы, как и в Qt. В sql есть, например, decimal.

Тут можно заметить, что основные типы данных в C++, по сути, те же, что в языке Си, в котором они те же, что дает архитектура процессора – т.к. язык Си – это высокоуровневый ассемблер, то есть байт, 2 байта, 4, возможно 8 для целых типов, и 4, 8 байтов для чисел с плавающей запятой. Так оно и тянется с тех пор… В тоже время, те же double/float неплохо подходят для инженерных расчетов, если бы в примере выше мы бы считали силу тока, движущий момент, любую другую физическую величину, никого бы не взволновала потеря одной сотой, на фоне величины числа a – эта ошибка вписывается в допустимую погрешность.

#job #IT #c_plus_plus #math
👍1
Спасибо копипасту

В программисткой среде известный метод “скопировать-и-вставить” с одной стороны считается чем-то, вроде, дурного тона – с другой стороны все так делают. Действительно: какой нормальный человек будет заново изобретать то, что он уже написал и работает если можно это скопировать и, вот тут самое главное – поправить и адаптировать – так что б это не выглядело копипастом. Ещё раз: речь не идёт про код в стиле макароны, индуский код, незнание SOLID и так далее – речь про то, что это рабочая методика где-то глубоко до открытия merge request и прохождения code-review. В наши дни многие уже умудряются писать код с помощью ChatGpt – на этом фоне про копипаст как нечто плохое и вовсе пора забыть...

Однако, я хотел вспомнить случай, когда копипаст мне помог решить задачу, сэкономив недели, а может быть месяцы ковыряния в спецификации. Дело было во времена моего участия в проекте из поста: пару месяцев уже прибывая там, я взял очередной баг, формулировка у него была что-то навроде: “flex layout не обрабатывает какой-то пограничный случай вот картинка как надо, вот что у нас” – дополнительная инфа от других разработчиков была – вот смотри тут в коде ссылка на спецификацию, ты почитай её. Ладно…звучит, как бага на пару строк кода, конечно, осталось найти куда эти строки вставить и сообразить, что там написать, ну в плохом варианте это неделя или две.

До того момента я про flex layout знал чуть меньше, чем ничего: HTML я глубоко не занимался, в Qt свои layouts, к тому же, мне никогда не нужно было их создавать – только использовать готовые, к слову, flex layout это совсем не QGridLayout. Глянув ещё раз пример, что должно быть и что было, глянув кратко спецификацию, я решил, что проще всего будет отдебажить, почему тут неправильно считается, это заняло дня 3: я нашёл, где поправить на пару строк и довольный открыл пулл реквест…Каково же было моё удивление узнать, что на самом деле, я должен был сделать не маленький фикс, а реализовать вот этот алгоритм из спецификации.

Тут мне взгруснулось…дело явно на месяц(ы), спецификация написана несколько пространным языком, не всё ясно как приземлять на то, что я имею уже в коде, конечно, это возможность стать “уникальным” специалистом по этой микро-нано области, но оно мне зачем? Очень быстро понял, что единственный рабочий вариант тут для меня – найти, где это уже реализовано, причём, понятно, что это должно быть реализовано. Нашёл исходной код проекта WebKit там довольно быстро нашёл именно этот алгоритм – тут дело уже пошло веселее: из WebKit можно было “перевести” в наш проект, надо было только учесть, что в WebKit всё аккуратно, размазано по своим классам, а у нас было свалено куда придётся, но это не важно. Был именно момент “copy-paste” , когда я скопировал кусок кода оттуда и потом просто адаптировал его, после чего уже успешно прошёл код-ревью и закрыл задачу…

#job #c_plus_plus #IT #web #HTML
🔥1
Сравните это

Часть 1.

Сегодня будет длинный пост, ещё и с кодом, так получается – ничего не могу поделать с этим.

Итак, я взял простенькую задачку с Leetcode: нужно написать одну функцию, принимающую на вход строку длинной от 1 до 1000 символом, символ – буква латинского алфавита в нижнем регистре т.е. a…z, функция должна вернуть true если все буквы встречаются в строке одинаковое количество раз, т.е. “a”, “ab”, “azzabb” – примеры входных данных на которых нужно вернуть true, а “aza”, “bzcc” – примеры на которых нужно вернуть false. Что удобно на Leetcode – можно просто написать решение нажать Run, Submit – и увидеть, работает оно или нет, и если да – то оно сравнит его скорость с другими решениями.

Задача понятная, задача простая…первым шагом нужно посчитать частоты встречающихся в слове букв и сохранить их. Вторым шагом нужно проверить, что они все одинаковы, т.е. выглядит так, что нужно написать 2 цикла, первый будет сложность O(n), где n – длина строки, второй тоже O(1) – потому, что букв в английском алфавите всего 26.

Тут зададимся вопросом, что мы, собственно говоря, можем и должны всегда стараться использовать в языке C++ - правильно, STD, а конкретно – его контейнеры и алгоритмы, тут и Leetcode нам подсказывает, изначально выдавая в наброске решения входным параметром функции типа std::string, а не бестолковый const char* времён Си. Но, что собственно может std::string для решения этой задачи – а ничего…range-based for завезли в C++ 11, это немного сократит нам код и на этом всё. Давайте перейдем к листингу:

    bool areOccurrencesEqual(string s) {
map<char, int> dict;
for (const auto& ch: s)
{
auto it = dict.find(ch);

if (dict.end() == it)
{
dict[ch] = 1;
}
else
{
++it->second;
}
}

int count = 0;

for (const auto& [_, value]: dict)
{
if (count != value && count)
{
return false;
}

if (!count)
{
count = value;
}
}

return true;
}


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

Но вот загвоздка: работает не очень быстро, всего лишь лучше 27% других решений – мы тут явно не ради такого собрались!

#IT #c_plus_plus #leetcode #today #ToBeContinued
1👍1
Я белыми, противник сдался после этого хода, до этого я чувствовал, что вообще проигрываю: он давил пешками я ходил вокруг – из-за положения его ладьи и ферзя никак не мог пролезть к королю...

#chess #flood
👍1👏1
Сравните это

Часть 2.

В прошлый раз, мы пришли с решением, работающим, но не очень быстрым. Что же можно легко и быстро поменять, в надежде ускорить его? Правильно – используемый для dict контейнер – попробуем вместо map, которая внутри себя красно-чёрное дерево, использовать хэш-таблицу, то есть unordered_map. Не буду приводить полный листинг тут – я только изменил map на unordered_map.

Этого уже оказалось достаточно , что б стать лучше 67 процентов других решений. . А нужен ли вообще тут настоящий ассоциативный контейнер? Очевидно, что нет: букв всего 26, в конкретном слове их может быть меньше, но больше – взять неоткуда. Более того, если глянуть на таблицу ASCII все эти буквы (сюрприз-сюрприз) идут подряд и представляют собой однобайтовые целые, иначе говоря, в Си/С++ я могу сделать такой вот трюк: ch-'a', и тогда для ‘a’ я получаю индекс равный нулю, для ‘b’ – одному и так далее. То есть: использовать простой массив длинной 26. Попробуем с простым “деревянным” Сишным массивом, опять меняем только объявление:

dict: int dict[26] = {0};


Уже лучше, чем 74 процента ответов. Сишный массив – некрасиво, в настоящей разработке лучше их вовсе избегать, а что же использовать там для массива – конечно, vector – излюбленная тема всех собесов по плюсам. Изменим наш тип на vector и сразу скажем ему, что будет ровно 26 элементов – дабы ничего не замедлилось на его любимых переалокациях. Не буду приводить полный листинг: поменял только объявление dict на это:
vector<int> dict(26);


Всё – “провалился” в 0мс, тут оставим вопрос, а что, если я нажму Submit несколько раз с тем же самым решением – кому интересно попробуйте сами.

Тут, для тех немногих, кто дочитал до этого момента, у меня сюрприз: идея поста родилась у меня позавчера, когда я читал документацию языку по Elixir, собственно, на первой странице там вот такой код:
ex> "Elixir" |> String.graphemes() |> Enum.frequencies()
%{"E" => 1, "i" => 2, "l" => 1, "r" => 1, "x" => 1}


И захотелось мне, сравнить его с тем, как это пишется на C++, собственно задачу я нагуглил под условия…Да, этот кусочек на Elixir делает не совсем то, что в задаче, вот эквивалентный код:
 def are_occurrences_equal(s) do
s |> String.graphemes() |> Enum.frequencies()
|> Map.values() |> Enum.uniq() |> Enum.count() == 1
end

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

#IT #c_plus_plus #Elixir #leetcode
Я белыми, в ходе размена пешек и легких фигур в центре поля, я смог поставить ферзя на одну диагональ со слоном: оппонент, пропустил этот момент, поставив под угрозу моего коня - как итог, ход ферзём на А7 с шахом, и мат следующим ходом

#chess #flood
Сложите это, пожалуйста

Написав эту заметку про типы с плавающей точкой, вспомнил, что был и другой интересный случай с этими числами, правда, из практики собеседований, но не суть. Вопрос звучал примерно так: дан массив чисел типа double, как лучше его отсортировать (по возрастающей или убывающей), чтобы просуммировать его с большой точностью.

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

Начну издалека: когда нас в детстве учат устному счёту 1, 2, 3…нас учат натуральным числам N и их свойствам - числам, которыми я могу посчитать количество предметов, далее, “добавляя” к ним 0 и отрицательные числа, мы получаем Z - целые числа, “присоединяя” к ним обыкновенные дроби мы получаем множество Q – множество рациональных чисел , наконец, решая квадратные и не только уравнения и рассматривая другие конструкции, мы получаем множество вещественных чисел R (примеры, корень из двух, экспонента и так далее). Эту цепочку можно было бы продолжить: комплексные числа, дуальные, кватернионы, а в вещественных выделить алгебраические и трансцендентные…Но я не буду об этом говорить, потому что, они все не имеют отношения к тем числам, что мы имеем в C++ как базовый типы. Об этом немножечко позже.

Непосредственно с числами, даже каким-то образом со способом их определения, связанны основные арифметические операции: сложение, вычитание, умножение, деление. На эту заметку нам хватит сложения: даже эти операции не так уж элементарны…особенно деление. Бинарная операция сложения (т.е. принимающая два аргумента) для множеств N, Z, Q, R (на самом деле и все остальные как я помню, но это не важно) обладает свойствами коммутативности и ассоциативности. Коммутативность — значит всего лишь, что a+b = b+a, совсем просто, а вот ассоциативность, посложнее: a + (b+c) = (a+b) + c. Иными словами, операция у нас бинарная, мы умеет сложить 2 числа, а вот если написать, что мы складываем 3 числа, то можно сначала сложить первые 2, или последние 2, но результат будет одинаков. Если вы думаете, что это какая-то глупость и так всегда, то посмотрите на вычитание: (1 – 2) – 3 = -4, но 1 – (2 – 3) = 0 (вычитание не ассоциативно). На самом деле Z – это кольцо, Q, R – это поле, но мы с этим не будем работать.

В C++ (и во многих остальных ЯП), есть различные операторы, конечно, включая и арифметические, не буду тут давать формального определения оператора в С++, понятно, что это некоторая синтаксическая сущность, тут важно, что для всех операторов стандарт языка предоставляет информацию об их ассоциативности, а именно, либо слева направо, либо справа налево. Большинство слева направо, но нас интересует только operator +.

Прошу ещё раз заметить: в C++ a+b+c это (a+b) + c, то есть в математике, скобки я могу ставить, как угодно, а компилятор, видя выражение без скобок, неявно поставит их таким образом, то есть, на самом деле, нет там никаких скобок, а есть ассемблерные инструкции (картинки с https://godbolt.org/ будут под постами)

#IT #job #c_plus_plus #math #weird #interview
Продолжение

Теперь вернемся, к тому какие встроенные типы для чисел в C++ мы имеем – integra и floating point…Первые мы пропускаем, насчёт вторых зададимся вопросом, вот в верху я представил множество различных чисел из алгебры – к чему относятся floating points? Правильный ответ: float и double способны представить некоторые рациональные числа. На этом всё, да, никаких настоящих вещественных чисел они не могут представить, корень из двух будет аппроксимирован некоторым рациональным приближением и так далее. Но, выше я писал, что рациональные числа обладают коммутативностью и ассоциативностью по сложению? Да…но… Уже случай из предыдущего поста, явно не ложится в наше пониманием рациональных чисел, демонстрируя, некоторое “поглощение”. Сложение чисел с плавающей точкой имеет свойства отличные от общеалгебраических, в результат их представления и реализации арифметических операций над ними. При этом, CPU, как и целочисленными значениями, складывает по 2 числа с плавающей точкой за одну инструкцию, было бы странно, если б мы сломали коммутативность: это был бы очевидный баг.

Я утверждаю следующее: можно найти тройку чисел с плавающей точкой (одновременно все float или double), такие что (a+b) +c не равно a + (b +c).
И сейчас я объясню, как я собираюсь их искать. Опять апеллирую к примеру с поглощением малого числа из прошлого поста, идея сводится к тому, что для начала мне нужно найти пару чисел, “большое” число a, и “малое” число b. Причём, число a “поглощает” число b, но число 2*a уже не будет поглощаться… Ещё раз: поскольку это поглощение основано на том, что при сложении числа выравниваются (представляются с одинаковыми экспонентами), малое число должно быть настолько малым, что б выровняться за правый край мантиссы. А если я умножу его на 2 (сдвину на разряд влево) оно учтётся пройдёт по последнему разряду мантиссы и учтётся.

Так же, я хочу числа с простым бинарным представлением, число a=1, число b…подсмотрим сюда, мантисса у нас 23 бита, т .е. я предполагаю, что нужно сдвинуть единицу направо на 24 разряда, иначе говоря мантисса у нас опять 1, а экспонента -24 (минус 24). Да нам удобнее для числа b использовать так называемую научную нотацию, да ещё и наполовину в 16ричной системе счисления…
Вот листинг, берем float т.к. он короче, и берем format т. к. он обещает “умный вывод”, с нулями только если они имеют смысл, и собираем всё с C++20:

#include <iostream>
#include <format>

int main()
{
float a = 1.0;
float b = 0x1.p-24;

std::cout << std::format("{}", a+b) << std::endl;
std::cout << std::format("{}", (a + b) + b) << std::endl;
std::cout << std::format("{}", a + (b + b)) << std::endl;

return 0;
}


Вывод будет:
1
1
1.0000001

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

#IT #job #c_plus_plus #math #weird #interview
Целочисленное сложение 3 чисел
#IT #job #c_plus_plus #asm
Сложение 3 double
#IT #c_plus_plus #job #asm
Не надо плавать

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

Тут прежде, чем изливать тонны кода, давайте остановимся на основах: почему это возможно и как это сделать. Итак, язык нам предоставляет базовые integral types, обычно 1,2,4, 8 байтовые со знаком или без (ну уж точно на x86-64 платформе это так) и предоставляет нам арифметические операторы для этих типов. Рассмотрим базовые моменты работы арифметических операторов на примере типов uint8_t, int8_t:
• Сложение для uint8_t происходит по модулю 256 т.е. 1+2 = 3, но 1+255=0
• Сложение знаковых чисел происходит той же самой инструкций ассемблера, т.е. для CPU нет никаких отрицательных чисел, границы этого типа -128…+127
• Перемножение двух int8_t дает результат типа int16_t
• Частное от деления int16_t \ int8_t дает результат типа int8_t, остаток отбрасывается

А теперь основная идея (или трюк, если хотите), я могу взять переменную типа int8_t и мысленно сказать, что она хранит не целое число…а например, число четвертей, то есть мысленно поставить точку после двух левых разрядов. Давайте на примере десятичной системы: я возьму какие-то числа, например 3.14 и 2.71 и скажу – “у меня есть 314 сотых, а также 271 сотая”. Если меня попросят их сложить или вычесть это можно сделать, не выходя за нотацию сотых прямо сразу: 314 + 271 = 585, 314 – 271 = 43. Давайте попробуем умножить, 314*271 = 85094 эм...это точно в сотых? Нет, это точно не в них – правильно будет вот так 851 т.е. сдвинуть на 2 разряда вправо. А причём тут сотые, и какие-то четверти, которые я упоминал ранее? Четверти – это одни четвертые (богатый русский язык), для двоичной системы, это тоже самое, что сотые для десятичной.

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

#IT #job #c_plus_plus #math #weird #ToBeContinued
👍1🔥1
Я белыми, чаще всего бывает, что противник протупливает фигуру как здесь, и потом получается поставить мат

#flood #chess
Две странных победы: оппоненты, в обоих случаях, сами довольно осознанно сдали ладью – там не было "вилки", можно было спокойно уйти и так далее, но через несколько ходов после этого они сдались. Там где я белыми, вероятно был план "пролезть" ферзём за рокировку, но что-то пошло не так

#flood #today #chess
Не надо плавать

Часть 2

Поскольку код вышел на Хабре, и более-того, комментаторы и я сам нашли довольно большое количество ошибок и переусложненных моментов, хотелось бы тут остановиться ещё раз на идее.
В языке C++ есть типы double и float, реализованные в соответствии со стандартом IEEE-754.

Знаменитая картинка из Википедии говорит о том, что мы храним отдельно знак, степень 2ки (экспоненту), и мантиссу – считай наши двоичные цифры. Тут хочется напомнить, что целые либо беззнаковые числа хранятся абсолютно иначе: нет ни отдельно знака, ни степени, по сути, хранится только само число.

Смысл статьи на Хабре и потуг предыдущего поста состоит в том, что можно взять целое число произвольного размера (но из тех, что нативно поддерживается компилятором) и сказать, что теперь я трактую n правых разрядов как значение идущее после точки – дробную часть числа, а остальные разряды как левую часть числа, вот пример для 8 битного числа где 2 младших разряда я отдаю под дробную часть 0000 1101, это тоже самое что 11.01 (2) = 3.25 (10) .

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

#today #IT #c_plus_plus #math #habr #ToBeContinued
👍1