Русский
Русский
English
Статистика
Реклама

Программирование

Перевод Как попасть в состояние потока?

18.06.2021 20:15:41 | Автор: admin
Для меня попадание в состояние потока является единственным способом продуктивной работы над сложными программными проектами. И я полагаю, что разработчик может так организовать свою жизнь, чтобы как можно сильнее удлинить время, которое он каждый день может проводить в этом состоянии. Тут я хочу рассказать о том, что лично я пытаюсь делать для того, чтобы чаще попадать в состояние потока.



Сон


image

Сон это самый важный фактор среди тех, что влияют на мою продуктивность. Если бы мне пришлось выбирать между хорошим ночным отдыхом и другими пунктами моего списка я выбрал бы сон. У меня есть жёсткое правило не пить кофе после 16:00. Если я делаю себе кофе в 15:45, а потом забываю о нём до 16:05, я убираю его в холодильник и оставляю на следующий день. Незначительный рост эффективности вечерней работы не стоит серьёзного ухудшения моей продуктивности на следующий день.

В те дни, когда я перевозбуждён, мне обычно помогает успокоиться приём 0,3-0,9 мг мелатонина. Я, кроме того, считаю, что спать лучше в прохладной комнате. Мне кажется, что оптимальным вариантом является температура в 16-18C.

Кофе


image

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

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

Качество воздуха


image

Можно очень сильно улучшить продуктивность, понизив уровень CO2 в воздухе рабочей комнаты. Под очень сильным улучшением я понимаю что-то в районе 20% или больше. Было одно исследование, где изучали воздействие уровня CO2 на когнитивную деятельность людей. Одна группа работала в помещении с уровнем CO2 в 1000 ppm, вторая с уровнем CO2 в 600 ppm. Для справки на открытом воздухе уровень CO2 составляет 400 ppm. Люди, работавшие в комнате с более высокой концентрацией CO2, набрали в когнитивных тестах примерно на 20% меньше баллов, чем люди, которые дышали воздухом с более низким содержанием CO2.

Я, пока не обзавёлся монитором качества воздуха, считал, что уровень CO2 в моей рабочей комнате достаточно низок, так как я часто проветривал эту комнату. А оказалось, что уровень CO2 в моей комнате составляет примерно 1200 ppm, что даже выше, чем в неблагополучной комнате вышеописанного исследования! Для того чтобы это исправить, я почти всегда, когда работаю, держу окно приоткрытым. Это позволяет поддерживать уровень CO2 в моей комнате примерно на отметке в 450 ppm.

Физические упражнения


image

Я не очень спортивный человек. Правда. Каждый раз, когда я устраиваю тренировку, не занимавшись несколько дней, я думаю: Замечательные ощущения. Почему я не занимаюсь этим чаще?.

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

Работа в одиночестве


image

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

Удаление игр


image

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

Блокировка сайтов


image

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

Составление плана работ на день


image

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

Музыка


image

Я полагаю, что идеальный музыкальный жанр, который позволяет мне длительное время не терять сосредоточения, это Deep House. Вот один из моих любимых миксов (живое выступление на воздушном шаре!). Ещё я пользуюсь наушниками с шумоподавлением. Без них я просто не могу сосредоточиться. Даже если я работаю в тихой комнате, мне очень нравится невероятная тишина, которую обеспечивают такие наушники.

Питание


image

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

Выбор правильной задачи


image

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

Как вы относитесь к состоянию потока?


Подробнее..

Перевод Практический взгляд на Raspberry Pi Pico с точки зрения STM32

19.06.2021 14:07:57 | Автор: admin
Сравнительно недавно Raspberry Pi Foundation выпустила плату Raspberry Pi Pico, основанную на микроконтроллере (Micro Controller Unit, MCU) RP2040. Эта плата привлекла большое внимание членов сообщества разработчиков различных электронных устройств. Появилось довольно много проектов, в которых используются программируемые модули ввода-вывода (Programmable I/O, PIO) Raspberry Pi Pico. Например, это проект PicoDVI, в котором конечные автоматы PIO используются для вывода DVI-сигнала.



Но с появлением Raspberry Pi Pico связано не только радостное возбуждение разработчиков электроники. Это событие заставило сообщество задаться важным вопросом о том, окажет ли появление платы какое-то ощутимое влияние на тех, кто пользуется STM32, SAM и другими микроконтроллерами, основанными на Cortex-M. Станет ли микроконтроллер RP2040 жизнеспособным выбором для некоторых из проектов, в которых используются похожие MCU? Учитывая то, что в состав RP2040 входит двухъядерный процессор ARM Cortex-M0+, кажется справедливой идея использования этого микроконтроллера там же, где применяются 32-битные MCU от ведущих производителей компонентов такого рода, в частности, от STMicroelectronics.

Сможет ли небольшой проект Raspberry Pi Foundation показать инженерам STM как надо делать микроконтроллеры, или создателям платы на RP2040 стоит пересмотреть некоторые из своих гипотез? Сложно ли будет портировать на RP2040 низкоуровневый код, рассчитанный на STM32?

Сложно ли перенести STM32-проект на RP2040?


Короче говоря, когда я обратила внимание на RP2040, мне подумалось, что будет интересно попытаться портировать на новый микроконтроллер мой C++-фреймворк для STM32. Правда, эта идея меня заинтересовала не из-за двухъядерного ARM Cortex-M0+. У меня есть двухъядерные микроконтроллеры STM32H7 (M4 и M7), которые, за счёт более совершенных характеристик, легко обойдут RP2040. Сильнее всего меня заинтриговали программируемые модули ввода-вывода RP2040, возникало такое ощущение, что они достойны того, чтобы познакомиться с ними поближе.


Плата Raspberry Pi Pico, основанная на RP2040 подключена к одноплатному компьютеру Raspberry Pi, играющему роль SWD-адаптера (оригинал)

Основываясь на опыте работы с STM32 я поняла, что смогу быстро портировать некоторые файлы, создав в репозитории проекта ветку RP, рассчитанную на другую архитектуру, и принявшись за дело. Ведь и в основном проекте, и в новой ветке код будет рассчитан на Cortex-M. Обычно работа с новым ARM-микроконтроллером заключается в том, чтобы найти даташит, справочное руководство и CMSIS-файлы для соответствующего устройства. А потом существующий низкоуровневый код можно легко адаптировать под новый подход к именованию периферийных устройств и под новую схему регистров, учитывая то, что фундаментальные компоненты нового и старого микроконтроллеров (SysTick, NVIC и так далее) ничем не отличаются.

Может, я поступила слишком опрометчиво, но я заказала плату Raspberry Pi Pico, даже не поинтересовавшись тем, есть ли для неё CMSIS-файлы, и даже не взглянув в справочное руководство по ней. Позже я, к своему удивлению, выяснила, что пока нет даже и речи о наличии CMSIS-файлов для Raspberry Pi Pico, или хотя бы о возможности взаимодействия RP2040 с другими устройствами из экосистемы Cortex-M. Но при этом SVD-файл для MCU RP2040 имеется в Pico SDK, а на основе этого файла можно создать заголовочный файл для устройства. Благодаря проекту cmsis-pi-pico в моём распоряжении, в итоге, оказалось рабочее решение.

Решение моей задачи можно было бы и упростить


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


Последовательность загрузки RP2040 (даташит RP2040, рисунок 15) (оригинал)

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

Первая сложность, которую нужно было преодолеть для того чтобы научиться работать с RP2040, заключалась в понимании особенностей цепочечного процесса загрузки микроконтроллера. Тут всё очень похоже на то, как в прошлом, на обычных компьютерах, была организована загрузка с дискет, или то, как устроена загрузка с HDD/SSD. А именно внешняя QSPI Flash ROM рассматривается MCU лишь как устройство, которое, возможно, содержит загрузочные данные. Загрузчик первой фазы загрузки интегрирован в MCU и располагается в ROM по адресу 0x0000 0000. Он обращается к интерфейсу QSPI и пытается загрузить из него 256 байт данных. Потом будет проверен CRC32-хеш этих данных. Если проверка пройдёт успешно, они будут признаны загрузчиком второй фазы загрузки.

Загрузчик второй фазы может решать множество задач, при этом некоторые задачи он должен решать в обязательном порядке. Этот процесс, реализованный в RP2040, если сравнить его с аналогичным процессом в некоторых знаменитых клонах STM32, тоже имеющих SPI ROM (вроде отличных клонов компании GigaDevice), не так понятен, не так хорошо документирован, не так прозрачен, как мог бы быть. В нём много такого, в чём можно запутаться.

Говорят, что хорошие художники копируют


У меня ушло достаточно много времени на то, чтобы понять, как подход к управлению тактированием периферийных устройств, принятый в STM32, соотносится с системной архитектурой RP2040. Я внимательно читала даташит RP2040 и всех вокруг спрашивала об этом. Как оказалось, RP2040-версия системы управления тактированием периферии называется RESETS. Эта система является полной противоположностью той, что применяется в STM32. А именно, нужно установить условие сброса периферийного блока в 0 для того чтобы включить его тактирование. Так, чтобы включить тактирование GPIO, нужно переключить бит 8 в RESETS_RESET (PADS_BANK0).


Функциональная схема GPIO-пина RP2040 (оригинал)

Когда я это поняла, я посмотрела раздел документации по GPIO-периферии (раздел 2.19). И кое-что тут же бросилось мне в глаза. А именно, то, что я там увидела, совершенно не похоже на то, как устроена практически вся GPIO-периферия, с которой я когда-либо сталкивалась. В частности, речь идёт о периферии STM32, AVR и SAM.

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

А теперь, когда я через всё это прошла, полагаю, можно будет просто переписать мой код, после чего он заработает на RP2040?

Причуды загрузки


Как уже было сказано, загрузчик второй фазы загрузки должен быть расположен в начале образа прошивки. Я считала, что это должен быть какой-то достаточно стандартный код, поэтому просто взяла готовый ASM-код, который выдал официальный Pico SDK, и использовала его при сборке примера Blinky. Добавив этот код к RP2040-порту моего проекта Nodate, я смогла без проблем собрать Blinky.

Запись результирующего ELF-бинарника в RP2040 стала ещё одним приключением. Дело в том, что на плате Raspberry Pi Pico нет встроенного SWD-адаптера, чего-то в духе ST-Link. А микроконтроллеру на двухъядерном Cortex-M нужен могоканальный SWD-адаптер. Единственным подобным устройством, которое было у меня под рукой, оказался адаптер, интегрированный в плату Nucleo-STM32H7. Поэтому я решила использовать кастомный форк OpenOCD, созданный Raspberry Pi Foundation. Его я запустила на Raspberry Pi.

После столь основательной подготовки мне удалось успешно прошить RP2040, но ничего не заработало. Беглая проверка вызвала у меня такое ощущение, что в ходе загрузки мне не удалось выйти за пределы исходного загрузчика и добраться до прошивки, находящейся в SPI ROM. Сейчас мне сложно дать ответ о причинах происходящего. Это могла быть проблема с ASM-кодом второй фазы загрузки, это могла быть ошибка в экспериментальных CMSIS-файлах RP2040, которые создавала не я. Это могло быть и что-то совершенно иное.

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



Raspberry Pi Pico (оригинал)

После того, как я потратила много часов на то, чтобы завести RP2040 с использованием CMSIS-файлов и файлов загрузчика второй фазы загрузки, мне кажется, что можно немного отстраниться от ситуации и переоценить происходящее. А именно, с того момента, когда начинала формироваться моя точка зрения на Raspberry Pi Pico, в запросе по поводу CMSIS-файлов появились сведения о том, что официальные CMSIS-файлы, возможно, появятся в Pico SDK 1.2.0. Это довольно-таки приятно.

Полагаю, любому, кто хочет поближе познакомиться с RP2040, пользуясь инструментами, ставшими индустриальным стандартом, имеет смысл дождаться этого релиза Pico SDK. А после того, как в моём распоряжении окажутся официальные CMSIS-файлы, я, вероятно, начну с переделывания примера Nodate Blinky, а потом попробую поработать с PIO. Перспектива создавать собственные интерфейсы кажется мне весьма привлекательной. И хотя возможности Raspberry Pi Pico не так мощны, как возможности CPLD или FPGA, они, всё равно, способны лечь в основу интереснейших проектов.

Возникает такое ощущение, что авторы даташита для RP2040 (он, скорее, похож на смесь справочного руководства и даташита) иногда забывают о том, что в нём должно быть описание микроконтроллера, а не чего-то другого. В эти моменты он превращается в учебное руководство по Pico SDK. Хотя материалы этого даташита и способны принести пользу тем, кто стремится освоить Pico SDK, тем, кто хочет написать что-то своё, пользы от него, однозначно, меньше, чем от более привычного даташита.

Полагаю, тех, кто захочет написать для Raspberry Pi Pico что-то своё, не особенно порадуют такие особенности платы, как запутанная работа с GPIO-периферией, сложный процесс загрузки, необходимость в загрузчике второй фазы загрузки, непрозрачность внешней ROM. В общем тому, кому интересна плата Raspberry Pi Pico, пока приходится ориентироваться на официальный SDK.

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

Пользовались ли вы Raspberry Pi Pico?


Подробнее..

Перевод Почему я всё ещё люблю C, но при этом терпеть не могу C?

19.06.2021 18:15:23 | Автор: admin
Мне на удивление часто приходится говорить о том, почему мне всё ещё нравится язык C, и о том, почему я плохо отношусь к C++. Поэтому я решил, что мне стоит об этом написать, а не снова и снова повторять одно и то же.



Как это обычно бывает у C-программистов, язык C не был ни моим первым языком, ни языком, после которого я уже не изучал ничего другого. Но мне всё ещё нравится этот язык, и когда мне нужно писать программы я выбираю именно его. Правда, в то же время, я стараюсь быть в курсе того, что происходит в мире современных (и не очень) языков программирования. Я слежу за тенденциями в этой сфере и пишу собственный хобби-проект, связанный с мультимедийными технологиями, на Rust. Почему же я до сих пор не поменял C на что-то более современное? И при чём тут C++?

Почему C это не самый лучший язык программирования?


Сразу скажу то, что, пожалуй, и так все знают: нет такого понятия, как самый лучший язык программирования. С каждым языком связан набор задач, для решения которых он подходит лучше всего. Например, хотя и можно заниматься трассировкой лучей в Excel, применяя VBA, лучше будет делать это с использованием более подходящего языка. Поэтому полезно знать об ограничениях языков программирования чтобы не жаловаться на то, что веб-серверы не пишут на Fortran, и на то, что почти нигде Perl или C++ не используется в роли внутренних скриптовых языков. C может считаться не очень хорошим языком по причинам, которые я перечислю ниже (это помимо того, что язык этот просто очень старый и того, что его нельзя назвать активно развивающимся языком, но это дело вкуса).

В синтаксисе C имеются неоднозначные конструкции (например, символ * может играть роль обычного оператора умножения, может применяться в виде унарного оператора разыменования, или может использоваться при объявлении указателей; а радости работы с typedef достойны отдельной статьи).

Этот язык не является безопасным. Например в C-программах довольно часто встречаются ошибки, связанные с обращением к элементам массивов, которые находятся за пределами границ массивов. В языке нет проверок на ошибки такого рода, производимых во время выполнения программы. А вот, например, в Borland Pascal, не говоря уже о более современных языках, такие проверки есть (это плюс, даже если подобные возможности можно, пользуясь параметрами компиляции, отключить ради повышения производительности). А использование в языке указателей ещё сильнее усложняет задачу программиста по поддержанию кода в хорошем состоянии. И, кроме того, C имеет и некоторые другие неприятные особенности, вроде возможности вызова функции без объявления прототипа, в результате чего функции легко можно передать аргумент неправильного типа.

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

Почему я, несмотря на все недостатки C, пользуясь именно этим языком?


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

Например, если нужно получить значение элемента массива, имея два смещения, одно из которых может быть отрицательным числом, то при программировании на C можно воспользоваться такой конструкцией: arr[off1 + off2]. А при использовании Rust это уже будет arr[((off1 as isize) + off2) as usize]. C-циклы часто короче, чем идиоматичные механизмы Rust, использование которых предусматривает комбинирование итераторов (конечно, обычными циклами можно пользоваться и в Rust, но это не приветствуется линтером, который всегда рекомендует заменять их итераторами). И, аналогично, мощными инструментами являются memset() и memmove().

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

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

При чём тут C++?


Если говорить о C++, то хочу сразу сказать, что я не отношусь к тем, кто ненавидит этот язык. Если вы этим языком пользуетесь и он вам нравится я ничего против этого не имею. Я не могу отрицать того, что C++, в сравнении с C, даёт нам два следующих преимущества. Во-первых это улучшение структуры программ (это поддержка пространств имён и классов; в Simula, в конце концов, есть хоть что-то хорошее). Во-вторых это концепция RAII (если описать это в двух словах, то речь идёт о наличии конструкторов для инициализации объектов при их создании и о наличии деструкторов для очистки ресурсов после уничтожения объектов; а если глубже разработать эту идею, то можно прийти к понятию времени жизни объекта из Rust). Но, в то же время, у C++ есть несколько особенностей, из-за которых мне этот язык очень и очень не нравится.

Это, прежде всего склонность языка постоянно вбирать в себя что-то новое. Если в каком-то другом языке появится какая-нибудь возможность, которая станет популярной, она окажется и в C++. В результате стандарт C++ пересматривают каждые пару лет, и при этом всякий раз в него добавляют новые возможности. Это привело к тому, что C++ представляет собой монструозный язык, которого никто не знает на 100%, язык, в котором одни возможности часто дублируют другие. И, кроме того, тут нет стандартного способа указания того, возможности из какой редакции C++ планируется использовать в коде. В Rust это поддерживается на уровне единицы компиляции. В IIRC C++ для той же цели предлагалась концепция эпох, но эта затея не удалась. И вот одно интересное наблюдение. Время от времени мне попадаются новости о том, что кто-то в одиночку (и за приемлемое время) написал рабочий компилятор C. Но я ни разу не встречал похожих новостей о компиляторе C++.

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

И, наконец, я мог бы вообще не обращать внимания на C++, если бы этот язык не был бы связан с C и не оказывал бы на C плохое влияние. Я не говорю о ситуации, когда, говоря о C и C++, их объединяют, упоминая как C/C++, и считая, что тот, кто знает C, знает и C++. Я имею в виду связь между языками, которая оказывает влияние и на стандарты, и на компиляторы. С одной стороны C++ основан на C, что придало C++, так сказать, хорошее начальное ускорение. А с другой стороны сейчас C++, вероятно, лучше смотрелся бы без большей части своего C-наследия. Конечно, от него пытаются избавиться, называя соответствующие конструкции устаревшими, но C-фундамент C++ никуда пока не делся. Да и будет ли популярным, скажем, некий С++24, вышедший в виде самостоятельного языка, основанного на C++21 и лишённого большей части устаревших механизмов? Не думаю.

Воздействие компиляторов C++ на C


Вышеописанная связь C и C++ приводит к тому, что к C относятся как к C++, лишённому некоторых возможностей. Печально известным примером такого восприятия C является C-компилятор Microsoft, разработчики которого не позаботились о поддержке возможностей C99 до выхода версии компилятора 2015 года (и даже тогда разработчики придерживались стратегии bug-for-bug compatibility, когда в новой реализации чего-либо воспроизводят старые известные ошибки; делалось это для того, чтобы пользователи компилятора не были бы шокированы, внезапно обнаружив, что вариадические макросы вдруг там заработали). Но тот же подход можно видеть и в стандартах, и в других компиляторах. Эти проблемы связаны друг с другом.

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

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

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

  • Поведение, определяемое архитектурой системы (то есть то, что зависит от архитектуры процессора). Сюда, в основном, входят особенности выполнения арифметических операций. Например, если я знаю, что целевая машина использует для представления чисел дополнение до двух (нет, это не CDC 6600), то почему компилятор (который, как представляется, тоже знает об особенностях целевой архитектуры) должен считать, что система будет вести себя иначе? Ведь тогда он сможет лучше выполнять некоторые теоретически возможные оптимизации. То же применимо к операциям побитового сдвига. Если я знаю о том, что в архитектуре x86 старшие биты, выходящие за пределы числа, отбрасываются, а на ARM отрицательный сдвиг влево это сдвиг вправо, почему я не могу воспользоваться этими знаниями, разрабатывая программу для конкретной архитектуры? В конце концов, то, что на разных платформах целые числа имеют разные размеры в байтах, считается вполне приемлемым. Компилятор, обнаружив нечто, рассчитанное на конкретную платформу, может просто выдать предупреждение о том, что код нельзя будет перенести на другую платформу, и позволить мне писать код так, как я его писал.
  • Неочевидные приёмы работы с указателями и каламбуры типизации. Такое ощущение, что плохое отношение к подобным вещам возникло лишь ради потенциальной возможности оптимизации кода компиляторами. Я согласен с тем, что использование memcpy() на перекрывающихся областях памяти может, в зависимости от реализации (в современных x86-реализациях копирование начинается с конца области) и от относительного расположения адресов, работать неправильно. Разумно будет воздержаться от подобного. А вот другие ограничения того же плана кажутся мне менее оправданными. Например запрещение одновременной работы с одной и той же областью памяти с использованием двух указателей разных типов. Я не могу представить себе проблему, возникновение которой может предотвратить наличие подобного ограничения (это не может быть проблема, связанная с выравниванием). Возможно, сделано это для того чтобы не мешать компилятору оптимизировать код. Кульминацией всего этого является невозможность преобразования, например, целых чисел в числа с плавающей запятой с использованием объединений. Об этом уже рассуждал Линус Торвальдс, поэтому я повторяться не буду. С моей точки зрения это делается либо для улучшения возможностей компиляторов по оптимизации кода, либо из-за того, что этого требует C++ для обеспечения работы системы отслеживания типов данных (чтобы нельзя было поместить экземпляр некоего класса в объединение, а потом извлечь его как экземпляр совсем другого класса; это тоже может как-то повлиять на оптимизации).
  • Поведение кода, определяемое реализацией (то есть поведение, которое не в точности отражает то, что предписано стандартом). Мой любимый пример подобной ситуации это вызов функции: в зависимости от соглашения о вызове функций и от реализации компилятора аргументы функций могут вычисляться в совершенно произвольном порядке. Поэтому результат вызова foo(*ptr++, *ptr++, *ptr++) неопределён и на подобную конструкцию не стоит полагаться даже в том случае, если программисту известна целевая архитектура, на которой будет выполняться код. Если аргументы передаются в регистрах (как в архитектуре AMD64) компилятор может вычислить значение для любого регистра, который покажется ему подходящим.
  • Полностью неопределённое поведение кода. Это тоже такой случай, когда сложно спорить со стандартом. Пожалуй, самый заметный пример такого поведения кода связан с нарушением правила, в соответствии с которым значение переменной в одном выражении можно менять лишь один раз. Вот как это выглядит в одном знаменитом примере: i++ + i++. А вот пример, который выглядит ещё страшнее: *ptr++ = *ptr++ + *ptr++е.

C++ это язык более высокого уровня, чем C. В то время, как в C++ имеется большинство возможностей C, использовать эти возможности не рекомендуется. Например, вместо прямого преобразования типов надо применять reinterpret_cast<>, а вместо указателей надо применять ссылки. От С++-программистов не ждут того, что они будут понимать низкоуровневый код так же хорошо, как C-программисты (это, конечно, лишь средняя температура по больнице, в реальности всё зависит от конкретного программиста). И всё же, из-за того, что существует очень много C++-программистов, и из-за того, что C и C++ часто воспринимают как C/C++, C-компиляторы часто расширяют в расчёте на поддержку ими C++ и переписывают на C++ для того чтобы упростить реализацию сложных конструкций. Это произошло с GCC, и того, кто начнёт отлаживать с помощью GDB С++-код, находящийся в .c-файлах, ждёт много интересного. Грустно то, что для того чтобы скомпилировать код C-компилятора нужен C++-компилятор, но, чему нельзя не радоваться, всё ещё существуют чистые C-компиляторы, вроде LCC, PCC и TCC.

Итоги


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

Но, по крайней мере, нельзя заменить C90 на какой-нибудь C90 Special Edition и сделать вид, что C90 никогда не существовало.

Как вы относитесь к языку C?


Подробнее..

Пятьдесят лет на стезе программирования. Часть I. Начало пути. Отчий дом и Казанское суворовское военное училище

21.06.2021 16:11:25 | Автор: admin
Логотип статьи определяет три, как временные, так и географические, точки на моём жизненном пути, через которые лежал мой путь в страну под названием Программирование. В городе Чебоксары, на родине легендарного комдива Гражданской войны В.И.Чапаева, прошло моё детство (1954-1968 г.г.), там я закончил 8 классов средней школы 6. В 1968 году я переместился в следующую географическую точку, в г. Казань, в Казанское суворовское военное училище (КзСВУ). После окончания КзСВУ в 1971 году мой путь лежал в столицу нашей Родины в Москву, в Военную орденов Ленина, Октябрьской Революции и Суворова Академию им. Ф.Э.Дзержинского (сокращённое название ВА им. Ф.Э.Дзержинского или ВАД), которой в 2020 году исполнилось 200 лет со дня ее основания. И 22 июня в трагический для нашей страны день в 1941 году и знаковый для меня в 1976 году я окончил ВА им. Ф.Э.Дзержинского и получил диплом по специальности Программирование с присвоением квалификации военного инженера программиста:



Это случилось 45 лет назад. Но на стезю программирования я вступил ещё раньше, а именно, в 1971 году, когда после окончания КзСВУ и приехал в столицу нашей Родины поступать в ВАД. А это было ровно пятьдесят лет назад. И вот именно о том, как я попал на эту стезю и как шёл и иду по ней, и будет мое повествование. И посвящено оно моим родителям Орлову Николаю Егоровичу и Орловой (в девичестве Кулыгиной) Зое Федоровне:



I. Отчий дом


Мой выбор стези Программирования, я думаю, был не случаен. Он был обусловлен всем моим счастливым детством.
Детство моё прошло в небольшом провинциальном городе Чебоксары, населения в нём тогда было менее 100 000 человек. Семья наша была многодетной. У отца с матерью нас было четыре брата, слева направо, Гена, Юра, Вова, Серёжа:



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



Именно из этого дома ушёл на войну мой дед Орлов Егор Михайлович и погиб под Москвой:



Мама росла без отца, а когда началась война, то на нее ушли и там погибли три её родных брата Борис, Владимир, Александр, мои дядья. И если бы не эти жертвы, то неизвестно как сложилась бы наша жизнь (если бы ещё она была).
Мои родители в Великую Отечественную войну, как сейчас принято говорить, сражались на трудовом фронте: отец работал в колхозе, а маму волей судеб занесло в Пензу на военный завод, где она стала стахановкой, чем очень гордилась.
В Чебоксары наша семья перебралась в 1954 году с Урала, где мать с отцом работали после войны на лесозаготовке и где родился я (на фотографии я в центре), а по дороге в г. Чебоксары и мой брат Гена:



Себя я помню с 3-4 лет. Особенно хорошо запомнил наши поездки в Москву в 1957 и 1958 годах. Самые яркие воспоминания посещение Мавзолея Ленина-Сталина, когда приходилось стоять в огромных очередях, берущих начало в Александровском саду, и тележки с горячими сосисками.
Первым нашим жильём в Чебоксарах была комната в бараке. Её дали маме от строящегося Чебоксарского завода тракторных запасных частей (сегодня это Агрегатный завод), куда она устроилась работать в горячий цех термистом.
Барак это обычно одноэтажное здание, чаще деревянное, с проходом во всю длину и разделённое перегородками на комнаты. Комнаты были по 12-16 квадратных метров. Туалет обычно находился во дворе, отопление было печным, а кухня общая. Вода в колонке на улице. К сожалению, мне не удалось найти фотографию именно нашего барака, но я нашел похожий:



После Великой Отечественной войны размер разрушений на территории СССР был колоссален, лишилось крова более 30 млн. человек, примерно, каждый седьмой по стране. Чернигов, Севастополь, Великие Луки, Белгород были разрушены полностью. В Сталинграде, Минске, Курске, Новгороде и многих других городах было разрушено более 90% зданий. В землянках оказались миллионы наших граждан. Строительство бараков было вынужденной и временной мерой как по расселению оставшихся без крова людей, так и по обеспечению жильем людей, прибывающих на строительство новых заводов. Так было и в Чебоксарах при строительстве Чебоксарского завода тракторных запасных частей.
Но если фотография нашего барака не сохранилась, то фотография нашей комнаты в этом бараке (и не одна) сохранилась:



Это где-то 1956-57 года. Посмотрите, какие счастливые лица у людей. На фотографии слева направо мой отец, из его подбородка выглядывает автор этой статьи, затем моя мама. Встреча состоялась по случаю приезда из Саратова брата отца с женой (пара в центре) и прихода в гости сестры мамы с мужем. Последние жили в Чебоксарах в своём доме на ул. Краснофлотской. Её сейчас нет, на её месте стоит Олимпийский стадион.
Как я уже сказал, бараки были временной мерой и уже в 1957 году мы переехали в более комфортабельное жильё, в коммунальную квартиру на Школьном проезде дом 4:



В СССР коммунальные квартиры, общежития и временные бараки были одним из основных типов жилья рабочих до начала массового жилищного строительства отдельных квартир в 1960-х, до начала строительства знаменитых хрущёвок.
Знаменитые стройки 70-х годов, такие как Нижнекамский и Чебоксарский химические комбинаты, КАМАЗ, Чебоксарская ГЭС, Чебоксарский завод промышленных тракторов (ЧЗПТ), уже возводились без бараков. Вместе с этими индустриальными гигантами вырастали современные города Нижнекамск, Набережные Челны, Новочебоксарск, а в Чебоксарах вырос Новоюжный район:



Но вернёмся в коммунальную квартиру. В коммунальной квартире уже были туалет, ванная, центральное отопление. Если мне не изменяет память, то в нашей квартире было четыре комнаты, в каждой из которых жила отдельная семья.
А поскольку родители работали, то мы часто дома оставались одни. Дверь в нашу комнату, естественно, всегда была открыта.
Однажды мы остались с братом одни и зашли с ним на общую кухню, где у каждой семьи был свой стол. Там нашли коробок со спичками. Кто-то из нас сказал, что они не настоящие и мы решили это проверить. Результат был плачевным, вспыхнула скатерть на одном из столов. Мы убежали с кухни в свою комнату. На наше счастье в одной из комнат дома была чья-то бабушка, которая одеялом потушила начинающийся пожар. С тех пор я на всю жизнь запомнил, что огонь можно потушить, накрыв источник огня, например, одеялом, и самое главное, что игры с огнём могут закончиться печально. Именно поэтому в те времени так был популярны плакаты на тему спичек и детей.
Отсюда я в 1960 году пошел в первый класс в школу 28, которая находилась на этой же улице рядом с домом. Здание школы сохранилось, но школы там нет:



Первый класс запомнился двумя знаменательными событиями. Первое, это полное солнечное затмение, когда нас вывели на улицу и мы первоклашки наблюдали как день становится ночью. А весной 1961 года я провалился под лёд в котлован, который находился во дворе школы. Надо сказать, что когда я пошёл в школу, то не умел ни читать, ни писать. Но этот недостаток скоро был устранён и чтение стало любимым моим занятием, особенно с фонариком под одеялом. Надо сказать, что вся наша семья много читала. У нас была хорошая библиотека. Я, например, зачитывался Карелом Чапеком (Средство Макропулоса, Война с саламандрами и др.) и, как ни странно, Емельяном Ярославским, его повествованием о Древнем Египте. Именно поэтому, наверное, у меня и появилась тяга к истории.
Как нам жилось в коммунальной квартире? Отвечу: хорошо. Вглядитесь в эти лица:



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



В первом ряду на этой фотографии два паренька в школьных фуражках это я (справа) и мой брат Гена.
В начале 70-х годов прошлого столетия мне довелось побывать в нашей коммунальной квартире. Только это уже была отдельная (я бы сказал шикарная) четырёхкомнатная квартира, в которой жила одна семья, наши соседи по коммуналке. Все остальные соседи получили бесплатно от государства отдельные квартиры и переехали в них.
Настал 1961 год, год первого полёта человека в космос. Эти человеком был гражданин Советского Союза Ю.А.Гагарин:



В этом же году наша семья в составе пяти человек (в 1960 году у меня появился второй брат Сергей) переезжает в отдельную со всеми удобствами квартиру по ул. Ярославская (сегодня ул. Энгельса, д.12, на фотографии первый дом справа):



Наша квартира была на шестом этаже (лифта не было) и как мы этим гордились. В те годы в Чебоксарах мало кто так высоко жил. Но самое главное было то, что весь первый этаж был отдан под Станцию Юных Техников (СЮТ). Сразу скажу, что сегодня этой станции там нет, и все помещения занимают коммерческие предприятия. И вот в один прекрасный день, уже учась во втором классе, я открыл дверь, переступил порог СЮТ и попал в волшебный мир. Меня приветливо встретили и провели по всей станции. Ребята (правда, они были постарше меня, я учился во втором классе) собирали модели кораблей и самолетов, строили планеры, печатали фотографии, а кто-то показывал фильмы. Глаза разбежались. В итоге я записался практически во все кружки: фото, авиа- и судомодельный. Записался и на курсы киномеханика. На курсах киномехаников нас учили крутить фильмы на кинопроекторе Украина.
Всё было абсолютно бесплатно. Как я успевал? Мне повезло, что СЮТ была в доме, в котором я жил, и школа 16 была рядом, в двух минутах ходьбы.
Я до сих пор с гордостью рассказываю, что уже во втором классе получил первое удостоверение киномеханика. Был случай, когда в клубе оказалось некому показать фильм Степан Разин. И тут моё удостоверение пригодилось. Пригодилось оно и в дальнейшем, когда в школе необходимо было демонстрировать учебный материал на Украине (видеомагнитофонов и компьютеров тогда не было).
А как народ сбегался смотреть, когда мы запускали в небо планеры, а тем более кордовые модели!
СЮТ была хорошей школой, она мне многое дала. Стацию юных техников я прекратил посещать после того, как мы переехали на новое местожительство.
А еще мне посчастливилось встречать в Чебоксарах в 1962 году Космонавта-3 А.Г.Николаева:



Мы с отцом в этот момент были на крыше дома слева. Надо было видеть, что творилось в городе. Посчастливилось мне встречать в Чебоксарах и первую женщину космонавта Валентину Терешкову:



В момент следования кортежа я находился в первом ряду около детского мира (на фотографии второй дом справа от левого верхнего угла) и один из мотоциклов своим колесом проехал по моему ботинку. Вот такие воспоминания. Жизнь просто бурлила.
Мой отец всю жизнь гордился, что тоже был причастен к полёту А.Г.Николаева в космос. Где-то за месяц до полёта бригаду плотников, в которой работал и мой отец, отправили в командировку (они такого слова не знали) в глухую чувашскую деревню.
Оказалось, надо срочно построить дома для простой чувашской старушки. Никто ничего не понимал, но дом был построен, а вскоре и секрет открылся, это был дом для матери Космонавта-3.
В 1961 году у меня появился еще один брат, третий Юра.
А весной 1964 года мною была предпринята первая попытка пойти учиться в суворовское военное училище. Как я уже говорил, я много читал, в том числе и про суворовцев. В те времена военно-патриотическое воспитание было на высоте. Я знал, что в суворовские военные училища (СВУ) берут после четвертого класса. Со мной в классе учился мальчик, чей отец был летчиком-испытателем и погиб при испытании самолёта. Мы с ним дружили и хотели вместе идти в СВУ. Но именно в 1964 году было принято решение о приёме в СВУ только после восьмого класса. Наша детская мечта была отложена на целых четыре года.
В этом же 1964 году мои родители получили трехкомнатную квартиру на улице В.И.Чапаева, д.11. Эта улица знаменита тем, что стоит на месте деревни Будайки, в которой родился В.И.Чапаев знаменитый комдив времен Гражданской войны. На том месте, где стоял дом семьи Чапаевых, сегодня стоит такой монумент:



Надпись на мемориальной доске гласит:
Здесь стоял дом, в котором 9 февраля 1887 года родился В.И.Чапаев

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



Они часто приходили к нам в дом, порой с ночёвкой. Именно они подарили мне шахматы и, самое главное, привили любовь к ним. Каждый их приход к нам начинался с игры в шахматы и не просто игры, а игры с разбором. И вот наступил момент, когда Николай мне сказал:
Всё, больше я тебе ничего здесь дать не могу. Ты превзошёл своего учителя
Кстати, всех сестер отца и младшего брата я звал по именам. Таково было их пожелания. Меня они всегда считали за равного. Это здорово.
А в начале 1968 году в Будайках, недалеко от нашего дома, был открыт Дом Спорта Спартак (его можно увидеть на фотографии ниже). И вот как-то, проходя мимо него, я увидел на доске объявлений, что проводится запись для участия в квалификационном шахматном турнире II разряда. Турнир организовывал и проводил международный мастер по шахматам В.Д. Сергиевский. Имя Сергиевского в те годы гремело в Чебоксарах и я следил за его успехами. А успехи у него были. В 1966 году он стал чемпионом России по шахматам, занял шестое место на мемориале М.И.Чигорина, выиграв в турнире и у будущего чемпиона мира Б.В.Спасского. Именно на мемориале М.И.Чигорина Сергиевский выполнил норматив международного мастера по шахматам.
Когда я увидел фамилию Сергиевский, сомнений у меня не осталосьи я вошёл в Дом Спорта записываться на турнир. Турнир я провёл хорошо, занял второе место. Победитель турнира, юноша старше меня года на 2-3, был значительно сильнее меня. Но надо иметь в виду, что я всё же был самоучкой. По условиям турнира за первое место давали II разряд, а за второе III разряд. Итак, я стал третьеразрядником по шахматам и, как окажется чуть позже, будет иметь свои последствия. По ходу турнира был курьёзный случай. Одну из партий я играл с мальчиком, который был младше меня года на четыре, но он занимался у самого Сергиевского и подавал большие надежды. До встречи со мной он у всех выигрывал, но мне проиграл. И тут случилось непредвиденное, он расплакался навзрыд, его никто не мог успокоить. В итоге ко мне подошёл сам Сергиевский и попросил меня переиграть партию. Для меня это было очень неожиданно: в шахматах переиграть партию Но Сергиевский нашёл какие-то слова и я согласился. Я думаю, Сергиевский всё же пожалел о своём поступке, паренёк снова проиграл. Я не помню сейчас, на каком месте он закончил турнир, но я своим вторым местом горжусь.
Отец мне с раннего детства привил любовь к периодической печати, к газетам и журналам. Родители мне выписали сначала газету Пионерская правда, где печаталась книга А.Волкова Урфин Джюс и его деревянные солдаты. О, как я ждал прихода газеты, чтобы прочитать продолжение!
Потом я их уговорил выписать журналы Юный техник, Техника молодёжи.
В пятом классе я увлёкся радиоделом и пришла очередь журнала Радио.
Откуда пошла тяга к радиоделу или, как сейчас сказали бы, к электронике, я не помню, но увлечение было серьёзное. Сначала детекторный приёмник, потом приёмники прямого усиления, потом супергетеродинный радиоприёмник да ещё с приёмом коротких волн. Вместо корпуса мыльница. Апофеозом стал магнитофон, где самое трудное было собрать лентопротяжный механизм, и миниатюрный телевизор. Последнее осталось незаконченным, хотя была разработана схема и изготовлена печатная плата. Проблема была в отсутствии кинескопа, электронно-лучевой трубки малого размера. Но когда я дома отремонтировал телевизор, мой авторитет в глазах родителей вырос до небес.
Вершиной нашего радиолюбительства я считаю создание радиосети в вашем квартале, которую можно было бы считать нашим детским прообразом современного Интернет. Нас было человек шесть, увлечённых радиоделом. Все мы увлекались радиолюбительством, собирали усилители, радиоприемники, ремонтировали телевизоры и т.д. Самое главное, обменивались новыми схемами и радиодеталями, которые было трудно достать. Главная проблема была в оперативной связи между собой. Телефонов ни у кого не было и приходилось чуть что, бежать друг к другу. Все мы жили в пределах одного квартала (6-й квартал, ул. Чапаева, г. Чебоксары) в новых пятиэтажках (как сейчас говорят в хрущевках). И вот кому из нас пришла идея: а почему бы нам не организовать радиосвязь между собой?! Нет, не собрать радиостанции, с этим было очень сложно. Нет, не собрать, а получить разрешение. И вот что мы придумали. Пробрасываем по крышам с дома на дом провода (не могу вспомнить, где же мы их взяли), каждый дома ставит усилитель, обзаводится микрофоном, присваиваем каждому позывной (про логин, естественно, тогда никто и не знал) и общаемся. Включаешь усилитель, берешь микрофон и говоришь, например: Первый, первый, вызывает пятый. Ответь. И заработало:



Более того, таким образом, мы на весь квартал включали музыку. Все было хорошо.
Но однажды, когда я был дома один, раздался звонок в дверь. Нет, звонили не сотрудники КГБ, в дверь звонил участковый милиционер. Он вежливо спросил, что это за провода идут с крыши соседнего дома к нам на балкон (мы жили на четвертом этаже фото). Пришлось ему все рассказать. Он попросил продемонстрировать, как все работает, и я связался с одним из своих товарищей. Больше всего его интересовал микрофон. В это время была просто напасть срезали телефонные трубки в телефонных будках, как правило, ради того микрофона и динамика. Но у меня был настоящий микрофон. Я как-то познакомился с главным инженером ДОСААФ (Добровольное Общество Содействия Армии, Авиации и Флоту), он взял над нами шефство и помогал радиодеталями. Так что никаких претензий ко мне и нашей группе у участкового не возникло. Он даже похвалил, что интересным делом занимаемся, а не бесцельно по улицам болтаемся. Еще добавил, что телефонные будки надо беречь. После того, как в 1968 году я уехал учиться в КзСВУ, то, приезжая в отпуск, всегда смотрел на наши провода и иногда даже пользовался этой связью. Но прошло несколько лет и в очередной приезд я увидел, что изоляция на проводах разрушается. Я дернул провода и они оборвались. Так перестала существовать наша сеть (ещё не вычислительная). Так заканчивалось моё детство.
Как я учился? Учёба мне всегда давалась легко. Я был твёрдый ударник и блистал в математике. В этом была большая заслуга нашей учительницы по математике Гавриловой Анны Порфирьевны. Но у меня было, как я говорю до сих пор, три недостатка: у меня отсутствует (или не развит?) музыкальный слух, я не умею танцевать, не пою, и еще я не умею рисовать. При этом я люблю музыку, с удовольствием хожу на концерты, люблю живопись и архитектуру. Эти недостатки не давали мне возможности быть круглым отличником. Да я и не стремился:



О том, что хорошая учёба нужна, прежде всего, мне и только мне, я усвоил уже в первом классе, когда принёс домой двойку. Я имел беседу с отцом, который сказал, что им с мамой некогда заниматься с нами, да и образования у них нет для этих занятий, и что моё будущее только в моих руках. Это был урок на всю жизнь. С тех пор мне было стыдно приносить в дневнике не то что двойки, а и тройки. Двойки и колы я всё приносил, но это были оценки не за мои знания, а так учителя пресекали мои подсказки и шпаргалки на уроках.
На родительские собрания отец никогда не ходил. А мама очень редко. Но один раз они пошли вместе на собрание. С чем это было связано не помню. Это был шестой-седьмой класс. Причём отец надел костюм с галстуком, осеннее пальто, шляпу.
Я стоял в подъезде на лестнице со своим товарищем Толей Ганиным, с которым мы вместе занимались радиолюбительством, когда в подъезд вошли мои родители, возвращающиеся после собрания. Отец шёл довольный, но увидев нас с Толей, сразу стал мне выговаривать: с кем ты дружишь, со шпаной какой-то, двоечником и т.д. Да, Толя не блистал в учёбе и был, как тогда говорили, хулиганом, рос без отца, но в радио разбирался хорошо и был верный товарищ. Вообще нас было трое, я, Толя и Саша Никитин. И каково было моё удивление, когда в разговор вмешалась моя мама. Она сказала, обращаясь к отцу, чтобы он сейчас же прекратил. А дальше сказала то, что я запомнил на всю жизнь, если ты доверяешь сыну, то доверяй и его друзьям. Если твой сын не может сделать плохого, то и друзья его это не сделают. Отец удивлённо и молча всё это выслушал и позвал всех домой пить чай. Инцидент был исчерпан. А Толя Ганин стал частым гостем в нашей квартире. В школе на собрании меня, как правило, всегда хвалили, а вот Толе доставалось от классного руководителя. Но зато я разглядел ещё одну сторону своих родителей.
Я заканчивал восьмой класс. Большинство из нашего класса уже решили, что покидают школу и идут кто в техникум, кто работать, кто в художественное училище, а я собирался в девятый класс. Но в один день всё перевернулось. Однажды, когда прозвенел последний звонок, в класс вошёл классный руководитель и попросил всех мальчиков задержаться, девочкам тоже не возбранялось остаться. В класс вошёл военный (я тогда не разбирался в званиях) и стал рассказывать про суворовские военные училища. И тут во мне всё всколыхнулось и я вспомнил свой четвёртый класс. Всё в одну минуту было решено, я иду в суворовское военное училище. Сразу после рассказа я подошёл к офицеру и расспросил, что нужно делать. Он рассказал, куда и когда подойди, какие документы принести и т.д.
Самым сложным было объявить своё решение дома. Когда о своём решение я сказал, то ни у кого не возникло даже мысли, что я могу не поступить. Мама расплакалась, как ты там, а как мы здесь без тебя и т.д. Но потом все успокоились и было решено, что я поступаю. Был ещё отбор в военкомате, была медкомиссия, были самые настоящие проводы в армию, на которых собрался практически весь мой класс, все родственники и даже моя учительница математики. К сожалению, фотографии с этих проводов нет. Но есть фотография моих проводов в академию им. Ф.Э. Дзержинского после окончания КзСВУ, на ней все те же лица, включая Гаврилову А.П. (вторая слева и слева от меня), которая привили мне любовь к математике:



Обилие овощей, солений, салатов, яблок на столе заставили вспомнить, что мои родители в 1957 году получили участок в 6 соток в садовом товариществе Заря. С тех пор все выходные летом, как правило, проводились, как гордо говорили мы, на даче:



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



Из взошедших по трапу на борт теплохода Ракета, окончить суворовское училище посчастливилось только четверым: Володя Орлов, Саша Усов, Саша Кленин и Ордяков.

II. Казанское суворовское военное училище


Через пару часов нас встречала Казань. А спустя ещё немного времени мы входили на территорию КзСВУ:



По приезде в КзСВУ нам дали несколько дней на подготовку к экзаменам. Экзамены мы сдавали по математике и русскому языку. Сдавали и письменный экзамен и устный. С этим проблем не оказалось. И мне объявили, что я зачислен в училище. Но был ещё и пятый экзамен, вернее, не экзамен, а собеседование по иностранному языку. Это было связано с тем, что в суворовских училищах в те времена изучению иностранных языков предавали серьёзное значение. По окончанию училища можно было получить удостоверение военного переводчика.
В училище велось обучение на двух языках: английском и французском. В школе я учил английский язык, и у меня была твердая четвёрка. Каково же было моё удивление, когда на собеседовании мне сказали, что у меня нулевые знания и что я буду учить французский язык. Удивление было, но расстройства не было. Учить французский язык язык Великой Французской революции, Парижской Коммуны! Что может быть лучше!,- думал я. А еще вспомнилось, как в пятом классе я очень хотел учить испанский язык, язык свободный Кубы, язык, на котором разговаривали Фидель Кастро и Че Гевара. И здесь мне казалось, что французский язык намного ближе к испанскому, чем английский. К французскому языку мы ещё вернёмся чуть ниже.
Я был зачислен в 3 взвод 7 роты (на фотографии третий ряд снизу, восьмой справа). Командиром роты был подполковник Харченко Б.К. (второй ряд снизу, шестой справа), а командиром взвода майор Беседин А.Г. (второй ряд снизу, четвертый справа):



На фотографии наш командир ещё в звании майора. Подполковника он получит чуть позднее, в октябре 1968 года.
Начальником Казанского суворовского военного училища был генерал-майор Смирнов А.П., участник Великой Отечественной войны, участник Сталинградской битвы. Мне на всю жизнь запомнились его слова, звучащие как напутствие:
Посеешь поступок пожнёшь привычку.
Посеешь привычку пожнёшь характер.
Посеешь характер пожнёшь судьбу
Сколько раз по жизни мне приходилось видеть подтверждение этих пророческих слов.
Когда экзамены были позади, когда мы получили форму и удостоверения суворовцев, было ещё одно собеседование, на этот раз на спортивную тему. Меня спросили, занимался ли я спортом и каким, я рассказал что занимался в баскетбольной секции и в секции самбо, а также получил III разряд по шахматам. Мне сказали, что баскетболист из меня с моим ростом так себе, а борцов в училище хватает. А вот то, что у меня разряд по шахматам, это здорово. Меня включили в группу шахматистов, которые после занятий, а также в выходные дни ходили в Дом Офицеров в шахматную секцию. Это, конечно, было что-то, у всех трехмесячный карантин без права выхода в город (без увольнительных), а ты имеешь это право выхода. А навыки самбо, естественно, пригодились на занятиях по рукопашному бою.
Но моя спортивная эпопея имела продолжение. Пришло время сдавать нормативы по ВСК (военно-спортивному комплексу). Это аналог комплекса ГТО (Готов к Труду и Обороне).
После забега на дистанцию 100 метров меня подзывает к себе преподаватель капитан Дергоусов Ю.И. и спрашивает, где я занимался бегом. Мой ответ, что это мой первый забег на 100 метров, его очень удивил. В этот момент мне было 15 лет, время я показал 12 секунд и бежали мы на стадионе по гаревой дорожке в обыкновенных кедах. С этого момента я стал тренироваться на спортивной дорожке. Пришлось мне выступать и на центральном стадионе г. Казани. Но особенно запомнился выигрыш нашей команды майской легкоатлетической эстафеты, посвященной Дню Победы (я в первом ряду третий слева):



Немножко было обидно, что нам за первое место выдали Кубок, а за второе и третье места участникам команд выдали спортивные костюмы. Но посмотрите на наши лица мы все счастливы.
Если говорить о физической подготовке, то надо всё же сказать и о лыжах. Когда наступала зима, мы начинали бегать на лыжах 5 и 10 километров и бегаешь до тех пор, пока не выполнишь норму II разряда. У нас во взводе был Серёжа Лазарев (на фотографии он на переднем плане, а я справа), родом из г. Сочи:



При этом у него были определённые успехи в легкой атлетике. Например, в высоту он брал 175 сантиметров. Но вот лыжи ему не давались, до училища он ни разу не становился на лыжи. Парень он был упёртый (в хорошем смысле этого слова) и с утра до ночи ходил и ходил на лыжах, а мы ему помогали. И он выполнил норматив. После училища он отказался от военной карьеры и в итоге стал парапсихологом, мало в чём уступающим Чумаку и Кашперовскому. Написав о Сергее, я вспомнил случай, произошедший со мной в училище. Как-то я прогуливался по нашему парку и вдруг как наяву вижу, как у моего родного брата Сергея лопается аппендицит. Я остановился как вкопанный. Что делать, как сообщить домой? Сотовых телефонов тогда не было. Да что сотовые, обыкновенные телефоны были ещё редкостью! В итоге, я решил, что это просто блажь, а через три дня (именно столько шли письма из дома в училище) получил письмо от родителей, в котором они сообщали, что у Сергея вырезали гнойный аппендицит, хорошо, что скорая помощь приехала быстро и успели отвести его в больницу. Может, мне тоже надо было в экстрасенсы податься?
Надо сказать, что все офицеры училища имели отличную физическую подготовку. Вот несколько примеров. Начальник училища генерал-майор Смирнов выезжал вместе с нами зимой в лагеря, вместе с нами рыл в заснеженном поле окопы и бросался вместе с нами в атаку вслед за танком. При этом он часто рассказывал, что именно отменная физическая подготовка спасла его от гибели под Сталинградом. Рассказывал он так, показывая рукой на левую грудь, где у него был шрам: когда сердце сжалось, в грудь вошла пуля, которая пролетела мимо сердца. После того как пуля вылетела, сердце разжалось и он продолжил бой.
Или наш ротный подполковник Харченко Борис Кузьмич. Как-то кто-то из нас что-то натворил и в воспитательных целях была объявлена тревога (дело было летом в летних лагерях и в 30-градусную жару). Рота была построена в полной выкладке (скатка шинели, автомат, противогаз). После этого прозвучала команда одеть противогазы и начался марш-бросок на 3 километра. Наш командир роты бежал вместе с нами и при этом следил, чтобы никто не снимал противогаза. Можно сказать, что это жестко, но мы так не считали. И какое было удовлетворение, когда мы это сделали и нас распустили. Можно много рассказывать, а ещё лучше написать отдельную книгу.
А теперь вернёмся к французскому языку. Обучение велось в хорошо оборудованных лингафонных кабинетах по группам. В каждой группе было не более 12 человек. Мне хорошо запомнилось первое занятие. Наше первое занятие началось с того, что преподаватель Милорадовская Е.А. предложила надеть наушники и послушать текст с пластинки. Я тоже внимательно слушал, но смог, как мне казалось, понять только два слова: Илья и Баку. И когда меня преподаватель спросила, о чём шла речь на пластинке, я ответил, что про какого-то Илью, приехавшего или жившего в Баку. Меня ждало разочарование. Оказывает Баку это не Баку, а beaucou в смысле много, а Илья тоже не Илья, а предложение il a в смысле он имеет. Так началось мое изучение французского языка. Учили нас прекрасно. Вообще мы стремились учиться, была какая-та хорошая состязательность. У меня был спарринг партнёр (если так можно выразиться) Серёжа Оглоблин, с которым мы соревновались, кто больше из нас получит отличных оценок за день, неделю и т.д.:



Сергей окончил училище с золотой медалью. А меня подвёл русский язык, несмотря на все старания нашего любимого преподавателя Смирновой Ноны Владимировны. Каждый день задавали выучить наизусть несколько страниц текста. Мы сами стремились пополнить свой словарный запас. Ряд предметов (например, астрономия, география) нам давали не на русском языке, а на том языке, который изучаешь. Естественно, была иностранная литература и военный перевод. В итоге, я выпустился из училища с удостоверением военного переводчика:



Помимо удостоверения переводчика я получил водительское удостовение и даже охотничий билет. К взрослой жизни по окончании КзСВУ я был готов, мог работать киномехаником, водителем и даже переводчиком.
Были и другие и комические, и трагические, и трагикомические случаи. Тут нашлась одна из моих записных книжек (дневник), там много чего интересного есть.
Первый мой отпуск домой в декабре 1968 года мог завершиться трагедией. Вместе со мной поступил в КзСВУ еще один юноша Кленин Саша, сын одного из офицеров Чебоксарского военкомата. Кстати, впоследствии он был участником той победной эстафеты (на фотографии второй справа в первом ряду).
Так вот, отец Саши Кленина на зимние каникулы прислал за ним машину ГАЗ-69 (газик/козлик), в которой приехали его мама и мой отец. Это было неожиданно и приятно.
И вот мы впятером отъехали от училища в сторону Чебоксар, а перед самым выездом из Казани решили пообедать в придорожном кафе (стекляшке, как их тогда называли). После обеда, удобно устроившись в машине, тронулись в сторону Чебоксар. Но мы не проехали и ста метров, как вдруг, кувыркаясь, полетели в кювет. Что же произошло? Был яркий солнечный и морозный день. Только что прошел снегопад. А вслед за снегопадом пошли грейдеры, которые расчистили не только дорогу, но и сравняли с дорогой кювет. А обрыв был приличный. И водитель, не подозревая этого подвоха, чуть-чуть прижался к правому краю дороги и мы полетели вниз. Первое, что я запомнил, это крик моего отца (машина лежала на крыше):- Володя, ты жив? Когда я откликнулся, он сумел выбраться из машины и вместе с водителем помог выбраться с заднего сидения и нам. Зрелище было печальное, машина лежит на крыше, лобового стекла нет, а у нас все лица залеплены осколками стекла. Но никто ничего не сломал, никто не порезался и кроме синяков никаких увечий никто из нас не получил. А наверху, на дороге, уже остановилось несколько машин и среди них был подъёмный кран. Когда стоящие наверху люди поняли, что с нами всё нормально, то стали думать, как нас вытащить. Решение было простым, кран опустил стрелу, водитель подцепил машину и её вытащили. Мы выбирались сами по заснеженному откосу. Оказалось, что машина на ходу, завелась с полоборота. Встала дилемма, ехать дальше в Чебоксары, но нет лобового стекла, на улице 20-градусный мороз и скоро начнёт темнеть. Либо где-то искать возможность достать и вставить стекло. Сколько на это уйдет времени неизвестно. Не забывайте на дворе 1968 год, Мы решили ехать в Чебоксары. А поскольку стекла не было, то ехали медленно. У Саши Кленина стали замерзать ноги. Как сейчас помню, как его мама всю дорогу их отогревала у себя на груди. Но худо-бедно мы добрались до Чебоксар. Мы с отцом поднялись на четвёртый этаж и позвонили в дверь. Дверь открыла мам и тут же присела, увидев нас, только и сказала:- Что с вами? Наши лица были синие, сплошной синяк. Но стол был накрыт, бульон для пельменей кипел. Жизнь продолжалась.
В суворовском училище нам, естественно, запрещали употреблять спиртные напитки и курить. Причём, наш взводный майор Беседин А.Г. говорил так:- Лучше выпить сто грамм водки, чем выкурить сигарету. Сам я курить начал только в 20 лет. Но любители побаловать сигаретой у нас во взводе были. И вот как-то утром после завтрака, но перед занятиями два моих товарища Андрей Николаев и Игорь Шишов, дают мне ключ от туалета, в котором шёл ремонт, и просят их закрыть там минут на десять, что я и сделал (третий слева я, четвёртый Игорь Шишов и пятый Андрей Николаев):



Но случилось непредвиденное я забыл про них. Удивительно и то, что за весь день, пока они сидели в туалете, никто про них не вспомнил. Ведь, когда начинались занятия, дежурный всегда докладывал преподавателю о наличии личного состава. И вспомнил я о них, когда рота строилась на вечернюю поверку. При выходе на них было жалко смотреть. Это я о вреде курения.
Был и неприятный случай, связанный с водкой. Однажды в расположении нашей роты нашлась бутылка с водкой и никто не хотел сознаваться, чья это бутылка. А поскольку в город мог свободно в рабочие дни (помните шахматы, Дом Офицеров) мог ходить только я, то волей неволей тень подозрения пала и на меня. Но спустя несколько дней отцы-командиры всё же разобрались кто и что. Этим человеком оказался суворовец, у которого были проблемы и с успеваемостью. В итоге он был отчислен из училища. Фамилию его называть не буду, но он есть на предыдущей фотографии и это не я.
Пить и курить запрещали, а жениться нет (при достижении 18 лет). И у нас был трагический случай. Один из суворовцев встречался с девушкой и она забеременела. Суворовец оказался не тот и отказался жениться на ней. Девушка бросилась под трамвай. К счастью (хотя о каком счастье может идти речь) она осталось жива, но без ступни. Суворовца отчислили, отправили служить в армию. Это о том, что за свои поступки надо отвечать.
Расскажу ещё один поучительный случай (а таких случаев было много). Летом мы выезжали в лагеря и жили в палатках:



Там, в лагерях у нас проходила и топографическая подготовка. На одном из занятий командир взвода майор Беседин раздал нам топографические карты, предупредив, что они секретные, и поставил всем задачу: какой маршрут пройти, что найти и т.д. Перед выходом на маршрут все карты мы сдали командиру. Но оказалось, не все. Когда я вернулся с маршрута, то увидел бледного своего командира, который первым делом спросил, не брал ли я карту с собой. Тут я узнал, что одной карты не хватает. Тот, кто имел дело с секретным делопроизводством, понимает, о чём идет речь. Но в итоге карта нашлась, один из нас, самый умный, взял карта с собой на маршрут в надежде, что это поможет ему лучше пройти маршрут. Оказалось, не помогло, он пришёл одним из последних, но нервы командиру потрепал. С тех пор я знаю цену секретному делопроизводству. Случались неприятные случаи и при обращении с оружием. Например, однажды наш первый стрелок кандидат в мастера спорта выпустил очередь из автомата перед носком сапога командира роты подполковника Харченко Б.К., когда тот шёл вдоль бруствера окопа, в котором с автоматами наизготовку находились мы, проверяя готовность нас к стрельбе по мишеням.
Приятными моментами нахождения в летних лагерях были заготовка берёзовых веников для отцов командиров и ловля карасей трёхлитровыми банками. Не обходилось и без самоволок до ближайшего посёлка Дербышки на танцы.
Суворовское училище было только ступенькой во взрослую жизнь. Надо было думать, кем же я хочу стать? В 1968 году выходит фильм Мертвый сезон, а в газете Комсомольская правда публикуется серия статей о советских разведчиках, если мне память не изменяет, об Абеле и Киме Филби. В 1969 году произошли кровавые события вокруг острова Даманского на Дальнем востоке. Как мы все рвались туда, на Даманский. Я решил для себя, что должен стать разведчиком. Всё шло к тому, что так и будет. Командир роты и командир взвода знали о моём заветном желании. Когда началась учёба в 11 классе, меня вдруг вызвали на собеседование. Как я потом узнал, пришла разнарядка в Приволжский военный округ на два места в высшую школу КГБ. Просмотрено было 600 (шестьсот) кандидатов и выбор пал на меня и еще одного суворовца, у которого отец служил в Комитете. Я был счастлив. Но когда в суворовском училище узнали, что я согласился идти в высшую школу КГБ, началось что-то для меня непонятное, преподаватели стали отговаривать. Кто говорил, что надо идти в академию связи, кто в академию химзащиты, кто в Можайку и т.д. И все говорили, что я зарываю свой талант, кто в математике, кто в физике, кто в химии и т.п. Но я стоял на своём и проходил различные комиссии. Однажды приехал ко мне отец и как обычно остановился у родителей Андрея Николаева. Его родители преподавали в ветеринарном институте, отец был профессором, мать доцентом (по крайней мере, так у меня отложилось в памяти) (слева на право моя мама, мама Андрея Николаева, Андрей, мой отец, внизу мой брат Сергей) и дружили с моими родителями:



И вот, когда я пришел к ним в гости и мы все сели за обеденный стол, опять поднялся разговор о том, где мне дальше учиться. Николай Сергеевич, отец Андрея, сказал: Представляешь, Володя, пройдет время и ты окажешься вот также за столом, но как только ты сядешь за стол, все замолчат и ты почувствуешь себя очень неуютно. И сейчас ты не горячись, а подумай хорошенько. Когда мы остались с отцом вдвоём, он мне сказал: Володя, а может они правы, они же грамотные люди в отличие от нас с матерью, может, правда не стоит туда идти. Эта была последняя капля. Я сказал, что хорошо, я откажусь от предложения учиться в вышке, но куда я пойду, пока не знаю. Потом у меня состоялся тяжелый разговор, тяжелый для меня, что я не оправдал оказанного мне доверия, с направленцем, который курировал отбор кандидатов в вышку. Мне кажется, он понял меня и когда он спросил, а кто, если не я, то я, не задумываясь, назвал Сережу Оглоблина. Так и случилось, Сергей пошёл вместо меня. Позже в Москве мы с ним встречались, а потом потерялись.
Надо было решать куда идти. Была мысль податься в медицинскую академию им. С.М.Кирова, то тут на глаза в газете Красная Звезда попалось объявление о наборе слушателей в Военную Академию им. Ф.Э.Дзержинского. Я срочно написал туда письмо с просьбой прислать условия приёма. И мне пришла бандероль с буклетом и программой вступительных экзаменов. Надо сказать, что в училище никто ничего не знал про эту академию. Но для меня всё было решено. Это учеба в Москве, есть факультет связи и радиоэлектроники, и самое главное это название им. Ф.Э.Дзержинского, где-то рядом с моей мечтой о подвигах разведчика. Да, о подготовке программистов там не было ни слова. Да я ещё и слова такого не знал. Меня опять стали отговаривать, но я уже не обращал внимания. Предпринял попытку меня отговорить и наш командир взвода майор Беседин А.Г. Он говорил следующее: Володя, у меня товарищ служил на острове Земля Франца Иосифа, там, куда продукты питания завозят раз в полгода. И вот настал момент, когда ему удалось вырваться оттуда и поступить именно в академию Дзержинского. Каково же было его разочарование, когда после окончания академии его снова направили на этот остров. Я не знал, что академия им. Ф.Э.Дзержинского готовил ракетчиков. Но теперь уже ничто меня не могло заставить сменить решение. Перед самым окончанием училища наш преподаватель математики Егорова Л.Ф. сказала, что сразу после выпускного начинаем готовиться к экзаменам. Но я ответил, что сразу после выпускного еду к родителям, а оттуда в академию. К поступлению я готов. Мне показалось, что её обидел мой отказ. Но в итоге я оказался прав.
В один из последних дней пребывания в училище вдруг ко мне подходит суворовец из соседней шестой роты, золотой медалист Женя Арсентьев. Он спросил: Это правда, что ты едешь в академию Дзержинского? Получив положительный ответ, он предложил идти в академию вместе. Оказалось, что его родной старший брат преподает в академии химию, имеет воинское звание полковник и ученую степень доктора наук. Мы договорились встретиться у академии, дай бог памяти, числа 30 июня 1971 года. Я всё ещё не знал, что с сентября 1971 года в академии начинается подготовка военных инженеров-программистов
Сейчас я понимаю, что у меня было три реперных точки, которые привели меня на стезю программирования: поступление в КзСВУ, отказ от учебы в высшей школе КГБ и встреча с Женей Арсентьевым. Но самое главное, это те глубокие знания, которые я получил в Казанском суворовском военном училище. Огромное спасибо нашим педагогам:



Перед выпуском из КзСВУ нас переодели в новенькую парадную курсантскую форму, выдали проездные, предписания и мы убыли во взрослую жизнь.

III. Поступление в Военную академию им. Ф.Э.Дзержинского


По дороге в ВАД я на несколько дней впервые в курсантской форме заскочил к родителям домой:



Но отпуск летом 1971 года был чисто символическим. И уже через несколько дней меня провожали в аэропорт на самолёт до Москвы. Отец настоял как всегда (за что я ему глубоко благодарен) на общей фотографии:



И вот я уже в Москве стою у КПП (контрольно пропускной пункт) академии им. Ф.Э.Дзержинского, жду Женю Арсентьева:



На этой фотографии ещё слева видна гостиница Россия. Подошёл Женя и сказал, что брат предложил пожить пару дней у него дома, пока в академии всё устаканится с приёмом абитуриентов. В Москве я был в самом раннем детстве в далёких 1957-58 годах. Поэтому для меня всё было вновь. Как оказалось, многое и для Евгения. Всё началось у нас с ним с комического случая. Когда мы приехали в дом брата Жени, то надо было подниматься на лифте (до этого на лифте я ездил раз-два и то на один-два этажа). Здесь надо было подниматься повыше. Когда мы вошли в лифт, то пол, естественно, но не для нас, опустился. Мы нажали нужный этаж и поехали, но тут кто-то из нас предложил давай подпрыгнем! И мы сделали это. Как результат, лифт остановился. Вокруг никого. Что делать, не знаем. На наше счастье минут через тридцать мимо проходила старушка, увидев нас, запричитала, опять сломался. Сейчас я вызову лифтера. Пришёл лифтер, открыл дверь и мы выползли (именно выползли) из лифта. Никто и не подумал, что это мы по своей провинциальной безграмотности сломали лифт. Вечером из академии вернулся полковник Арсентьев, мы поужинали и он сказал:-
В академии открыт новый факультет, на котором будут готовить специалистов по программированию.

Что это такое он толком не понимает, но чувствует, что за этим стоит большое будущее и его нам совет держать путь на второй факультет на специальность Программирование. Но если младшему Арсентьеву как золотому медалисту требовалось только его желание, то мне еще предстояло сдать экзамены.
Через три дня я прихожу в академию и начинаю искать приёмную начальника II факультета. Заблудиться в коридорах академии не составляет труда. И тут навстречу мне идет курсант в повседневной форме, сапоги блестят, форма отутюжена и самое главное с суворовским знаком. Мне показалось, что этот курсант учится здесь уже не первый год и я обратился к нему за помощью, спросив, куда и как пройти. Это сейчас я сразу вспоминаю Фросю Бурлакову из кинофильма Доживём до понедельника, а тогда этот курсант всё уверенно мне рассказал и показал и мы расстались. Каково же было моё удивление, когда на письменном экзамене по математике (я уже по традиции собирался сдавать работу и выходить из аудитории) меня кто-то потрогал сзади по плечу. Я оглянулся и увидел того курсанта, который так толково мне всё объяснил. Он меня попросил не сдавать работу, не уходить и помочь решить его задачу. Я выполнил его просьбу. Николай Гудим (на фотографии он слева), именно так его звали, в итоге тоже поступил в академию:



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



Всего в 1971 году было набрано два отделения программистов, которые с сентября 1971 года начали обучение по этой специальности. А еще было три отделения, где готовили инженеров по эксплуатации электронно-вычислительных комплексов.
На фотографии ниже моё отделение (я третий слева во втором ряду снизу, Коля Гудим второй справа во втором ряду сверху):



IV. Послесловие к первой части


После окончания академии наши пути с Колей Гудимом разошлись. Но спустя шесть лет, после того как я послужил в ГРУ (так или иначе, но я следовал своей мечте), защитил в 1982 году диссертацию по системам управления распределёнными базами данных в той же Дзержинке, мы с ним встретились в 4-м Центральном научно-исследовательском институте Министерства Обороны (4 ЦНИИ МО). Но об этом речь пойдет в следующей части:



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

Скрываем номера курьеров и клиентов с помощью key-value хранилища

17.06.2021 18:06:38 | Автор: admin

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

Каждый сервис использует свои решения для маскировки номеров клиентов и курьеров. В данной статье я расскажу, как сделать это с помощью key-value хранилища в Voximplant.

Как это будет работать

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

У нас будет только один нейтральный номер, на который будут звонить и клиент, и курьер. Номер мы арендуем в панели Voximplant. Затем создадим некую структуру данных, где клиент и курьер будут связаны между собой номером заказа (то есть ключом в терминологии key-value storage).

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

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

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

Перейдем непосредственно к реализации.

Вам понадобятся

1) Чтобы начать разработку, войдите в свой аккаунт: manage.voximplant.com/auth. В меню слева нажмите Приложения, затем Создать приложение в правом верхнем углу. Дайте ему имя, например, numberMasking и снова кликните Создать.

2) Зайдите в новое приложение, переключитесь на вкладку Сценарии и создайте сценарий, нажав на +. Назовём его kvs-scenario. Здесь мы будем писать код, но об этом чуть позже.

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

4) Далее арендуем реальный городской номер. Для этого перейдем в раздел Номера, выберем и оплатим номер. На него будут звонить и клиент, и курьер, и он будет отображаться вместо их настоящих номеров.

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

5) Осталось привязать его к нашему приложению. Заходим в приложение, открываем вкладку Номера Доступные и нажимаем Прикрепить. В открывшемся окне можно также прикрепить наше правило, тогда оно будет автоматически назначено для входящих вызовов, а все остальные правила будут проигнорированы.

6) Далее необходимо верифицировать аккаунт, чтобы использовать этот номер для звонков.

Отлично, структура готова, осталось заполнить key-value хранилище и добавить код в сценарий.

Key-value хранилище

Чтобы сценарий заработал, нужно положить что-то в хранилище. Это можно сделать, воспользовавшись Voximplant Management API. Я буду использовать Python API client, он работает с Python 2.x или 3.x с установленным pip и setuptools> = 18.5.

1) Зайдем в папку проекта и установим SDK, используя pip:

python -m pip install --user voximplant-apiclient

2) Создадим файл с расширением .py и добавим в него код, при выполнении которого данные о заказе попадут в key-value хранилище. Применим метод set_key_value_item:

from voximplant.apiclient import VoximplantAPI, VoximplantExceptionif __name__ == "__main__":    voxapi = VoximplantAPI("credentials.json")        # SetKeyValueItem example.    KEY = 12345    VALUE = '{"courier": "79991111111", "client": "79992222222"}'    APPLICATION_ID = 1    TTL = 864000        try:        res = voxapi.set_key_value_item(KEY,            VALUE,            APPLICATION_ID,            ttl=TTL)        print(res)    except VoximplantException as e:        print("Error: {}".format(e.message))

Файл с необходимыми credentials вы сможете сгенерировать при создании сервисного аккаунта в разделе Служебные аккаунты в настройках панели.

APPLICATION_ID появится в адресной строке при переходе в ваше приложение.

В качестве ключа (KEY) будет использоваться пятизначный номер заказа, а в качестве значений телефонные номера: courier номер курьера, client номер клиента. TTL нам здесь необходимо для указания срока хранения значений.

3) Осталось запустить файл, чтобы сохранить данные заказа:

python3 kvs.py

Если мы больше не захотим, чтобы клиент и курьер беспокоили друг друга, можно будет удалить их данные из хранилища. Информацию о всех доступных методах key-value storage вы найдёте в нашей документации: management API и VoxEngine.

Код сценария

Код, который необходимо вставить в сценарий kvs-scenario, представлен ниже, его можно смело копировать as is:

Полный код сценария
require(Modules.ApplicationStorage);/** * @param {boolean} repeatAskForInput - была ли просьба ввода произнесена повторно * @param longInputTimerId - таймер на отсутствие ввода * @param shortInputTimerId - таймер на срабатывание фразы для связи с оператором * @param {boolean} firstTimeout - индикатор срабатывания первого таймаута * @param {boolean} wrongPhone - индикатор совпадения номера звонящего с номером, полученным из хранилища * @param {boolean} inputRecieved - получен ли ввод от пользователя *  */let repeatAskForInput;let longInputTimerId;let shortInputTimerId;let firstTimeout = true;let wrongPhone;let inputRecieved;const store = {    call: null,    caller: '',    callee: '',    callid: '74990000000',    operator_call: null,    operatorNumber: '',    input: '',    data: {        call_operator: '',        order_number: '',        order_search: '',        phone_search: '',        sub_status: '',        sub_available: '',        need_operator: '',        call_record: ''    }}const phrases = {    start: 'Здр+авствуйтте. Пожалуйста, -- введите пятизначный номер заказa в тт+ооновом режиме.',    repeat: 'Пожалуйста , , - - введите пятизначный номер заказа в т+оновом режиме,, или нажмите решетку для соединения со специалистом',    noInputGoodbye: 'Вы - ничего не выбрали. Вы можете посмотреть номер заказа в смс-сообщении и позвонить нам снова. Всего д+обровоо до свидания.',    connectToOpearator: 'Для соединения со специалистом,, нажмите решетку',    connectingToOpearator: 'Ожидайте, соединяю со специалистом',    operatorUnavailable: 'К сожалению,, все операторы заняты. Пожалуйста,,, перезвоните позднее. Всего д+обровоо до свидания.',    wrongOrder: 'Номер заказа не найден. Посмотрите номер заказа в смс-сообщении и введите его в т+оновом режиме. Или свяжитесь со специалистом,, нажав клавишу решетка.',    wrongOrderGoodbye: 'Вы ничего не выбрали, всего д+обровоо до свидания.',    wrongPhone: 'Номер телефона не найден. Если вы кли+ент, перезвоните с номера, который использовали для оформления заказа. Если вы курьер, перезвоните с номера, который зарегистрирован в нашей системе. Или свяжитесь со специалистом,,- нажав клавишу решетка.',    wrongPhoneGoodbye: 'Вы ничего не выбрали. Всего доброго, до свидания!',    courierIsCalling: `Вам звонит курьер по поводу доставки вашего заказа, - - ${store.data.order_number}`,    clientIsCalling: `Вам звонит клиент по поводу доставки заказа, - - ${store.data.order_number} `,    courierUnavailable: 'Похоже,,, курь+ер недоступен. Пожалуйста,,, перезвоните через п+ару мин+ут. Всего д+обровоо до свидания.',    clientUnavailable: 'Похоже,,, абонент недоступен. Пожалуйста,,, перезвоните через пп+ару мин+ут. Всего д+обровоо до свидания.',    waitForCourier: 'Ожидайте на линии,, - соединяю с курьером.',    waitForClient: 'Ожидайте на линии,, соединяю с клиентом.'}VoxEngine.addEventListener(AppEvents.Started, async e => {    VoxEngine.addEventListener(AppEvents.CallAlerting, callAlertingHandler);})async function callAlertingHandler(e) {    store.call = e.call;    store.caller = e.callerid;    store.call.addEventListener(CallEvents.Connected, callConnectedHandler);    store.call.addEventListener(CallEvents.Disconnected, callDisconnectedHandler);    store.call.answer();}async function callDisconnectedHandler(e) {    await sendResultToDb();    VoxEngine.terminate();}async function callConnectedHandler() {    store.call.handleTones(true);    store.call.addEventListener(CallEvents.RecordStarted, (e) => {        store.data.call_record = e.url;    });    store.call.record();    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.start);    addInputTimeouts();}function dtmfHandler(e) {    clearInputTimeouts();    store.input += e.tone;    Logger.write('Введена цифра ' + e.tone)    Logger.write('Полный код ' + store.input)    if (e.tone === '#') {        store.data.need_operator = "Да";        store.call.removeEventListener(CallEvents.ToneReceived);        store.call.handleTones(false);        callOperator();        return;    }    if (!wrongPhone) {        if (store.input.length >= 5) {            repeatAskForInput = true;            Logger.write(`Получен код ${store.input}. `);            store.call.handleTones(false);            store.call.removeEventListener(CallEvents.ToneReceived);            handleInput(store.input);            return;        }    }    addInputTimeouts();}function addInputTimeouts() {    clearInputTimeouts();    if (firstTimeout) {        Logger.write('Запущен таймер на срабатывание фразы для связи с оператором');        shortInputTimerId = setTimeout(async () => {            await say(phrases.connectToOpearator);        }, 1500);        firstTimeout = false;    }    longInputTimerId = setTimeout(async () => {        Logger.write('Сработал таймер на отсутствие ввода от пользователя ' + longInputTimerId);        store.call.removeEventListener(CallEvents.ToneReceived);        store.call.handleTones(false);        if (store.input) {            handleInput(store.input);            return;        }        if (!repeatAskForInput) {            Logger.write('Просим пользователя повторно ввести код');            store.call.handleTones(true);            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);            await say(phrases.repeat);            addInputTimeouts();            repeatAskForInput = true;        } else {            Logger.write('Код не введен. Завершаем звонок.');            await say(inputRecieved ? phrases.wrongOrderGoodbye : phrases.noInputGoodbye);            store.call.hangup();        }    }, 8000);    Logger.write('Запущен таймер на отсутствие ввода от пользователя ' + longInputTimerId);}function clearInputTimeouts() {    Logger.write(`Очищаем таймер ${longInputTimerId}. `);    if (longInputTimerId) clearTimeout(longInputTimerId);    if (shortInputTimerId) clearTimeout(shortInputTimerId);}async function handleInput() {    store.data.order_number = store.input;    Logger.write('Ищем совпадение в kvs по введенному коду: ' + store.input)    inputRecieved = true;    let kvsAnswer = await ApplicationStorage.get(store.input);    if (kvsAnswer) {        store.data.order_search = 'Заказ найден';        Logger.write('Получили ответ от kvs: ' + kvsAnswer.value)        let { courier, client } = JSON.parse(kvsAnswer.value);        if (store.caller == courier) {            Logger.write('Звонит курьер')            store.callee = client;            store.data.sub_status = 'Курьер';            store.data.phone_search = 'Телефон найден';            callCourierOrClient();        } else if (store.caller == client) {            Logger.write('Звонит клиент')            store.callee = courier;            store.data.sub_status = 'Клиент';            store.data.phone_search = 'Телефон найден';            callCourierOrClient();        } else {            Logger.write('Номер звонящего не совпадает с номерами, полученными из kvs');            wrongPhone = true;            store.data.phone_search = 'Телефон не найден';            store.input = '';            store.call.handleTones(true);            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);            await say(phrases.wrongPhone);            addInputTimeouts();        }    } else {        Logger.write('Совпадение в kvs по введенному коду не найдено');        store.data.order_search = 'Заказ не найден';        store.input = '';        store.call.handleTones(true);        store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);        await say(phrases.wrongOrder);        Logger.write(`Очищаем таймер ${longInputTimerId}. `);        addInputTimeouts();    }}async function callCourierOrClient() {    clearInputTimeouts();    Logger.write('Начинаем звонок курьеру/клиенту');    await say(store.data.sub_status === 'Курьер' ? phrases.waitForClient : phrases.waitForCourier, store.call);    const secondCall = VoxEngine.callPSTN(store.callee, store.callid);    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');    secondCall.addEventListener(CallEvents.Connected, async () => {        store.data.sub_available = 'Да';        await say(store.data.sub_status === 'Курьер' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);        store.call.stopPlayback();        VoxEngine.sendMediaBetween(store.call, secondCall);    });    secondCall.addEventListener(CallEvents.Disconnected, () => {        store.call.hangup();    });    secondCall.addEventListener(CallEvents.Failed, async () => {        store.data.sub_available = 'Нет';        store.call.stopPlayback();        await say(store.data.sub_status === 'Курьер' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);        store.call.hangup();    });}async function callOperator() {    Logger.write('Начинаем звонок оператору');    await say(phrases.connectingToOpearator, store.call);    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');    store.operator_call = VoxEngine.callPSTN(store.operatorNumber, store.callid);    store.operator_call.addEventListener(CallEvents.Connected, async () => {        store.data.call_operator = 'Оператор свободен';        VoxEngine.sendMediaBetween(store.call, store.operator_call);    });    store.operator_call.addEventListener(CallEvents.Disconnected, () => {        store.call.hangup();    });    store.operator_call.addEventListener(CallEvents.Failed, async () => {        store.data.call_operator = 'Оператор занят';        await say(phrases.operatorUnavailable, store.call);        store.call.hangup();    });}async function sendResultToDb() {    Logger.write('Данные для отправки в БД');    Logger.write(JSON.stringify(store.data));    const options = new Net.HttpRequestOptions();    options.headers = ['Content-Type: application/json'];    options.method = 'POST';    options.postData = JSON.stringify(store.data);    await Net.httpRequestAsync('https://voximplant.com/', options);}function say(text, call = store.call, lang = VoiceList.Yandex.Neural.ru_RU_alena) {    return new Promise((resolve) => {        call.say(text, lang);        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));        });    });};

Код тщательно прокомментирован, но в некоторые моменты углубимся подробнее.

Вводим номер заказа

Первое, что мы делаем при звонке просим звонящего ввести номер заказа и обрабатываем введенное значение с помощью функции dtmfHandler.

store.input += e.tone;

Если звонящий ввел #, сразу соединяем его с оператором:

if (e.tone === '#') {    store.data.need_operator = "Да";    store.call.removeEventListener(CallEvents.ToneReceived);    store.call.handleTones(false);    callOperator();    return;}

Если он ввел последовательность из 5 цифр, вызываем функцию handleInput:

if (store.input.length >= 5) {    repeatAskForInput = true;    Logger.write('Получен код ${store.input}. ');    store.call.handleTones(false);    store.call.removeEventListener(CallEvents.ToneReceived);    handleInput(store.input);    return;}

Ищем заказ в хранилище

Здесь мы будем сравнивать введенный номер заказа с номером в хранилище, используя метод ApplicationStorage.get(), в качестве ключа используем введенную последовательность:

store.data.order_number = store.input;Logger.write('Ищем совпадение в kvs по введенному коду: ' + store.input)inputRecieved = true;let kvsAnswer = await ApplicationStorage.get(store.input);

Если заказ найден, получаем для него номера клиента и курьера:

if (kvsAnswer) {    store.data.order_search = 'Заказ найден';    Logger.write('Получили ответ от kvs: ' + kvsAnswer.value)    let { courier, client } = JSON.parse(kvsAnswer.value);

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

if (store.caller == courier) {    Logger.write('Звонит курьер')    store.callee = client;    store.data.sub_status = 'Курьер';    store.data.phone_search = 'Телефон найден';    callCourierOrClient();} else if (store.caller == client) {    Logger.write('Звонит клиент')    store.callee = courier;    store.data.sub_status = 'Клиент';    store.data.phone_search = 'Телефон найден';    callCourierOrClient();}

Если номера нет в хранилище, просим перезвонить с другого номера, который указывался при оформлении заказа:

else {    Logger.write('Номер звонящего не совпадает с номерами, полученными из kvs');    wrongPhone = true;    store.data.phone_search = 'Телефон не найден';    store.input = '';    store.call.handleTones(true);    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.wrongPhone);    addInputTimeouts();}

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

else {    Logger.write('Совпадение в kvs по введенному коду не найдено');    store.data.order_search = 'Заказ не найден';    store.input = '';    store.call.handleTones(true);    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.wrongOrder);    Logger.write(`Очищаем таймер ${longInputTimerId}. `);    addInputTimeouts();}

Звоним клиенту/курьеру

Переходим непосредственно к звонку клиенту/курьеру, то есть к логике функции callCourierOrClient. Здесь мы сообщим звонящему, что переводим его звонок на курьера/клиента, и включим музыку на ожидание. С помощью метода callPSTN позвоним клиенту или курьеру (в зависимости от того, чей номер был ранее идентифицирован как номер звонящего):

await say(store.data.sub_status === 'Курьер' ? phrases.waitForClient : phrases.waitForCourier, store.call);const secondCall = VoxEngine.callPSTN(store.callee, store.callid);store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');

В этот же момент сообщим второй стороне о том, что звонок касается уточнения информации по заказу:

secondCall.addEventListener(CallEvents.Connected, async () => {    store.data.sub_available = 'Да';    await say(store.data.sub_status === 'Курьер' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);    store.call.stopPlayback();    VoxEngine.sendMediaBetween(store.call, secondCall);});

Обработаем событие дисконнекта:

secondCall.addEventListener(CallEvents.Disconnected, () => {    store.call.hangup();});

И оповестим звонящего, если вторая сторона недоступна:

secondCall.addEventListener(CallEvents.Failed, async () => {    store.data.sub_available = 'Нет';    store.call.stopPlayback();    await say(store.data.sub_status === 'Курьер' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);    store.call.hangup();});

За все фразы, который произносит робот, отвечает функция say, а сами фразы перечислены в ассоциативном массиве phrases. В качестве TTS провайдера мы используем Yandex, голос Alena:

function say(text, call = store.call, lang = VoiceList.Yandex.Neural.ru_RU_alena) {    return new Promise((resolve) => {        call.say(text, lang);        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));        });    });};

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

Тестируем

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

Позвоним с телефона клиента или курьера на номер, арендованный в панели. Затем введем номер заказа (в нашем случае это 12345) и будем ждать соединения со второй стороной.

Если все сделано верно, клиент и курьер смогут созваниваться и обсуждать детали заказа, не зная настоящих номеров друг друга, а значит, не нарушая прайваси. Круто, не так ли?) Желаем вам успешной разработки и беспроблемной доставки!

P.S. Также мой коллега недавно рассказал, как обезопасить общение клиента и курьера с помощью Voximplant Kit (наш low-code/no-code продукт). Если эта тема вас заинтересовала, переходите по ссылке :)

Подробнее..

Интервью с Марселем Ибраевым о распиле монолита или Успех распила монолита грамотный менеджмент

17.06.2021 20:21:19 | Автор: admin
Я как-то видел, когда в команду разработки закинули задачу распилить монолит. И всё. Люди должны были работать в два раза больше это ужасно.
Когда поступает похожий запрос, важно не наворотить дел и понять, как избежать новых трудностей. Об этом рассказал Марсель Ибраев, технический директор Слёрма.

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



Давай представим, что перед нами стоит задача распилить монолит.

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

Так, и какими дальше будут наши действия?

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

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

Есть мнение, что монолит это плохо и нужно сразу делать микросервис. Это так?

Нет. Я в корне не согласен с этим. Монолит и микросервис это две разные архитектуры под две разные задачи. Например, если проект представляет собой магазин носков, который ежедневно посещают пять человек и в будущем их будет не сильно больше, то, скорее всего, лучше делать монолит. Так будет быстрее и проще. Главное не упустить момент, когда требования к проекту и подходам сильно изменились, а ваш магазин стал вторым Amazon. :)

А есть ли какое-то решение в обход распила монолита?
Допустим, мы мигрируем в облако, и нам требуется при миграции подготовить свое приложение к Кубу, а именно распилить на сервисы.


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

То есть не получится запихать legacy в контейнер и сказать, вот, пожалуйста, влезло.

Нет. Такая идея может возникнуть. Например, кто-то написал какое-нибудь древнее legacy, которое все боятся трогать. Допустим, этот человек пару лет назад уволился, а сейчас кто-нибудь предлагает взять и запихать его код в контейнер со словами: Пусть это там само работает. Звучит дико, согласен. И я бы не поверил, что такое может быть, если бы не увидел своими глазами.

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


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

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

Почему?

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

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

Есть какие-то выходы из этой ситуации?

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

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

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

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

Проекты бывают разные. У кого-то интернет-магазин, у кого-то платформа для торгов. Отличаются ли аспекты распила для них?

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

Копнём про гибкость глубже. Распил какой-то части отодвигается на месяц, как тогда быть?

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

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

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


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

С кем важно договориться в первую очередь?

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

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

Сколько нужно разработчиков и админов для распила монолита? Кто из них будет работать дольше или больше?

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

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

Да, у нас было такое. Пришел к нам клиент, которому нужен был Kubernetes. Поскольку мы ребята опытные, то сразу задаём встречный вопрос: А вам это зачем? Бывали случаи, когда мы отказывали клиентам, потому что не видели необходимость в Kubernetes, естественно всё объясняли. Человек услышал что-то про Kubernetes, решил, что это круто, и захотел. Расскажу про ситуацию, когда мы не отказали.

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

Часть монолита хостилась на нескольких серверах в разных частях. Примерно всей кодовой базы была неизвестна: туда никто не лез, потому что люди, которые ее писали, уволились, и изучение того, что там, оставляли на потом. Проект был в сложном состоянии. Контейнеры крутились в Docker Swarm. В одном контейнере был просто запущен кусок монолита, хотя ради справедливости стоит отметить, что было и несколько микросервисов. В итоге, конечно, мы разобрались со всем этим.

И всем стало хорошо?

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

Оказалось, история была следующей. У проекта изначально были некоторые бизнес-проблемы, в которых видели техническую причину. Один из разработчиков тогда сказал, что причина, почему так все плохо в монолите. В тот же день поставили задачу переезд на микросервисы. И действительно даже начали распил и частично заехали в Docker SWARM. Только счастье все равно не наступало, просвета не наблюдалось, проблемы не решались. Тогда решили, что проблема в Docker Swarm. И обратились к нам.

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

Что именно вы делали?

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

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

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

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

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

Приведи пример.

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

Подведём итог. Рецепт распила монолита состоит в следующем: компетентные кадры, грамотное планирование, четкое понимание целей и гибкость в работе. Ничего не упустил?

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

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

Перевод Карманная книга по TypeScript. Часть 7. Классы

18.06.2021 12:04:53 | Автор: admin

image


Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript".

Другие части:



Члены класса (class members)


Вот пример самого простого класса пустого:


class Point {}

Такой класс бесполезен, поэтому давайте добавим ему несколько членов.


Поля (fields)


Поле это открытое (публичное) и доступное для записи свойство класса:


class Point {x: numbery: number}const pt = new Point()pt.x = 0pt.y = 0

Аннотация типа является опциональной (необязательной), но неявный тип будет иметь значение any.


Поля могут иметь инициализаторы, которые автоматически запускаются при инстанцировании класса:


class Point {x = 0y = 0}const pt = new Point()// Вывод: 0, 0console.log(`${pt.x}, ${pt.y}`)

Как и в случае с const, let и var, инициализатор свойства класса используется для предположения типа этого свойства:


const pt = new Point()pt.x = '0'// Type 'string' is not assignable to type 'number'.// Тип 'string' не может быть присвоен типу 'number'

--strictPropertyInitialization


Настройка strictPropertyInitialization определяет, должны ли поля класса инициализироваться в конструкторе.


class BadGreeter {name: string// Property 'name' has no initializer and is not definitely assigned in the constructor.// Свойство 'name' не имеет инициализатора и ему не присваивается значения в конструкторе}class GoodGreeter {name: stringconstructor() {this.name = 'привет'}}

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


Если вы намерены инициализировать поле вне конструктора, можете использовать оператор утверждения определения присвоения (definite assignment assertion operator, !):


class OKGreeter {// Не инициализируется, но ошибки не возникаетname!: string}

readonly


Перед названием поля можно указать модификатор readonly. Это запретит присваивать полю значения за пределами конструктора.


class Greeter {readonly name: string = 'народ'constructor(otherName?: string) {if (otherName !== undefined) {this.name = otherName}}err() {this.name = 'не ok'// Cannot assign to 'name' because it is a read-only property.// Невозможно присвоить значение свойству 'name', поскольку оно является доступным только для чтения}}const g = new Greeter()g.name = 'тоже не ok'// Cannot assign to 'name' because it is a read-only property.

Конструкторы (constructors)


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


class Point {x: numbery: number// Обычная сигнатура с дефолтными значениямиconstructor(x = 0, y = 0) {this.x = xthis.y = y}}class Point {// Перегрузкиconstructor(x: number, y: string)constructor(s: string)constructor(xs: any, y?: any) {// ...}}

Однако, между сигнатурами конструктора класса и функции существует несколько отличий:


  • Конструкторы не могут иметь параметров типа это задача возлагается на внешнее определение класса, о чем мы поговорим позже


  • Конструкторы не могут иметь аннотацию возвращаемого типа всегда возвращается тип экземпляра класса



super


Как и в JS, при наличии базового класса в теле конструктора, перед использованием this необходимо вызывать super():


class Base {k = 4}class Derived extends Base {constructor() {// В ES5 выводится неправильное значение, в ES6 выбрасывается исключениеconsole.log(this.k)// 'super' must be called before accessing 'this' in the constructor of a derived class.// Перед доступом к 'this' в конструкторе или производном классе необходимо вызвать 'super'super()}}

В JS легко забыть о необходимости вызова super, в TS почти невозможно.


Методы (methods)


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


class Point {x = 10y = 10scale(n: number): void {this.x *= nthis.y *= n}}

Как видите, TS не добавляет к методам ничего нового.


Обратите внимание, что в теле метода к полям и другим методам по-прежнему следует обращаться через this. Неквалифицированное название (unqualified name) в теле функции всегда будет указывать на лексическое окружение:


let x: number = 0class C {x: string = 'привет'm() {// Здесь мы пытаемся изменить значение переменной `x`, находящейся на первой строке, а не свойство классаx = 'world'// Type 'string' is not assignable to type 'number'.}}

Геттеры/сеттеры


Классы могут иметь акцессоры (вычисляемые свойства, accessors):


class C {_length = 0get length() {return this._length}set length(value) {this._length = value}}

TS имеет несколько специальных правил, касающихся предположения типов в случае с акцессорами:


  • Если set отсутствует, свойство автоматически становится readonly


  • Параметр типа сеттера предполагается на основе типа, возвращаемого геттером


  • Если параметр сеттера имеет аннотацию типа, она должна совпадать с типом, возвращаемым геттером


  • Геттеры и сеттеры должны иметь одинаковую видимость членов (см. ниже)



Если есть геттер, но нет сеттера, свойство автоматически становится readonly.


Сигнатуры индекса (index signatures)


Классы могут определять сигнатуры индекса. Они работают также, как сигнатуры индекса в других объектных типах:


class MyClass {[s: string]: boolean | ((s: string) => boolean)check(s: string) {return this[s] as boolean}}

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


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


Как и в других объектно-ориентированных языках, классы в JS могут наследовать членов других классов.


implements


implements используется для проверки соответствия класса определенному interface. При несоответствии класса интерфейсу возникает ошибка:


interface Pingable {ping(): void}class Sonar implements Pingable {ping() {console.log('пинг!')}}class Ball implements Pingable {// Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.// Класс 'Ball' некорректно реализует интерфейс 'Pingable'. Свойство 'ping' отсутствует в типе 'Ball', но является обязательным в типе 'Pingable'pong() {console.log('понг!')}}

Классы могут реализовывать несколько интерейсов одновременно, например, class C implements A, B {}.


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


Важно понимать, что implements всего лишь проверяет, соответствует ли класс определенному интерфейсу. Он не изменяет тип класса или его методов. Ошибочно полагать, что implements изменяет тип класса это не так!


interface Checkable {check(name: string): boolean}class NameChecker implements Checkable {check(s) {// Parameter 's' implicitly has an 'any' type.// Неявным типом параметра 's' является 'any'// Обратите внимание, что ошибки не возникаетreturn s.toLowercse() === 'ok'// any}}

В приведенном примере мы, возможно, ожидали, что тип s будет определен на основе name: string в check. Это не так implements не меняет того, как проверяется тело класса или предполагаются его типы.


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


interface A {x: numbery?: number}class C implements A {x = 0}const c = new C()c.y = 10// Property 'y' does not exist on type 'C'.// Свойства с названием 'y' не существует в типе 'C'

extends


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


class Animal {move() {console.log('Moving along!')}}class Dog extends Animal {woof(times: number) {for (let i = 0; i < times; i++) {console.log('woof!')}}}const d = new Dog()// Метод базового классаd.move()// Метод производного классаd.woof(3)

Перезапись методов


Производный класс может перезаписывать свойства и методы базового класса. Для доступа к методам базового класса можно использовать синтаксис super. Поскольку классы в JS это всего лишь объекты для поиска (lookup objects), такого понятия как супер-поле не существует.


TS обеспечивает, чтобы производный класс всегда был подтипом базового класса.


Пример легального способа перезаписи метода:


class Base {greet() {console.log('Привет, народ!')}}class Derived extends Base {greet(name?: string) {if (name === undefined) {super.greet()} else {console.log(`Привет, ${name.toUpperCase()}`)}}}const d = new Derived()d.greet()d.greet('читатель!')

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


// Создаем синоним для производного экземпляра с помощью ссылки на базовый классconst b: Base = d// Все работаетb.greet()

Что если производный класс не будет следовать конракту базового класса?


class Base {greet() {console.log('Привет, народ!')}}class Derived extends Base {// Делаем этот параметр обязательнымgreet(name: string) {// Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.// Свойство 'greet' в типе 'Derived' не может быть присвоено одноименному свойству в базовом типе 'Base'...console.log(`Привет, ${name.toUpperCase()}`)}}

Если мы скомпилируем этот код, несмотря на ошибку, такой сниппет провалится:


const b: Base = new Derived()// Не работает, поскольку `name` имеет значение `undefined`b.greet()

Порядок инициализации


Порядок инициализации классов может быть неожиданным. Рассмотрим пример:


class Base {name = 'базовый'constructor() {console.log('Меня зовут ' + this.name)}}class Derived extends Base {name = 'производный'}// Вывод: 'базовый', а не 'производный'const d = new Derived()

Что здесь происходит?


Порядок инициализации согласно спецификации следующий:


  • Инициализация полей базового класса


  • Запуск конструктора базового класса


  • Инициализация полей производного класса


  • Запуск конструктора производного класса



Это означает, что конструктор базового класса использует собственное значение name, поскольку поля производного класса в этот момент еще не инициализированы.


Наследование встроенных типов


В ES2015 конструкторы, неявно возвращающие объекты, заменяют значение this для любого вызова super. Для генерируемого конструктора важно перехватывать потенциальное значение, возвращаемое super, и заменять его значением this.


Поэтому подклассы Error, Array и др. могут работать не так, как ожидается. Это объясняется тем, что Error, Array и др. используют new.target из ES6 для определения цепочки прототипов; определить значение new.target в ES5 невозможно. Другие компиляторы, обычно, имеют такие же ограничения.


Для такого подкласса:


class MsgError extends Error {constructor(m: string) {super(m)}sayHello() {return 'Привет ' + this.message}}

вы можете обнаружить, что:


  • методы объектов, возвращаемых при создании подклассов, могут иметь значение undefined, поэтому вызов sayHello завершится ошибкой


  • instanceof сломается между экземплярами подкласса и их экземплярами, поэтому (new MsgError()) instanceof MsgError возвращает false



Для решения данной проблемы можно явно устанавливать прототип сразу после вызова super.


class MsgError extends Error {constructor(m: string) {super(m)// Явно устанавливаем прототипObject.setPrototypeOf(this, MsgError.prototype)}sayHello() {return 'Привет ' + this.message}}

Тем не менее, любой подкласс MsgError также должен будет вручную устанавливать прототип. В среде выполнения, в которой не поддерживается Object.setPrototypeOf, можно использовать __proto__.


Видимость членов (member visibility)


Мы можем использовать TS для определения видимости методов и свойств для внешнего кода, т.е. кода, находящегося за пределами класса.


public


По умолчанию видимость членов класса имеет значение public. Публичный член доступен везде:


class Greeter {public greet() {console.log('Привет!')}}const g = new Greeter()g.greet()

Поскольку public является дефолтным значением, специально указывать его не обязательно, но это повышает читаемость и улучшает стиль кода.


protected


Защищенные члены видимы только для подклассов класса, в котором они определены.


class Greeter {public greet() {console.log('Привет, ' + this.getName())}protected getName() {return 'народ!'}}class SpecialGreeter extends Greeter {public howdy() {// Здесь защищенный член доступенconsole.log('Здорово, ' + this.getName())}}const g = new SpecialGreeter()g.greet() // OKg.getName()// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.// Свойство 'getName' является защищенным и доступно только в классе 'Greeter' и его подклассах

Раскрытие защищенных членов


Производные классы должны следовать контракту базового класса, но могут расширять подтипы базового класса дополнительными возможностями. Это включает в себя перевод protected членов в статус public:


class Base {protected m = 10}class Derived extends Base {// Модификатор отсутствует, поэтому значением по умолчанию является `public`m = 15}const d = new Derived()console.log(d.m) // OK

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


Доступ к защищенным членам за пределами иерархии классов


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


class Base {protected x: number = 1}class Derived1 extends Base {protected x: number = 5}class Derived2 extends Base {f1(other: Derived2) {other.x = 10}f2(other: Base) {other.x = 10// Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.// Свойство 'x' является защищенным и доступно только через экземпляр класса 'Derived2'. А это  экземпляр класса 'Base'}}

Java, например, считает такой подход легальным, а C# и C++ нет.


TS считает такой подход нелегальным, поскольку доступ к x из Derived2 должен быть легальным только в подклассах Derived2, а Derived1 не является одним из них.


private


Частные члены похожи на защищенные, но не доступны даже в подклассах, т.е. они доступны только в том классе, где они определены.


class Base {private x = 0}const b = new Base()// Снаружи класса доступ получить нельзяconsole.log(b.x)// Property 'x' is private and only accessible within class 'Base'.class Derived extends Base {showX() {// В подклассе доступ получить также нельзяconsole.log(this.x)// Property 'x' is private and only accessible within class 'Base'.}}

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


class Base {private x = 0}class Derived extends Base {// Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.// Класс 'Derived' неправильно расширяет базовый класс 'Base'. Свойство 'x' является частным в типе 'Base', но не в типе 'Derived'x = 1}

Доступ к защищенным членам между экземплярами


Разные языки ООП также по-разному подходят к предоставлению доступа экземплярам одного класса к защищенным членам друг друга. Такие языки как Java, C#, C++, Swift и PHP разрешают такой доступ, а Ruby нет.


TS разрешает такой доступ:


class A {private x = 10public sameAs(other: A) {// Ошибки не возникаетreturn other.x === this.x}}

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


Подобно другим аспектам системы типов TS, private и protected оказывают влияние на код только во время проверки типов. Это означает, что конструкции вроде in или простой перебор свойств имеют доступ к частным и защищенным членам:


class MySafe {private secretKey = 12345}// В JS-файле...const s = new MySafe()// Вывод 12345console.log(s.secretKey)

Для реализации настоящих частных членов можно использовать такие механизмы, как замыкания (closures), слабые карты (weak maps) или синтаксис приватных полей класса (private fields, #).


Статические члены (static members)


В классах могут определеяться статические члены. Такие члены не связаны с конкретными экземплярами класса. Они доступны через объект конструктора класса:


class MyClass {static x = 0static printX() {console.log(MyClass.x)}}console.log(MyClass.x)MyClass.printX()

К статическим членам также могут применяться модификаторы public, protected и private:


class MyClass {private static x = 0}console.log(MyClass.x)// Property 'x' is private and only accessible within class 'MyClass'.

Статические члены наследуются:


class Base {static getGreeting() {return 'Привет, народ!'}}class Derived extends Base {myGreeting = Derived.getGreeting()}

Специальные названия статических членов


Изменение прототипа Function считается плохой практикой. Поскольку классы это функции, вызываемые с помощью new, некоторые слова нельзя использовать в качестве названий статических членов. К таким словам относятся, в частности, свойства функций name, length и call:


class S {static name = 'S!'// Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.// Статическое свойство 'name' вступает в конфликт со встроенным свойством 'Function.name' функции-конструктора 'S'}

Почему не существует статических классов?


В некоторых языках, таких как C# или Java существует такая конструкция, как статический класс (static class).


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


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


// Ненужный статический классclass MyStaticClass {static doSomething() {}}// Альтернатива 1function doSomething() {}// Альтернатива 2const MyHelperObject = {dosomething() {},}

Общие классы (generic classes)


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


class Box<Type> {contents: Typeconstructor(value: Type) {this.contents = value}}const b = new Box('Привет!')// const b: Box<string>

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


Параметр типа в статических членах


Следующий код, как ни странно, является НЕлегальным:


class Box<Type> {static defaultValue: Type// Static members cannot reference class type parameters.// Статические члены не могут ссылаться на типы параметров класса}

Запомните, что типы полностью удаляются! Во время выполнения существует только один слот Box.defaultValue. Это означает, что установка Box<string>.defaultValue (если бы это было возможным) изменила бы Box<number>.defaultValue, что не есть хорошо. Поэтому статические члены общих классов не могут ссылаться на параметры типа класса.


Значение this в классах во время выполнения кода


TS не изменяет поведения JS во время выполнения. Обработка this в JS может показаться необычной:


class MyClass {name = 'класс'getName() {return this.name}}const c = new MyClass()const obj = {name: 'объект',getName: c.getName,}// Выводится 'объект', а не 'класс'console.log(obj.getName())

Если кратко, то значение this внутри функции зависит от того, как эта функция вызывается. В приведенном примере, поскольку функция вызывается через ссылку на obj, значением this является obj, а не экземпляр класса.


TS предоставляет некоторые средства для изменения такого поведения.


Стрелочные функции


Если у вас имеется функция, которая часто будет вызываться способом, приводящим к потере контекста, имеет смысл определить такое свойство в виде стрелочной функции:


class MyClass {name = 'класс'getName = () => {return this.name}}const c = new MyClass()const g = c.getName// Выводится 'класс'console.log(g())

Это требует некоторых компромиссов:


  • Значение this будет гарантированно правильным во время выполнения, даже в коде, не прошедшем проверки с помощью TS


  • Будет использоваться больше памяти, поскольку для каждого экземпляра класса будет создаваться новая функция


  • В производном классе нельзя будет использовать super.getName, поскольку отсутствует входная точка для получения метода базового класса в цепочке прототипов



Параметры this


При определении метода или функции начальный параметр под названием this имеет особое значение в TS. Данный параметр удаляется во время компиляции:


// TSfunction fn(this: SomeType, x: number) {/* ... */}// JSfunction fn(x) {/* ... */}

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


class MyClass {name = 'класс'getName(this: MyClass) {return this.name}}const c = new MyClass()// OKc.getName()// Ошибкаconst g = c.getNameconsole.log(g())// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.// Контекст 'this' типа 'void' не может быть присвоен методу 'this' типа 'MyClass'

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


  • Мы все еще имеем возможность вызывать метод неправильно


  • Выделяется только одна функция для каждого определения класса, а не для каждого экземпляра класса


  • Базовые определения методов могут по-прежнему вызываться через super



Типы this


В классах специальный тип this динамически ссылается на тип текущего класса:


class Box {contents: string = ''set(value: string) {// (method) Box.set(value: string): thisthis.contents = valuereturn this}}

Здесь TS предполагает, что типом this является тип, возвращаемый set, а не Box. Создадим подкласс Box:


class ClearableBox extends Box {clear() {this.contents = ''}}const a = new ClearableBox()const b = a.set('привет')// const b: ClearableBox

Мы также можем использовать this в аннотации типа параметра:


class Box {content: string = ''sameAs(other: this) {return other.content === this.content}}

Это отличается от other: Box если у нас имеется производный класс, его метод sameAs будет принимать только другие экземпляры этого производного класса:


class Box {content: string = ''sameAs(other: this) {return other.content === this.content}}class DerivedBox extends Box {otherContent: string = '?'}const base = new Box()const derived = new DerivedBox()derived.sameAs(base)// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.

Основанные на this защитники типа


Мы можем использовать this is Type в качестве возвращаемого типа в методах классов и интерфейсах. В сочетании с сужением типов (например, с помощью инструкции if), тип целевого объекта может быть сведен к более конкретному Type.


class FileSystemObject {isFile(): this is FileRep {return this instanceof FileRep}isDirectory(): this is Directory {return this instanceof Directory}isNetworked(): this is Networked & this {return this.networked}constructor(public path: string, private networked: boolean) {}}class FileRep extends FileSystemObject {constructor(path: string, public content: string) {super(path, false)}}class Directory extends FileSystemObject {children: FileSystemObject[]}interface Networked {host: string}const fso: FileSystemObject = new FileRep('foo/bar.txt', 'foo')if (fso.isFile()) {fso.content// const fso: FileRep} else if (fso.isDirectory()) {fso.children// const fso: Directory} else if (fso.isNetworked()) {fso.host// const fso: Networked & FileSystemObject}

Распространенным случаем использования защитников или предохранителей типа (type guards) на основе this является ленивая валидация определенного поля. В следующем примере мы удаляем undefined из значения, содержащегося в box, когда hasValue проверяется на истинность:


class Box<T> {value?: ThasValue(): this is { value: T } {return this.value !== undefined}}const box = new Box()box.value = 'Gameboy'box.value// (property) Box<unknown>.value?: unknownif (box.hasValue()) {box.value// (property) value: unknown}

Свойства параметров


TS предоставляет специальный синтаксис для преобразования параметров конструктора в свойства класса с аналогичными названиями и значениями. Это называется свойствами параметров (или параметризованными свойствами), такие свойства создаются с помощью добавления модификаторов public, private, protected или readonly к аргументам конструктора. Создаваемые поля получают те же модификаторы:


class Params {constructor(public readonly x: number,protected y: number,private z: number) {// ...}}const a = new Params(1, 2, 3)console.log(a.x)// (property) Params.x: numberconsole.log(a.z)// Property 'z' is private and only accessible within class 'Params'.

Выражения классов (class expressions)


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


const someClass = class<Type> {content: Typeconstructor(value: Type) {this.content = value}}const m = new someClass('Привет, народ!')// const m: someClass<string>

Абстрактные классы и члены


Классы, методы и поля в TS могут быть абстрактными.


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


Абстрактные классы выступают в роли базовых классов для подклассов, которые реализуют абстрактных членов. При отсутствии абстрактных членов класс считается конкретным (concrete).


Рассмотрим пример:


abstract class Base {abstract getName(): stringprintName() {console.log('Привет, ' + this.getName())}}const b = new Base()// Cannot create an instance of an abstract class.// Невозможно создать экземпляр абстрактного класса

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


class Derived extends Base {getName() {return 'народ!'}}const d = new Derived()d.printName()

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


class Derived extends Base {// Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.// Неабстрактный класс 'Derived' не реализует унаследованный от класса 'Base' абстрактный член 'getName'// Забыли про необходимость реализации абстрактных членов}

Сигнатуры абстрактных конструкций (abstract construct signatures)


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


Рассмотрим пример:


function greet(ctor: typeof Base) {const instance = new ctor()// Cannot create an instance of an abstract class.instance.printName()}

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


// Плохо!greet(Base)

Вместо этого, мы можем написать функцию, которая принимает нечто с сигнатурой конструктора:


function greet(ctor: new () => Base) {const instance = new ctor()instance.printName()}greet(Derived)greet(Base)/*Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.Cannot assign an abstract constructor type to a non-abstract constructor type.*//*Аргумент типа 'typeof Base' не может быть присвоен параметру типа 'new () => Base'.Невозможно присвоить тип абстрактного конструктора типу неабстрактного конструктора*/

Теперь TS правильно указывает нам на то, какой конструктор может быть вызван Derived может, а Base нет.


Отношения между классами


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


Например, следующие два класса являются взаимозаменяемыми, поскольку они идентичны:


class Point1 {x = 0y = 0}class Point2 {x = 0y = 0}// OKconst p: Point1 = new Point2()

Также существуют отношения между подтипами, даже при отсутствии явного наследования:


class Person {name: stringage: number}class Employee {name: stringage: numbersalary: number}// OKconst p: Person = new Employee()

Однако, существует одно исключение.


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


class Empty {}function fn(x: Empty) {// С `x` можно делать что угодно}// OK!fn(window)fn({})fn(fn)



Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Recovery mode Странные шахматы как тестовое задание

17.06.2021 22:13:59 | Автор: admin

Добрый вечер хаброжители!

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

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

Управление я добавил мышью, а играть против компьютерного алгоритма. Черные - человек, белые - ИИ.

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

Немного кода для наглядности:

Game::Game(){run = true;//флаг признак нажатия кнопки выхода F5Matrix = new int* [8];//Поле 64 ячейки - значения 0 - для пустой ячейки, для игрока каждая пешка-шашка от 1 до 9, для компьютера значения в матрице от 10 до 18for (int i = 0; i < 8; i++)Matrix[i] = new int[8];//Квадраты координат нужны чтобы программа знала какие ячейки над указателем мыши, 64 квадратаQuadCoorXleft = new int* [8];//каждой ячейки матрицы Matrix соответстует квадрат координат для мыши xleft означает левую координату xQuadCoorXright = new int* [8];//xright - правая xQuadCoorYdown = new int* [8];//верхняя y координатаQuadCoorYup = new int* [8];//нижняя y координатаfor (int i = 0; i < 8; i++){QuadCoorXleft[i] = new int[8];QuadCoorXright[i] = new int[8];QuadCoorYdown[i] = new int[8];QuadCoorYup[i] = new int[8];}//Координаты пешек для отрисовкиChessX = new double[18];//XChessY = new double[18];//Y//Выделяемая пешка ее координаты и значенияActiveX = -1;//XActiveY = -1;//YActive = -1;//Valuefirstplayer = true;//флаг того что можете игрок 1й ходитьsecondplayer = false;//флаг того что можете игрок 2й ходитьai = new bool[18];//ячейки флаги того что пешка на финишной позицииchessai tmp;for (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i+1;Ai.push_back(tmp);//Вектор с флагами финиша каждой пешки для искуственного интеллекта}}aicountfirstrow = 0;//счетчик кол-ва пешек ИИ(искуственного интеллекта) на верхней строчке(0-я)aicountsecondrow = 0;//счетчик кол-ва пешек ИИ на предверхней строчке(1-я)aicountthirdrow = 0;//счетчик кол-ва пешек ИИ на предпредверхней строчке(2-я)}

Для отрисовки и захвата мыши используется библиотеки OpenGL и SDL2.

void Draw_Circle(){//Отрисовка круга(пешек-шахмат) черногоfor (int i = 0; i <= 50; i++) {float a = (float)i / 50.0f * 3.1415f * 2.0f;glVertex2f(cos(a), sin(a));}}void Draw_Circle_Fill(){//Отрисовка круга(пешек-шахмат) белогоfor (int i = 0; i <= 50; i++) {float a = (float)i / 50.0f * 3.1415f * 2.0f;glVertex2f(0.0, 0.0);glVertex2f(cos(a), sin(a));}}

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

...for (int i = 0; i < 9; i++){glPushMatrix();glTranslatef(ChessX[i], ChessY[i], 0);glScalef(0.05, 0.05, 1);glBegin(GL_LINE_LOOP);Draw_Circle();glEnd();glPopMatrix();}//Рисуем белые пешки ИИfor (int i = 9; i < 18; i++){glPushMatrix();glTranslatef(ChessX[i], ChessY[i], 0);glScalef(0.05, 0.05, 1);glBegin(GL_LINES);Draw_Circle_Fill();glEnd();glPopMatrix();}...

Ходы игрока:

void Game::Move_Up(){//Ход игрока вверхif (Active > 0 && ActiveX != 0 && Matrix[ActiveX-1][ActiveY] == 0)//Если выделенная пешка и не самая верхняя строчка и ячейка выше пустая{Matrix[ActiveX-1][ActiveY] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке выше текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessY[Active-1] += 0.2;//перемещаем координату У пешки вверх для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << " Player MoveUp " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Down(){//Ход игрока внизif (Active > 0 && ActiveX != 7 && Matrix[ActiveX+1][ActiveY] == 0)//Если выделенная пешка и не самая нижняя строчка и ячейка ниже пустая{Matrix[ActiveX+1][ActiveY] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке ниже текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessY[Active-1] -= 0.2;//перемещаем координату У пешки вниз для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "Player MoveDown " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Right(){//Ход игрока вправоif (Active > 0 && ActiveY != 7 && Matrix[ActiveX][ActiveY+1] == 0)//Если выделенная пешка и не самая правая строчка и ячейка справа пустая{Matrix[ActiveX][ActiveY+1] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке справа текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessX[Active-1] += 0.2;//перемещаем координату Х пешки вправо для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "MoveRight " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Left(){//Ход игрока влево if (Active > 0 && ActiveY != 0 && Matrix[ActiveX][ActiveY-1] == 0)//Если выделенная пешка и не самая левая строчка и ячейка слева пустая{Matrix[ActiveX][ActiveY-1] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке слева текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessX[Active-1] -= 0.2;//перемещаем координату Х пешки влево для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "MoveLeft " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}

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

void Game::ReccurentWalk(){//Реккурентный ход ИИcurrent = -1, currentI = -1, currentJ = -1;//изначально выделенная пешка не определенаfor (int i = 0; i < Ai.size(); i++)//поиск по массивуif (!Ai[i].ai)//если не завершены ходы для конкретных пешек{if (Check_MoveUp(Ai[i].value) || Check_MoveLeft(Ai[i].value))//Можно ли походить вверх или влево?{current = Ai[i].value;//запоминаем текущую пешкуbreak;}else{//Если походить нельзя стираем из массива ходов пешкуstd::vector<chessai>::iterator position = std::find_if(Ai.begin(), Ai.end(), find_s(Ai[i].value));if (position != Ai.end()) // == vector.end() means the element was not foundAi.erase(position);}}for (int i = 0; i < 8; i++)for (int j = 0; j < 8; j++)if (Matrix[i][j] == current)//ищем в матрице пешку и запоминаем индексы{currentI = i;currentJ = j;break;}if (currentI != -1 && currentJ != -1)//если какая либо найдена ходим либо вверх либо влево{if (!Move_UpAI(currentI, currentJ))if (!Move_LeftAI(currentI, currentJ)){ReccurentWalk();}}else{//если не найдена заполняем массив ходов снова пешкамиchessai tmp;for (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i + 1;Ai.push_back(tmp);}}//ищем ту которая может походить вправо или внизfor (int i = 0; i < Ai.size(); i++)if (!Ai[i].ai){if (Check_MoveRight(Ai[i].value) || Check_MoveDown(Ai[i].value)){current = Ai[i].value;break;}else{//если не может то стираем из массиваstd::vector<chessai>::iterator position = std::find_if(Ai.begin(), Ai.end(), find_s(Ai[i].value));if (position != Ai.end()) // == Vector.end() means the element was not foundAi.erase(position);}}//ищем ее индексы в матрицеfor (int i = 0; i < 8; i++)for (int j = 0; j < 8; j++)if (Matrix[i][j] == current){currentI = i;currentJ = j;break;}//ходим вправо или внизif(!Move_RightAI(currentI, currentJ))if (!Move_DownAI(currentI, currentJ)){std::cout <<"Artificial Intellegence asked: WTF?" << std::endl;}}chessai tmp;if(Ai.empty())//если список ходов пуст заполняем снова всемиfor (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i + 1;Ai.push_back(tmp);}}}

Ну и собственно опишу словами, что делает ИИ. Он собирает три пешки вверху на 0-ой строке и если ходить нельзя влево, то двигает вверх остальные 3 пешки на 1 строку, аналогично и на 2-ю строку. Если ходить влево и вверх нельзя, то он берет любую шашку и двигает либо вниз либо вправо если можно конечно, то есть стремится всегда двигать влево или вверх( при условии что выше меньше двух шашек в строке).

Ну собственно и геймплей:

https://youtu.be/XaQVeSKdQcs

И ссылка на исходный код:

https://github.com/Beginerok/DominiGames

Подробнее..

Перевод Повышение производительности дебажных билдов в два-три раза

18.06.2021 02:21:22 | Автор: admin

Нам удалось добиться значительного повышения производительности рантайма для дебажной (отладочной) конфигурации по умолчанию Visual Studio в компиляторе C++ для x86/x64. Для программ, скомпилированных в режиме дебага в Visual Studio 2019 версии 16.10 Preview 2, мы отмечаем ускорение в 23 раза. Эти улучшения связаны с уменьшением накладных расходов на проверки ошибок в рантайме (/RTC), которые включены по умолчанию.

Дебажная конфигурация по умолчанию (Default debug configuration)

Когда вы компилируете свой код в Visual Studio с дебажной конфигурацией, по умолчанию компилятору C++ передаются некоторые флаги. Наиболее релевантными для этой статьи являются /RTC1, /JMC и /ZI.

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

Рассмотрим следующую простую функцию:

int foo() {    return 32;}

и сборку для x64, сгенерированную компилятором 16.9 при компиляции с флагами /RTC1 /JMC /ZI (ссылка Godbolt):

int foo(void) PROC                  $LN3:        push rbp        push rdi        sub rsp, 232                ; дополнительное пространство, выделенное из-за /ZI, /JMC        lea rbp, QWORD PTR [rsp+32]        mov rdi, rsp        mov ecx, 58                 ; (= x)        mov eax, -858993460         ; 0xCCCCCCCC        rep stosd                   ; записать 0xCC в стек для x DWORDов        lea rcx, OFFSET FLAT:__977E49D0_example@cpp        ; вызов из-за /JMC        call __CheckForDebuggerJustMyCode        mov eax, 32        lea rsp, QWORD PTR [rbp+200]        pop rdi        pop rbp        ret 0 int foo(void) ENDP

В показанной выше сборке флаги /JMC и /ZI добавляют в сумме 232 дополнительных байта в стек (строка 5). Это пространство в стеке не всегда необходимо. В сочетании с флагом /RTC1, который инициализирует выделенное пространство стека (строка 10), это потребляет много тактов ЦП. В этом конкретном примере, хоть выделенное пространство стека необходимо для правильного функционирования /JMC и /ZI, его инициализация - нет. Мы можем убедиться во время компиляции, что в этих проверках нет необходимости. Таких функций предостаточно в любой реальной кодовой базе на C++ - отсюда и выигрыш в производительности.

Далее мы глубже погрузимся в каждый из этих флагов, их взаимодействие с /RTC1 и узнаем, как мы избегаем ненужных накладных расходов.

/RTC1

Использование флага /RTC1 эквивалентно использованию обоих флагов /RTCs и /RTCu. /RTCs инициализирует стек функций с 0xCC для выполнения различных проверок в рантайме, а именно обнаружения неинициализированных локальных переменных, обнаружения переполнения или недозаполнения массива и проверки указателя стека (для x86). Вы можете посмотреть код, раздутый /RTC, здесь.

Как видно из приведенного выше ассемблерного кода (строка 10), инструкция rep stosd, внесенная /RTCs, является основной причиной замедления работы. Ситуация усугубляется, когда /RTC (или /RTC1) используется вместе с /JMC, /ZI или обоими.

Взаимодействие с /JMC

/JMC означает Just My Code Debugging (функциональ дебага только моего кода), и во время отладки он автоматически пропускает функции, написанные не вами (например, фреймворк, библиотека и другой непользовательский код). Он работает, вставляя вызов функции в пролог, который вызывает рантайм библиотеку. Это помогает дебагеру различать пользовательский и непользовательский код. Проблема здесь в том, что вставка вызова функции в пролог каждой функции в вашем проекте означает, что во всем вашем проекте больше не будет листовых функций (leaf functions). Если функции изначально не нужен какой-либо стек фрейм, теперь он ей будет нужен, потому что в соответствии с AMD64 ABI для Windows платформ нам нужно иметь по крайней мере четыре слота стека, доступные для параметров функции (так называемая домашняя область параметров - Param Home area). Это означает, что все функции, которые ранее не инициализировались /RTC, потому что они были листовыми функциями и не имели стек фрейма, теперь будут инициализированы. Наличие множества листовых функций в вашей программе - это нормально, особенно если вы используете сильно шаблонную библиотеку, такую как STL. В этом случае /JMC с радостью съест часть ваших тактов ЦП. Это не относится к x86 (32 бит), потому что там у нас нет домашней области параметров. Вы можете посмотреть эффекты /JMC здесь.

Взаимодействие с /ZI

Следующее взаимодействие, о котором мы поговорим, будет с /ZI. Он позволяет вашему коду использовать функцию Edit and Continue (изменить и продолжить), что означает, что вам не нужно перекомпилировать всю программу во время дебага для небольших изменений.

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

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

Решение

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

Ситуация немного усложняется, когда вы компилируете с edit-and-continue, потому что теперь вы можете добавлять неинициализированные переменные в процессе дебага, которые могут быть обнаружены только в том случае, если мы инициализируем область стека. А мы, скорее всего, этого не сделали. Чтобы решить эту проблему, мы включили необходимые биты в дебажную информацию и предоставили ее через Debug Interface Access SDK. Эта информация сообщает дебагеру, где область заполнения, введенная /ZI начинается и заканчивается. Она также сообщает дебагеру, нужна ли функции инициализация стека. Если да, то отладчик безоговорочно инициализирует область стека в этом диапазоне памяти для функций, которые вы редактировали во время сеанса дебаггинга. Новые переменные всегда размещаются поверх этой инициализированной области, и наши проверки в рантайме теперь могут определить, безопасен ли ваш недавно добавленный код или нет.

Результаты

Мы скомпилировали следующие проекты в конфигурации отладки по умолчанию, а затем использовали сгенерированные исполняемые файлы для проведения тестов. Мы заметили 23-кратное улучшение во всех проектах, которые мы тестировали. Для проектов с сильным использованием STL могут потребоваться более значительные улучшения. Сообщите нам в комментариях о любых улучшениях, которые вы заметили в своих проектах. Проект 1 и Проект 2 предоставлены пользователями.

Расскажите нам, что вы думаете!

Мы надеемся, что это ускорение сделает ваш рабочий процесс дебаггинга эффективным и приятным. Мы постоянно прислушиваемся к вашим отзывам и работаем над улучшением вашего рабочего цикла. Мы хотели бы услышать о вашем опыте в комментариях ниже. Вы также можете связаться с нами в сообществе разработчиков, по электронной почте (visualcpp@microsoft.com) и в Twitter (@VisualC).


Напоминаем о том, что сегодня, в рамках курса "C++ Developer. Basic" пройдет второй день бесплатного интенсива по теме: "HTTPS и треды в С++. От простого к прекрасному".

Подробнее..

Вебинар Вычисляем на видеокартах. Технология OpenCL

18.06.2021 12:04:53 | Автор: admin
22 июня в 18.30 (Мск) Яндекс.Практикум проведет открытый вебинар Вычисляем на видеокартах. Технология OpenCL. На вебинаре расскажем, как использовать видеокарту в качестве полноценного вычислительного устройства, мощности которого чаще всего простаивают.

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



Для кого?


  • Для программистов, которые хотят освоить новую технологию и кардинально улучшить производительность программ
  • Для учёных, которым не хватает вычислительных мощностей для решения своих научных задач
  • Для всех, кому интересно, почему видеокарты завоевали мир и в чём секрет успешного майнинга


В программе вебинара


1. История возникновения и особенности видеокарт


Почему устройство, созданное для графики в играх, идеально для прогнозирования погоды, майнинга криптовалюты и deep learning

2. Архитектура видеокарты


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

3. Конкретные цифры: обзор технологий


  • CUDA vs OpenCL. Преимущества и недостатки этих технологий
  • Чем отличаются видеокарты компаний NVidia и AMD Radeon


4. Как программировать на OpenCL


  • Пишем первую программу для видеокарты
  • Разберём, что можно, а что нельзя на OpenCL
  • Посмотрим, как использовать OpenCL не только с C++, но и с другими языками программирования


5. Популярные алгоритмы


  • Базовые алгоритмы и принципы программирования через призму видеокарты
  • Разбор типичных проблем


6. Отладка, обфускация, тулинг


  • Что делать, если ваш код BSODит. Отладка на видеокарте: миф или реальность
  • Профилируем OpenCL и делаем его быстрее
  • Как защитить свой код: пара слов об обфускации


7. Q&A-сессия



Ведущий Георгий xjossy Осипов, разработчик в Лаборатории компьютерной графики и мультимедиа ВМК МГУ и автор факультета Разработчик C++ в Яндекс.Практикуме

Вебинар пройдёт 22 июня в 18.30 (Мск). Подробности и регистрация
Подробнее..

Оцениваем открытые и коммерческие цифровые модели рельефа

18.06.2021 14:19:30 | Автор: admin

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


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



Спутниковая карта Google Satellite наложена на детальный рельеф USGD NED DEM 1 м


Введение


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


Рассмотрим ниже некоторые принципиальные особенности цифрового рельефа и далее проверим его качество на примерах от различных производителей.


Заявленное пространственное разрешение рельефа


Пространственное разрешение рельефа, даже построенного по одним и тем же исходным данным, варьируется в широких пределах в зависимости от территории. И, тем более, разное разрешение и точность имеют результаты, полученные разными способами скажем, методом триангуляции космоснимков и с помощью лидарной съемки. К примеру, для территории США доступно множество продуктов рельефа разрешением от 1 м до 30 м, так что вся территория страны покрыта рельефом 30 м и 10 м, а часть территорий доступны с разрешением 5 м и 1 м. Таким образом, единый рельеф разрешением 1 м на всю территорию США будет синтезом разномасштабных рельефов и получившиеся детализация и точность будут варьироваться по территории. В идеале, следовало бы объединять данные в спектральной области или с использованием интерполяции методом ближайшего соседа, на практике же часто используются нелинейные методы интерполяции, так что получившийся продукт содержит широкую полосу мусорных компонент пространственного спектра. При взгляде на такой рельеф становится понятно, что выглядит он как-то не так, но точную оценку можно получить лишь при анализе его пространственного спектра.


Заявленная вертикальная точность рельефа


Точность рельефа может определяться совершенно разными способами, например, как величина ошибки относительно набора референсных точек на поверхности или относительно исходных данных (вопрос точности которых это совсем другая история). Точность может указываться и так, к примеру: ошибка не более 5 м, что на самом деле означает ошибку не более 5 м с доверительным интервалом 95% (или другим), то есть вовсе не гарантирует точность 5 м для любого отдельно взятого пиксела или участка. Поскольку точность оценивается для отдельных пикселов и отдельных участков, для которых есть точные отметки высот, то в пределах большой территории может сильно варьироваться. Например, если 99% рельефа занимает плоская равнина с малыми перепадами высот и, следовательно, высокоточным рельефом, то оставшийся 1% рельефа может иметь точность в 100 раз худшую. Поэтому рельеф заявленной точности 5 м доступен на всю территорию планеты, а 10 см точности только выборочно. Но и это еще не все. Точность рельефа видимой поверхности (Digital Surface Model, DSM) соотносится к точности использованных для создания рельефа данных к измеренной поверхности (в зависимости от местности это может быть лес или скалы и так далее), так что при другом ветре и в другой сезон измеренные значения окажутся далеко за пределами заявленной точности. В случае же рельефа непосредственно поверхности планеты (Digital Terrain Model, DTM) есть разные методы исключения растительности (даже трава и кустарник дают погрешность по высоте более 10 см, не говоря про деревья), а оценка точности производится, как правило, по некоторым референсным точкам только на открытой местности.


Оценка реального пространственного разрешения рельефа


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


Оценка вертикальной точности рельефа


При выполнении анализа и обработки рельефа в частотной области, или домене (Frequency domain) вертикальная точность определяется шириной спектра. Обрезание спектра по реальному разрешению рельефа не изменяет его точности, поскольку обрезаемые компоненты не достоверны и не могут систематически улучшать оцениваемую точность. Это дает следующий критерий корректности величина отклонения рельефа с обрезанным спектром от исходного не должна превышать заявленную точность исходного рельефа. Далее, при наличии референсных наземных точек или точного рельефа (для отдельных участков) можно оценить рельеф стандартным способом вычисления ошибок. Кроме того, возможно использование спутниковой интерферометрии для вычисления вертикальных смещений точек поверхности между двумя моментами времени и сравнение этих смещений для двух рельефов, построенных по тем же данным радарной съемки и для тех же моментов времени.


Глобальный рельеф всей планеты ALOS World 3D 30m (AW3D30) вертикальной точностью 5 м, построенный методом триангуляции. Реальное разрешение 30 м


Это комбинированный открытый продукт размером около 220 ГБ, доступный на сайте производителя ALOS World 3D 30m (AW3D30) и на платформе Google Earth Engine (GEE) как ALOS DSM: Global 30m. Комбинированный он потому, что использует для заполнения пропущенных значений рельефы SRTM 30m, ASTER DEM и другие. На мой взгляд, является лучшим из открытых глобально доступных. Если SRTM содержит серьезные пиксельные артефакты, а ASTER DEM буквально кляксы некорректно интерполированных значений, то ALOS практически не грешит подобными проблемами. Анализ пространственного спектра в Python ноутбуке показывает следующий результат:



Для пространственного спектра рассматриваемого рельефа в двойных логарифмических координатах коэффициент детерминации R-квадрат равен 98% для масштаба от 30 метров. Таким образом, этот рельеф явно получен из более детального и действительно, поставщик также предлагает коммерческий рельеф разрешением 5 м и все той же вертикальной точностью 5 м.


Коммерческий рельеф 1 м, построенный методом триангуляции. Реальное разрешение skipped


Образец коммерческого рельефа со спутникового аппарата Pliades от компании Airbus DS Geo SA получен с официального сайта Elevation1 DSM + Pliades Ortho 0.5m pan-sharpened (Orthoimage included). Для целей тестирования я выбрал участок горной местности почти без техногенных объектов. Обратим внимание на лицензионное соглашение, которое разрешает только внутреннюю техническую оценку продукта ("to use the PRODUCT for internal technical evaluation purposes only") и запрещает публикацию любых результатов ("any derivative product or information"). В связи с этим, я опубликую только Python ноутбук, который вы можете запустить для собственной "внутренней технической оценки продукта", согласно лицензии, и получить точное значение пространственного разрешения. Обратите внимание, что отметка 60 м на графике пространственного разрешения в коде ноутбука поставлена исключительно для удобства оценки фрактальности спектра и я не несу ответственности за то, что она может вам показаться равной реальному пространственному разрешению рассматриваемого рельефа.


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


Рельеф США USGD NED DEM 1 м вертикальным разрешением 10 см, построенный по данным лидарной съемки. Реальное разрешение около 2 м


Предоставляется на Amazon AWS Staged Products Directory в виде ~7700 исходных zip архивов общим объемом около 1 ТБ и распакованным размером 1.7 ТБ. Реальная карта покрытия составляет примерно половину от официально заявленной я скачал все тайлы и сделал карту покрытия, на которой оказалось, что многие тайлы целиком или существенно перекрываются (хотя не должны, по документации перекрытие соседних тайлов составляет ровно 6 граничных пикселов). Содержит заметные пиксельные артефакты, но качество впечатляет по сравнению с рельефом, построенным методом триангуляции снимков. Анализ пространственного спектра в Python ноутбуке показывает следующий результат:



Для пространственного спектра рассматриваемого рельефа в двойных логарифмических координатах коэффициент детерминации R-квадрат равен 98% для масштаба от 2х метров (для масштаба от 4х метров R-квадрат равен 99%). Таким образом, как и следует из теории, точный рельеф фрактален на всех масштабах.


Заключение


С помощью качественного рельефа высокого разрешения можно выполнить гидродинамическое моделирование на поверхности, смотрите Гидродинамическое моделирование (CFD) на рельефе с помощью MantaFlow и визуализация результатов в ParaView, и построить детальные геологические модели как показано в статье Построение достоверных геологических моделей. При использовании методов фрактальной математики становится возможным выделение рудных объектов метрового масштаба, см. Пространственные спектры и фрактальность рельефа, силы тяжести и снимков. Также детальный рельеф помогает, при определенных условиях, уточнить спутниковые интерферограммы и получить более детальную картину отражения сейсмических волн от глубинных объектов и более точную модель смещений поверхности, см. Геология XXI века как наука данных о Земле и Вычислительная геология и визуализация.


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


Также смотрите


Подробнее..

Автоматизация машинного обучения

18.06.2021 14:19:30 | Автор: admin

Datascience это не только fit-predict

Представим, что вы начали работать в компании, которая производит однообразные операции с бесконечными таблицами. Например, в крупном ретейлере или у ведущего оператора связи. Ежедневно перед вами ставят задачу выяснить, останется ли клиент с вами или хватит ли товара на полках до конца недели. Алгоритм выглядит просто. Вы берете выборку, изучаете бесконечные ряды признаков, удаляете мусор, генерируете новые признаки, собираете сводную таблицу. Подаете готовые данные в модель, настраиваете параметры и с нетерпением ждете заветных цифр итоговой метрики. Это повторяется день за днем. Затрачивая каждый день всего 60 минут на генерацию фич или подбор параметров, за месяц вы израсходуете минимум 20 часов. Это, без малого, целые сутки, за которые можно выполнить новую задачу, обучить нейросеть или прочесть несколько статей на arxive.

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

TL:DR

В статье мы рассмотрим применение трех решений оптимизации рабочего процесса: генератор признаков featuretools, подборщик гиперпараметров optuna и коробочное решение автоматического машинного обучения от H2O AutoML.

Сравним работу новых инструментов против классических pandas и sklearn. Не будем глубоко закапываться в код. Приведем основные моменты и оставим анализ полного функционала в качестве домашнего задания. Статья будет полезна DS-специалистам и аналитикам. Требуется понимание основных шагов процесса машинного обучения.

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

Нужно больше данных

Начнем с генерации признаков. Зачем вообще нужны новые фичи в машинном обучении? Они агрегируют данные, дают новую информацию для анализа и улучшают качество работы моделей. Например, методы groupby() и sum() это рядовые представители сгенерированных признаков. Классический подход инжиниринга данных изучить датасет вручную и написать код для каждого нового признака. Такой метод практичен, но отнимает много времени и велика вероятность ошибки. Особенно, когда однообразного кода становится много.

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

Инструмент предлагает два направления генерации фич:feature primitives(fp, англ. примитивные признаки) иdeep feature synthesis(dfs, англ. глубокий синтез признаков).Первый метод производит простые математические операции между определенными фичами. Второй, более сложный, позволяет соединять несколько примитивных операций над признаками. Каждая новая операция увеличивает глубину (depth) сложности алгоритма. Например, мы можем посчитать сначала все средние значения по определенному пользователю, а затем сразу суммировать их.

Переходим к практическому применению. Установка и импорт.

pip install featuretoolsimport featuretools as ft

Featuretools обладают очень удобным API и встроенной документацией. Для вызова возможных функций, просто запустите следующий код:

ft.primitives.list_primitives()

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

В нашем примере мы рассмотрим применение библиотеки с одним датасетом. В несколько строк кода мы создадим попарно умноженные и попарно сложенные исходные признаки. Работа featuretools начинается с создания класса EntitySet (англ. множество сущностей), которому обязательно присвоить id. Id используется системой для идентификации определенной сущности, когда вы работаете сразу с несколькими источниками. Затем последовательно добавляем в него имеющиеся датафреймы, в нашем случае единственный. И запускаем автоматическую генерацию признаков.

es = ft.EntitySet(id=data)                                                                           # Новый пустой EntitySetes.entity_from_dataframe(entity_id = january,                                    # Добавляем в него информацию      dataframe=df_train.drop(target, axis=1),      index=1)feature_matrix, feature_defs = ft.dfs(entityset=es,                                # Запускаем генерацию признаков          target_entity=january,          trans_primitives=[add_number, multiply_numeric],          verbose=1)

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

Всю мощь featuretools раскрывает при обработке нескольких таблиц. Подробный туториал с примерами кода есть наофициальном сайте. Рекомендую к ежедневному использованию.

Искусство легких настроек

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

Самые известные инструменты поиска гиперпараметров из sklearn GridSearchCVиRandomizedSearchCV. GridSearchCV перебирает все возможные комбинации параметров на кросс-валидации. RandomizedSearchCV сначала создает словарь с несколькими случайно выбранными параметрами из всех переданных значений. Затем алгоритм тестирует отобранные значения на моделях и выбирает лучшие. Первый метод является крайне ресурсозатратным. Второй быстрее, но мало применим к сложным ансамблям градиентного бустинга. Для обучения серьезных моделей классические алгоритмы не подходят.

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

Разберем применения самого быстрого и самого молодого из них optuna.

Кроме настройки классических моделей sklearn и ансамблей бустинга (xgboost, lightgbm, catboost), фреймворк позволяет настраивать нейросети, написанные на pytorch, tensorflow, chainer и mxnet. В отличие от классических sklearn методов, optuna, вместе с другими новыми фреймворками, может обрабатывать непрерывные значения гиперпараметров. Например, альфа- или лямбда-регуляризации будут принимать любые значения с плавающей точкой в заданном диапазоне. Все это делает его одним из самых гибких инструментов настройки моделей глубокого обучения.

В своей работе optuna использует байесовские алгоритмы подбора с возможностью удаления заведомо проигрышного пространства заданных гиперпараметров из анализа. Рассмотрим практическое применение фреймворка. Как библиотека устроена под капотом на английском языке можно прочесть наarxiv.orgНа русском языке есть подробная статья наХабре. В ней вы найдете математическое описание методов оптимизации практически на пальцах.

Перейдем к написанию кода. В качестве примера рассмотрим настройку градиентного бустинга LightGBM. Данные будем использовать в первозданном виде. Напомню, что код к статье вы можете найтиздесь.

# установка (при необходимости)pip install optuna# импорт библиотеки и средств визуализацииimport optunafrom optuna.visualization import plot_optimization_history, plot_param_importances

Посмотрим на работу модели без подбора гиперпараметров. Базовая метрика RMSE на приватном списке победителей = 0.73562. Запустим optuna и посмотрим на улучшение метрики за 5 итераций. Имеющиеся данные достаточно простые и мы не ждем глобального скачка качества после подбора гиперпараметров. Нам интересен сам процесс и скорость его работы.

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

reg_alpha: trial.suggest_loguniform(reg_alpha, 1e-3, 10.0)  # передаем нижнюю и верхнюю границы непрерывного параметраnum_leaves: trial.suggest_int(num_leaves, 1, 300)# передаем нижнее и верхнее значение числового параметраmax_depth: trial.suggest_categorical(max_depth, [-1,10,20]) # передаем список определенных значений категориального признака, которые надо проверить

Сам процесс обучения прописывается в две строки:

study = optuna.create_study(direction=minimize)  # минимизируем ошибкуstudy.optimize(objective, n_trials=5)       # objective  задание для поиска, 5  количество      # итераций оптимизации  

В приложенном к статьеkaggleноутбуке рассматривается базовый вариант запуска optuna. За 5 итераций удалось улучшить метрику на отложенной выборке на 0.1 пункта метрики RMSE. Процесс длился 5 минут. Интересно, сколько по времени работал бы традиционный GridSearchCV с данным количеством параметров. Метрика на финальном сабмите с оптимизацией = 0.7221, что лучше ручной настройки модели на сырых данных.

MachineLearningза 7 строк кода

Такой заголовок подошел бы для кликбейтовой рекламы в интернете. Но мы действительно создадим полный пайплайн обучения за минимальное количество блоков кода. Поможет нам в этомh2оот Amazon. Это библиотека содержит в себе все популярные модули обработки данных и машинного обучения. Разберем модуль автоматического машинного обучения automl. Фреймворк позволяет подбирать модель и параметры без участия специалиста.

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

pip install f https://h2o-release.s3.amazonaws.com/h2o/latest_stable_Py.html h2o from h2o.automl import H2OAutoML# установим максимальный размер используемой оперативной памятиh2o.init(max_mem_size=16G)

Pandas больше не нужен! Можно загружать данные в рабочий ноутбук с помощью AutoML.

train = h20.import_file(../input/tabular-playground-series-jan-21/train.csv)test = h20.import_file(../input/tabular-playground-series-jan-21/test.csv)

Выберем признаки и таргет.

x = test.columns[1:]y = target

Запускаем машинное обучение. Библиотека будет последовательно обучать встроенные модели с различными гиперпараметрами. В нашем примере настроим количество моделей, random_state=47 и максимальное время обучения одной модели = 3100 секунд (3600 секунд по умолчанию)

aml = H20AutoML(max_models=2,  # Количество различных моделей для обучения                                 seed=SEED,                                  max_runtime_secs = 3100)   # Максимальное время обучения одной моделиaml.train(x=x, y=y, training_frame=train)# Ждем, когда заполнится строка обучения AutoML

Готово! Параметры подобраны, модели выбраны. Все сделано в автоматическом режиме. Можно посмотреть на метрики лучших моделей или вывести информацию о наилучшей. Подробный код смотрите в учебном ноутбуке наkaggle. Остается создать и отправить на валидацию предсказания модели. Это единственный случай, когда потребуется загруженный pandas.

preds = aml.predict(test) df_sub[target] = preds.as_data_frame().values_flatten() df_sub.to_csv(h2o_submission_5.csv, index=False)

Финальная метрика AutoMl на приватном списке победителей = 0.71487. Напомню, что метрика среднеквадратичной ошибки (root mean squared error, RMSE) отражает среднее отклонение предсказаний от реального значения. Простыми словами она должна быть минимальной и стремиться к нулю. RMSE у победителя соревнования = 0.69381. В пределах рутинных задач с которыми можно столкнуться в ежедневной работе разница так же невелика. Но время AutoML экономит значительно.

Заключение

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

Оптимизация параметров и автоматизированное обучение повысили качество финальной метрики RMSE в сравнении с базовой моделью, на 0.2 пункта. Для простых синтетических данных это адекватный результат, который демонстрирует применимость методов к серьезным задачам.

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

Подробнее..

Создаем веб-приложение на Haskell с использованием Reflex. Часть 4

18.06.2021 18:20:53 | Автор: admin

Часть 1.


Часть 2.


Часть 3.


Всем привет! В новой части мы рассмотрим использование JSFFI.


intro


JSFFI


Добавим в наше приложение возможность установки даты дедлайна. Допустим, требуется сделать не просто текстовый input, а чтобы это был выпадающий datepicker. Можно, конечно, написать свой datepicker на рефлексе, но ведь существует большое множество различных JS библиотек, которыми можно воспользоваться. Когда существует уже готовый код на JS, который, например, слишком большой, чтобы переписывать с использованием GHCJS, есть возможность подключить его с помощью JSFFI (JavaScript Foreign Function Interface). В нашем случае мы будем использовать flatpickr.


Создадим новый модуль JSFFI, сразу добавим его импорт в Main. Вставим в созданный файл следующий код:


{-# LANGUAGE MonoLocalBinds #-}module JSFFI whereimport Control.Monad.IO.Classimport Reflex.Domforeign import javascript unsafe  "(function() { \  \ flatpickr($1, { \  \   enableTime: false, \  \   dateFormat: \"Y-m-d\" \  \  }); \  \})()"  addDatePicker_js :: RawInputElement GhcjsDomSpace -> IO ()addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker = liftIO . addDatePicker_js . _inputElement_raw

Так же не забудем добавить в элемент head необходимые скрипт и стили:


  elAttr "link"    (  "rel" =: "stylesheet"    <> "href" =: "https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" )    blank  elAttr "script"    (  "src" =: "https://cdn.jsdelivr.net/npm/flatpickr")    blank

Пробуем скомпилировать, так же как и раньше, и получаем следующую ошибку:


src/JSFFI.hs:(9,1)-(16,60): error:     The `javascript' calling convention is unsupported on this platform     When checking declaration:        foreign import javascript unsafe "(function() {    flatpickr($1, {      enableTime: false,      dateFormat: \"Y-m-d\"    });   })()" addDatePicker_js          :: RawInputElement GhcjsDomSpace -> IO ()  |9 | foreign import javascript unsafe  |

Действительно, сейчас мы собираем наше приложение с помощью GHC, который понятия не имеет, что такое JSFFI. Напомним, что сейчас запускается сервер, который с помощью вебсокетов отправляет обновленный DOM, когда требуется, и код на JavaScript для него чужд. Здесь напрашивается вывод, что использовать наш datepicker при сборке с помощью GHC не получится. Тем не менее, в продакшене GHC для клиента не будет использоваться, мы будем компилировать в JS при помощи GHCJS, и полученный JS встраивать уже в нашу страницу. ghcid не поддерживает GHCJS поэтому смысла грузиться в nix shell нет, мы будем использовать nix сразу для сборки:


nix-build . -A ghcjs.todo-client -o todo-client-bin

В корневой директории приложения появится директория todo-client-bin со следующей структурой:


todo-client-bin bin     todo-client-bin     todo-client-bin.jsexe         all.js         all.js.externs         index.html         lib.js         manifest.webapp         out.frefs.js         out.frefs.json         out.js         out.stats         rts.js         runmain.js

Открыв index.html в браузере, увидим наше приложение. Мы собрали проект с помощью GHCJS, но ведь для разработки все равно удобнее использовать GHC вместе с ghcid, поэтому модифицируем модуль JSFFI следующем образом:


{-# LANGUAGE CPP #-}{-# LANGUAGE MonoLocalBinds #-}module JSFFI whereimport Reflex.Dom#ifdef ghcjs_HOST_OSimport Control.Monad.IO.Classforeign import javascript unsafe  "(function() {\    flatpickr($1, {\      enableTime: false,\      dateFormat: \"Y-m-d\"\    }); \  })()"  addDatePicker_js :: RawInputElement GhcjsDomSpace -> IO ()addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker = liftIO . addDatePicker_js . _inputElement_raw#elseaddDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker _ = pure ()#endif

Мы добавили условную компиляцию: в зависимости от платформы, либо будем использовать вызов JS функций, либо заглушку.


Теперь требуется изменить форму добавления нового задания, добавив туда поле выбора даты:


newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  dEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Deadline"      <> "style" =: "max-width: 150px" )  addDatePicker dEl  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Скомпилируем наше приложение, попробуем его запустить, и мы все еще ничего не увидим. Если посмотрим в консоль разработчика в браузере, увидим следующую ошибку:


uncaught exception in Haskell main thread: ReferenceError: flatpickr is not definedrts.js:5902 ReferenceError: flatpickr is not defined    at out.js:43493    at h$$abX (out.js:43495)    at h$runThreadSlice (rts.js:6847)    at h$runThreadSliceCatch (rts.js:6814)    at h$mainLoop (rts.js:6809)    at rts.js:2190    at runIfPresent (rts.js:2204)    at onGlobalMessage (rts.js:2240)

Замечаем, что необходимая нам функция не определена. Так получается, потому что элемент script со ссылкой создается динамически, равно как и вообще все элементы страницы. Поэтому, когда мы используем вызов функции flatpickr, скрипт, содержащий библиотеку с этой функцией может быть еще не загружен. Надо явно расставить порядок загрузки.
Решим эту проблему при помощи пакета reflex-dom-contrib. Этот пакет содержит много полезных при разработке функций. Его подключение нетривиально. Дело в том, что на Hackage лежит устаревшая версия этого пакета, поэтому придется брать его напрямую c GitHub. Обновим default.nix следующим образом.


{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {    owner = "reflex-frp";    repo = "reflex-platform";    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";    })}:(import reflex-platform {}).project ({ pkgs, ... }:let  reflexDomContribSrc = builtins.fetchGit {    url = "https://github.com/reflex-frp/reflex-dom-contrib.git";    rev = "11db20865fd275362be9ea099ef88ded425789e7";  };  override = self: pkg: with pkgs.haskell.lib;  doJailbreak (pkg.overrideAttrs  (old: {    buildInputs = old.buildInputs ++ [ self.doctest self.cabal-doctest ];  }));in {  useWarp = true;  overrides = self: super: with pkgs.haskell.lib; rec {    reflex-dom-contrib = dontHaddock (override self      (self.callCabal2nix "reflex-dom-contrib" reflexDomContribSrc { }));  };  packages = {    todo-common = ./todo-common;    todo-server = ./todo-server;    todo-client = ./todo-client;  };  shells = {    ghc = ["todo-common" "todo-server" "todo-client"];    ghcjs = ["todo-common" "todo-client"];  };})

Добавим импорт модуля import Reflex.Dom.Contrib.Widgets.ScriptDependent и внесем изменения в форму:


newTodoForm :: MonadWidget t m => m (Event t (Endo Todos))newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  dEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Deadline"      <> "style" =: "max-width: 150px" )  pb <- getPostBuild  widgetHoldUntilDefined "flatpickr"    (pb $> "https://cdn.jsdelivr.net/npm/flatpickr")    blank    (addDatePicker dEl)  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

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


Но мы никак не задействовали это поле. Изменим тип Todo, не забыв добавить импорт Data.Time:


data Todo = Todo  { todoText     :: Text  , todoDeadline :: Day  , todoState    :: TodoState }  deriving (Generic, Eq, Show)newTodo :: Text -> Day -> TodonewTodo todoText todoDeadline = Todo {todoState = TodoActive False, ..}

Теперь изменим функцию с формой для нового задания:


...  today <- utctDay <$> liftIO getCurrentTime  let    dateStrDyn = value dEl    dateDyn = fromMaybe today . parseTimeM True      defaultTimeLocale "%Y-%m-%d" . unpack <$> dateStrDyn    addNewTodo = \todo date -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo date) todos    newTodoDyn = addNewTodo <$> value iEl <*> dateDyn    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"...

И добавим отображение даты в списке:


todoActive  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> Day -> m ()todoActive ix todoText deadline = divClass "d-flex border-bottom" $ do  elClass "p" "p-2 flex-grow-1 my-auto" $ do    text todoText    elClass "span" "badge badge-secondary px-2" $      text $ pack $ formatTime defaultTimeLocale "%F" deadline  divClass "p-2 btn-group" $ do  ...

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


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

Подробнее..

Перевод Rust 1.53.0 IntoIterator для массивов, quotquot в шаблонах, Unicode-идентификаторы, поддержка имени HEAD-ветки в Cargo

18.06.2021 18:20:53 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.53.0 вам достаточно выполнить следующую команду:


rustup update stable

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


Что было стабилизировано в 1.53.0


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


IntoIterator для массивов


Это первый выпуск Rust, в котором массивы реализуют типаж IntoIterator. Теперь вы можете итерироваться в массиве по значению:


for i in [1, 2, 3] {    ..}

Раньше это было возможно только по ссылке, с помощью &[1, 2, 3] или [1, 2, 3].iter().


Аналогично вы теперь можете передать массив в методы, ожидающие T: IntoIterator:


let set = BTreeSet::from_iter([1, 2, 3]);

for (a, b) in some_iterator.chain([1]).zip([1, 2, 3]) {    ..}

Это не было реализовано ранее из-за проблем с совместимостью. IntoIterator всегда реализуется для ссылок на массивы и в предыдущих выпусках array.into_iter() компилировался, преобразовываясь в (&array).into_iter().


Начиная с этого выпуска, массивы реализуют IntoIterator с небольшими оговорками для устранения несовместимости кода. Компилятор, как и прежде, преобразовывает array.into_iter() в (&array).into_iter(), как если бы реализации типажа ещё не было. Это касается только синтаксиса вызова метода .into_iter() и не затрагивает, например, for e in [1, 2, 3], iter.zip([1, 2, 3]) или IntoIterator::into_iter([1, 2, 3]), которые прекрасно компилируются.


Так как особый случай .into_iter() необходим только для предотвращения поломки существующего кода, он будет удалён в новой редакции Rust 2021, которая выйдет позже в этом году. Если хотите узнать больше следите за анонсами редакции.


"Или" в шаблонах


Синтаксис шаблонов был расширен поддержкой |, вложенного в шаблон где угодно. Это позволяет писать Some(1 | 2) вместо Some(1) | Some(2).


match result {     Ok(Some(1 | 2)) => { .. }     Err(MyError { kind: FileNotFound | PermissionDenied, .. }) => { .. }     _ => { .. }}

Unicode-идентификаторы


Теперь идентификаторы могут содержать не-ASCII символы. Можно использовать все действительные идентификаторы символов Unicode, определённые в UAX #31. Туда включены символы из многих разных языков и письменностей но не эмодзи.


Например:


const BLHAJ: &str = "";struct  {    : String,}let  = 1;

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


warning: identifier pair considered confusable between `` and `s`

Поддержка имени HEAD-ветки в Cargo


Cargo больше не предполагает, что HEAD-ветка в git-репозитории называется master. А следовательно, вам не надо указывать branch = "main" для зависимостей из git-репозиториев, в которых ветка по умолчанию main.


Инкрементальная компиляция до сих пор отключена по умолчанию


Как ранее говорилось в анонсе 1.52.1, инкрементальная компиляция была отключена для стабильных выпусков Rust. Функциональность остаётся доступной в каналах beta и nightly. Метод включения инкрементальной компиляции в 1.53.0 не изменился с 1.52.1.


Стабилизированные API


Следующие методы и реализации типажей были стабилизированы:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.53.0


Множество людей собрались вместе, чтобы создать Rust 1.53.0. Мы не смогли бы сделать это без всех вас. Спасибо!




От переводчиков


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


Данную статью совместными усилиями перевели TelegaOvoshey, blandger, Belanchuk и funkill.

Подробнее..

Перевод Перевод Курса по стартапам и бизнесу от Стэнфордского Университета. Лекция 2. Команда и реализация стартапа

19.06.2021 10:16:36 | Автор: admin

Почему Курс по стартапам и бизнесу от Стэнфордского Университета, является одним из самых лучших в мире для стартаперов? Дело в том, что лекторами данного курса являются лучшие из лучших стартап-предприниматели, такие как: Сем Альтман президент самого известного венчурного фонда Y Combinator, Дастин Московиц сооснователь Facebook, Питер Тиль сооснователь PayPal и первый инвестор, который вложил деньги в Facebook, Брайан Чески основатель Airbnb, Альфред Лин основатель Zappos, Алан Кей отец ООП и много других гуру современного интернет-бизнеса.

В этом курсе отсутствуют сложные теории, а присутствуют только практические примеры и советы.
Например, как найти бизнес-идею и оценить её, как привлечь клиентов и постоянно увеличивать свои продажи, как построить успешную бизнес-стратегию и подобрать сильную команду и т. д.
В первой лекции будут выступать Сэм Альтман гений-программист, который сейчас возглавляет венчурный фонд Y Combinator, с чьей помощью успешно реализовано более 400 стартапов и также Дастин Московиц сооснователь Facebook.

В этой небольшой статье я хочу кратко описать содержимое лекции 2 - Команда и реализация стартапа из курса по стартапам и бизнесу от Стэнфордского Университета.

Итак, из Лекции 2 - Команда и реализация стартапа вы узнаете слендующее:

00:43 - Как определить какие рынки будут развиваться и расти в будущем?

01:35 - Как преодолеть усталость от стартапа основателям?

02:45 - Как найти со-основателя или партнера и выстроить с ним хорошие отношения?

05:15 - Какие характеристики должен иметь каждый партнер?

07:47 - Какое количество со-основателей желательно должен иметь стартап?

08:00 - Как и когда нанимать новых сотрудников?

08:24 - Как подбирать персонал и какими качествами должны обладать ваши работники?

12:20 Сколько времени нужно выделить на поиски сотрудников?

14:15 Источники поисков кандидатов.

15:27 Какой опыт должны иметь кандидаты в сотрудники?

16:05 Три вещи на которые стоит обращать внимание при найме людей.

16:55 Лучший способ с помощью которого можно понять качество кандидата.

17:54 Почему важны коммуникативные способности?

19:47 - Как распределять доли компании между работниками компании?

21:07 - Как удержать в компании хороших сотрудников?

23:27 - Когда нужно увольнять неподходящего для вашей компании человека?

24:30 Как найти баланс между быстрым увольнением работника и обеспечением чувства безопасности вашей команды?

25:55 - Когда со-основатели должны принимать решение об разделении капитала?

26:42 - Как узнать, что работник вырастит до хорошего уровня в будущем?

27:32 - Как сохранить компанию в случае ухода одного из со-основателей на раннем этапе?

29:00 - Что делать если при приеме сотрудника компания теряет свою конкурентную способность

29:37 - Как работать с основателями, которые находятся в другой географической локации?

30:20 - Несколько слов про основные правила реализации стартапа.

31:50 - Какие задачи должен выполнять основатель в стартапе?

32:50 - Как уделённое внимание на задачи, а также концентрация на выполнении определённой задачи и интенсивность её выполнения влияют на развитие стартапа?

40:20 Один из основных критериев хорошего стартапа.

41:15 Почему важна интенсивность выполнения задач?

41:50 Как нужно бороться за клиента?

43:50 - Почему для стартапа жизненно важен его рост?

45:05 Почему продажи самый лучший показатель успешности стартапа?

45:50 Один из вариантов решения проблем стартапа.

47:05 - Как стоит реагировать на новости от конкурентов?

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

Оригинал на сайте http://startupclass.samaltman.com/

Подробнее..

Локальное время и дата рожденияили зачем UTC

19.06.2021 12:05:20 | Автор: admin

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

Пример

У вас в паспорте записана дата рождения например 1990-05-05 при этом также указывается место рождения. По нему можно определить местное время и сдвиг к мировому времени.

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

  1. Вы родились во Владивостоке в 23 часа ночи - то есть UTC+10, а по Москве это минус 7 часов (московское время - это сдвиг UTC+03),

  2. А заполняете форму своей персоны, например, находясь в Москве - в результате программное обеспечение на вашем локальном устройстве (например мобилка, веб-сайт, полная нода блокчейн Erachain) подставит UTC+03

  3. Точное время рождения вы не ставите и вместо вас его ставит ваше устройство как 00:00.

  4. В результате в блокчейн Erachain ваша дата рождения будет такая 1990-05-05 в 00:00 UTC+03

При этом если вы посмотрите дату рождения в международном стандарте, то получится что вы родились на день раньше: 1990-05-04 в 21:00.

Математически все верно, но по человечески не совсем!

Теперь если вы находитесь в Москве или Владивостоке, то день рождения (5=е число) не изменится даже с учетом применения локального сдвига.

Однако, если вы например находитесь в Европе, то ваша дата рождения станет 4-е число!

Это можно исправить если в поле где будет отображаться ваша дата рождения принудительно ввести смещение UTC+03.

Пути решения

  1. При вводе важных дат обращать внимание на точное время до минут и на локальный сдвиг в международном стандарте UTC, а не полагаться на ваше локальное время, которое выставит ваше устройство (с которого вы вводите дату и время), и которое может не совпадать с нужным смещением, так как действие тогда происходило в другой местности с другим временным сдвигом. То есть нужно всегда вводить свой UTC, который соответствует нужной местности и точное время до минут.

  2. При выводе даты и времени всегда обращать внимание на сдвиг по времени на том устройстве на котором вы его видите. Так в Японии у вас дата рождения будет 1990-05-05, а в Европе уже 1990-05-04, так как устройство которое будет производить отображение само подставит локальный сдвиг и преобразует дату в международном формате в локальное время. Поэтому обращайте внимание на UTC так же при выводе ваших данных и пересчитывайте время в уме или задайте UTC при выводе, если есть такая возможность.

Подробнее..

Как подружить Redis Cluster c Testcontainers?

20.06.2021 12:09:43 | Автор: admin
В 26-м выпуске NP-полного подкаста я рассказывал, что начал переводить один из своих сервисов из Redis Sentinel на Redis Cluster. На этой неделе я захотел потестировать данный код, и, конечно же, выбрал Testcontainers для этого. К сожалению, Redis Cluster в тестовых контейнерах не завелся из коробки, и мне пришлось вставить несколько костылей. О них и пойдет речь далее.



Вводные


Сначала я бы хотел описать все вводные, а потом рассказать про костыли. Мой проект построен на Spring Boot. Для взаимодействия с редисом используется Lettuce клиент. Для тестирования testcontainers-java с JUnit. Версия обоих редисов 6. В общем, всё типичное, нет ничего особенного с точки зрения стека.

Если кто-то еще не знаком с testcontainers, то пара слов о них. Это библиотека для интеграционного тестирования. Она построена на другой библиотеке https://github.com/docker-java/docker-java. Тестконтейнеры, по сути говоря, помогают быстро и просто запускать контейнеры с разными зависимостями в ваших интеграционных тестах. Обычно это базы данных, очереди и другие сложные системы. Некоторые люди используют testcontainers и для запуска своих сервисов, от которых зависит тестируемое приложение (чтобы тестировать микросервисное взаимодействие).

Про Redis Cluster


Redis Cluster это одна из нескольких реализаций распределнного режима Редиса. К сожалению, в Редисе нет единого правильного способа, как масштабировать базу. Есть Sentinel, есть Redis Cluster, а еще ребята активно разрабатывают RedisRaft распредеделенный редис на базе протокола консенсуса Raft (у них там своя реализация, которая, как они сами заявляют, не совсем каноничный Рафт, но конкретно для Redis самое то).

В целом, про Redis Cluster есть две замечательных статьи на официальном сайте https://redis.io/topics/cluster-tutorial и https://redis.io/topics/cluster-spec. Большинство деталей описано там.

Для использования Redis Cluster в testcontainers важно знать несколько вещей из документации. Во-первых, Redis Cluster использует gossip протокол поэтому каждый узел кластера имеет TCP-соединение со всеми другими узлами. Поэтому, между нодами должна быть сетевая связность, даже в тестах.

Вторая важная штука, которую надо знать при тестировании это наличие в Redis Cluster bootstrap узлов для конфигурации. То есть, вы в настройках можете задать лишь подмножество узлов, которые будут использоваться для старта приложения. В последствие, Redis клиент сам получит Топологию кластера через взаимодействие с Редисом. Исходя из этого, получается вторая особенность тестируемое приложение должно иметь сетевую связность с теми Redis URI, которые будут аннонсированы со стороны редис кластера (кстати, эти адреса можно сконфигурировать через cluster-announce-port и cluster-announce-ip).

Про костыли с Redis Cluster и testcontainers


Для тестирования я выбрал довольно популярный docker-образ https://github.com/Grokzen/docker-redis-cluster. Он не подходит для продакшена, но очень прост в использовании в тестах. Особенность этого образа все Редисы (а их 6 штук, по умолчанию 3 мастера и 3 слейва) будут подняты в рамках одного контейнера. Поэтому, мы автоматически получаем сетевую связность между узлами кластера из коробки. Осталось решить вторую из двух проблем, связанную с получением приложением топологии кластера.

Я не хотел собирать свой docker-образ, а выбранный мной image не предоставляет возможности задавать настройки cluster-announce-port и cluster-announce-ip. Поэтому, если ничего не делать дополнительно, при запуске тестов вы увидите примерно такие ошибки:

Unable to connect to [172.17.0.3/<unresolved>:7003]: connection timed out: /172.17.0.3:7003


Ошибка означает, что мы со стороны приложения пытаеся приконнектится к Узлу редис кластера, используя IP докер контейнера и внутренний порт (порт 7003 используется данным узлом, но наружу он отображается на какой-то случайный порт, который мы и должны использовать в нашем приложении; внутренний порт, по понятным причинам, не доступен из вне). Что касается данного IP-адреса он доступен для приложения, если это Linux, и он не доступен для приложения, если это MacOs/Windows (из-за особенностей реализации докера на этих ОС).

Решение проблемы (а-ка костыль) я собрал по частичкам из разных статей. А давайте сделаем NAT RedisURI на стороне приложения. Ведь это нужно именно для тестов, и тут не так страшно вставлять такой ужас. Решение, на самом деле, состоит из пары строк (огромное спасибо Спрингу и Lettuce, где можно сконфигурировать практически всё, только и успевай, как переопределять бины).

public SocketAddress resolve(RedisURI redisURI) {    Integer mappedPort = redisClusterNatPortMapping.get(redisURI.getPort());    if (mappedPort != null) {        SocketAddress socketAddress = redisClusterSocketAddresses.get(mappedPort);        if (socketAddress != null) {            return socketAddress;        }        redisURI.setPort(mappedPort);    }    redisURI.setHost(DockerClientFactory.instance().dockerHostIpAddress());    SocketAddress socketAddress = super.resolve(redisURI);    redisClusterSocketAddresses.putIfAbsent(redisURI.getPort(), socketAddress);    return socketAddress;}


Полный код выложен на гитхаб https://github.com/Hixon10/spring-redis-cluster-testcontainers.

Идея кода супер простая. Будем хранить две Map. В первой маппинг между внутренними портами редиса (7000..7005) и теми, что доступны для приложения (они могут быть чем-то типа 51343, 51344 и тд). Во-второй внешние порты (типа, 51343) и SocketAddress, полученный для них. Теперь, когда мы получаем от Редиса при обновлении топологии что-то типа 172.17.0.3:7003, мы сможем легко найти нужный внешний порт, по которому сможем найти SocketAddress и переиспользовать его. То есть, с портами проблема решена. А что с IP?

С IP-адресом всё просто. Тут нам на помощь приходят Тест контейнеры в которых есть утилитный метод DockerClientFactory.instance().dockerHostIpAddress(). Для MacOs/Windows он будет отдавать localhost, а для linux IP-адрес контейнера.

Выводы


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

Powershell настоящий язык программирования. Скрипт оптимизации рутины в техподдержке

20.06.2021 14:08:21 | Автор: admin

Работая в компании IT-аутсорса в качестве руководителя 3 линии поддержки, задумался, как автоматизировать подключение сотрудников по RDP, через VPN к серверам десятков клиентов.

Таблички с адресами, паролями и прочими настройками серверов, конечно, хорошо, но поиск клиента и вбивание адресов с аккаунтами занимает довольно существенное время.
Держать все подключения к VPN в Windows не самая лучшая идея, да и при переустановке оного, создавать VPNы тоже не доставляет удовольствие.
Плюс к тому, в большинстве случаев, требуется установить VPN подключение к клиенту без использования шлюза. дабы не гонять весь интернет-трафик через клиента.
Задача, к тому же, осложняется тем, что у некоторых клиентов pptp, у кого-то l2tp, у некоторых несколько подсетей, туннели и т.п.

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

До написания этого скрипта-приложения программированием не занимался вообще, разве что лет 20 назад что-то пописывал на VBS в MS Excel и MS Access, поэтому не гарантирую красивость кода и принимаю критику от опытных программистов, как можно было бы сделать красивее.

В Powershell, начиная с Windows 8 и, конечно в Windows 10, появилась прекрасная возможность создавать VPN подключения командой Add-VpnConnection и указывать какие маршруты использовать с этими соединениями командой Add-VpnConnectionRoute, для использования VPN без шлюза.

На основании этих команд и создано данное приложение. Но, обо всем по порядку.

Для начала, создаем в Google Disk таблицу с именованными столбцами:
Number; Name; VPNname; ServerAddress; RemoteNetwork; VPNLogin; VPNPass; VPNType; l2tpPsk; RDPcomp; RDPuser; RDPpass; DefaultGateway; PortWinbox; WinboxLogin; WinboxPwd; Link; Inform

  • VPNname произвольное имя для VPN соединения

  • ServerAddress адрес VPN сервера

  • RemoteNetwork адреса подсети или подсетей клиента, разделенные ;

  • VPNLogin; VPNPass учетная запись VPN

  • VPNType -тип VPN (пока используется pptp или l2tp)

  • l2tpPsk PSK для l2tp, в случае pptp оставляем пустым

  • RDPcomp адрес сервера RPD

  • RDPuser; RDPpass учетная запись RPD

  • DefaultGateway принимает значение TRUE или FALSE и указывает на то, использовать ли Шлюз по умолчанию для этого соединения. В 90% случаев = FALSE

  • PortWinbox; WinboxLogin; WinboxPwd порт, логин и пароль для Winbox, поскольку у нас большинство клиентов использует Mikrotik)

  • Link ссылка на расширенную информацию о компании, например, на диске Google, или в любом другом месте, будет выводиться в информационном поле для быстрого доступа к нужной информации

Inform примечание

Пример таблицы доступен по ссылке

Number

Name

VPNname

ServerAddress

RemoteNetwork

VPNLogin

VPNPass

VPNType

l2tpPsk

RDPcomp

RDPuser

RDPpass

DefaultGateway

PortWinbox

WinboxLogin

WinboxPwd

Link

Inform

1

Тест1

Test1

a.b.c.d

192.168.10.0/24: 10.10.0.0/24

vpnuser

passWord

pptp

none

192.168.10.1

user

passWord

TRUE

8291

Admin

Admin

http://yandex.ru

тест

2

Тест2

Test2

e.f.j.k

192.168.2.0/24

vpnuser

passWord

l2tp

KdoSDtdP

192.168.2.1

user

passWord

FALSE

8291

Admin

Admin

Скриншот работающего приложения с затертыми данными:

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

function Get-Clients #Функция принимает строку адреса файла в Google Drive и возвращает в виде массива данных о клиентах{param([string]$google_url = "")[string]$xlsFile = $google_url$csvFile = "$env:temp\clients.csv"$Comma = ','Invoke-WebRequest $xlsFile -OutFile $csvFile$clients = Import-Csv -Delimiter $Comma -Path "$env:temp\clients.csv"Remove-Item -Path $csvFilereturn $clients}function Main {<#    Функция, срабатываемая при запуске скрипта#>Param ([String]$Commandline)#Иннициализируем переменные и присваиваем начальные значения. Здесь же, указываем путь к таблице с клиентами$Global:Clients = $null$Global:Current$Global:CurrentRDPcomp$Global:google_file = "https://docs.google.com/spreadsheets/d/1O-W1YCM4x3o5W1w6XahCJZpkTWs8cREXVF69gs1dD0U/export?format=csv" # Таблица скачивается сразу в виде csv-файла$Global:Clients = Get-Clients ($Global:google_file) # Присваиваем значения из таблицы массиву #Скачиваем Winbox64 во временную папку$download_url = "https://download.mikrotik.com/winbox/3.27/winbox64.exe"$Global:local_path = "$env:temp\winbox64.exe"If ((Test-Path $Global:local_path) -ne $true){$WebClient = New-Object System.Net.WebClient$WebClient.DownloadFile($download_url, $Global:local_path)}  #Разрываем все текущие VPN соединения (на всякий случай)foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){Rasdial $item.Name /disconnect}  #Удаляем все, ранее созданные программой временные соединения, если вдруг не удалились при некорректном закрытии приложенияget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Запускаем приложениеShow-MainForm_psf}#Собственно, само приложениеfunction Show-MainForm_psf{[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')#Создаем форму и объекты формы[System.Windows.Forms.Application]::EnableVisualStyles()$formКлиентыАльбус = New-Object 'System.Windows.Forms.Form'$statusbar1 = New-Object 'System.Windows.Forms.StatusBar'$groupboxTools = New-Object 'System.Windows.Forms.GroupBox'$buttonPing = New-Object 'System.Windows.Forms.Button'$buttonВыход = New-Object 'System.Windows.Forms.Button'$buttonWindox = New-Object 'System.Windows.Forms.Button'$buttonПеречитатьДанные = New-Object 'System.Windows.Forms.Button'$buttonPingAll = New-Object 'System.Windows.Forms.Button'$groupboxRDP = New-Object 'System.Windows.Forms.GroupBox'$comboboxRDP = New-Object 'System.Windows.Forms.ComboBox'$textboxRDPLogin = New-Object 'System.Windows.Forms.TextBox'$textboxRdpPwd = New-Object 'System.Windows.Forms.TextBox'$buttonПодключитьRDP = New-Object 'System.Windows.Forms.Button'$groupboxVPN = New-Object 'System.Windows.Forms.GroupBox'$buttonПодключитьVPN = New-Object 'System.Windows.Forms.Button'$buttonОтключитьVPN = New-Object 'System.Windows.Forms.Button'$checkboxШлюзПоумолчанию = New-Object 'System.Windows.Forms.CheckBox'$richtextboxinfo = New-Object 'System.Windows.Forms.RichTextBox'$listbox_clients = New-Object 'System.Windows.Forms.ListBox'$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'  #----------------------------------------------# Обработчики событий#----------------------------------------------$formКлиентыАльбус_Load = {#При загрузке формы очистить поле информации и заполнить поле с клиентами (их названиями) $richtextboxinfo.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)} # В листбокс добавляем всех наших клиентов по именам и массива при загрузке формы}$listbox_clients_SelectedIndexChanged = {#Прочитать из массива информацию о клиенте при выборе его в поле listbox_clients (массив, как мы помним считан из файла с диска Google)$statusbar1.Text = 'Выбран клиент: ' + $listbox_clients.SelectedItem.ToString() # Пишем клиента в статусбар$Global:Current = $Global:Clients.Where({ $_.Name -eq $listbox_clients.SelectedItem.ToString() })If ($Current.PortWinbox -ne 0) # Если порт Winbox указан, то у клиента Mikrotik, включаем соответствующую кнопку{$buttonWindox.Enabled = $true$buttonWindox.Text = "Winbox"}$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только ихswitch ($Global:Current.VPNType) #В зависимости от типа VPN пишем на кнопке "Подключить pptp VPN" или "Подключить l2tp VPN", если у клиента нет VPN, то пишем "Здесь нет VPN"{"pptp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить pptp VPN"}"l2tp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить l2tp VPN"}DEFAULT{$buttonПодключитьVPN.Enabled = $false$buttonПодключитьVPN.Text = "Здесь нет VPN"}}switch ($Global:Current.DefaultGateway) #Смотрим в массиве, используется ли у клиента "Шлюз по-умолчанию" и заполняем соответствующий чекбокс{"FALSE"{ $checkboxШлюзПоумолчанию.Checked = $false }"Нет"{ $checkboxШлюзПоумолчанию.Checked = $false }"TRUE"{ $checkboxШлюзПоумолчанию.Checked = $true }"Да"{ $checkboxШлюзПоумолчанию.Checked = $true }DEFAULT{ $checkboxШлюзПоумолчанию.Checked = $false }}$VPNStatus = (ipconfig | Select-String $VPNname -Quiet) #Проверяем, не установлено ли уже это VPN соединение?If ($VPNStatus) #Если установлено, то разблокируем кнопку "Подключить RDP"{$buttonПодключитьRDP.Enabled = $true}else{$buttonПодключитьRDP.Enabled = $false}$richtextboxinfo.Clear() #Очищаем информационное поле # И заполняем информацией о клиенте из массива$richtextboxinfo.SelectionColor = 'Black'$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine + `"Имя VPN: " + $Global:Current.VPNname + [System.Environment]::NewLine + `"Тип VPN: " + $Global:Current.VPNType + [System.Environment]::NewLine + `"Адрес сервера: " + $Global:Current.ServerAddress + [System.Environment]::NewLine + `"Подсеть клиента: " + $Global:Current.RemoteNetwork + [System.Environment]::NewLine + `"Адрес сервера RDP: " + $Global:Current.RDPcomp + [System.Environment]::NewLine + [System.Environment]::NewLine + `"DefaultGateway: " + $Global:Current.DefaultGateway + [System.Environment]::NewLine + [System.Environment]::NewLine + `"Примечание: " + [System.Environment]::NewLine + $Global:Current.Inform + [System.Environment]::NewLine + `"Connection '" + $VPNname + "' status is " + $buttonПодключитьRDP.Enabled + [System.Environment]::NewLine$richtextboxinfo.AppendText($Global:Current.Link)$RDPServers = $Global:Current.RDPcomp.Split(';') -replace '\s', '' #Считываем и разбираем RDP серверы клиента из строки с разделителем в массив#Добавляем из в выпадающее поле выбора сервера$comboboxRDP.Items.Clear()$comboboxRDP.Text = $RDPServers[0]foreach ($RDPServer in $RDPServers){$comboboxRDP.Items.Add($RDPServer)}#Заполняем поля имени и пароля RDP по умолчанию из таблицы о клиенте (при желании, их можно поменять в окне программы)$textboxRdpPwd.Text = $Global:Current.RDPpass$textboxRdpLogin.Text = $Global:Current.RDPuser} # Форма заполнена, при смене выбранного клиента произойдет перезаполнение полей в соответствии с выбранным клиентом$buttonWindox_Click = {#Обработка нажатия кнопки WinboxIf ($Global:Current.PortWinbox -ne 0) #Если порт Winbox заполнен, то открываем скачанный ранее Winbox, подставляем туда имя и пароль к нему и запускаем{$runwinbox = "$env:temp\winbox64.exe"$ServerPort = $Global:Current.ServerAddress + ":" + $Global:Current.PortWinbox$ServerLogin = " """ + $Global:Current.WinboxLogin + """"$ServerPass = " """ + $Global:Current.WinboxPwd + """"$Arg = "$ServerPort $ServerLogin $ServerPass "Start-Process -filePath $runwinbox -ArgumentList $Arg}}$buttonПодключитьVPN_Click = {#Обработка нажатия кнопки ПодключитьVPN$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их$richtextboxinfo.Clear() #Очищаем информационное поля для вывода туда информации о процессе подключения$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLineforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }) #Разрываем все установленные соединения{$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}Remove-VpnConnection $VPNname -Force #Удаляем соединение, если ранее оно было создано$RemoteNetworks = $Global:Current.RemoteNetwork.Split(';') -replace '\s', '' #Считываем и разбираем по строкам в массив список подсетей клиента разделенный ;switch ($Global:Current.VPNType) #В зависимости от типа VPNа создаем pptp или l2tp соединение{"pptp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем pptp подключение " + $VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -Force -RememberCredential -PassThru)}}"l2tp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем l2tp подключение " + $Global:Current.VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -Force -RememberCredential -PassThru)}}}$richtextboxinfo.AppendText("Устанавливаем " + $Global:Current.VPNType + " подключение к " + $VPNname + [System.Environment]::NewLine)$Errcon = Rasdial $VPNname $Global:Current.VPNLogin $Global:Current.VPNPass #Устанавливаем созданное VPN подключение и выводим информацию в поле$richtextboxinfo.Text = $richtextboxinfo.Text + [System.Environment]::NewLine + $Errcon + [System.Environment]::NewLineIf ((ipconfig | Select-String $VPNname -Quiet)) #Проверяем успешность соединения и, если все удачно, разблокируем кнопку RDP  и кнопку "Отключить VPN"{$buttonПодключитьRDP.Enabled = $true$buttonОтключитьVPN.Visible = $true$buttonОтключитьVPN.Enabled = $true$statusbar1.Text = $Global:Current.Name + ' подключен'}}$formКлиентыАльбус_FormClosing = [System.Windows.Forms.FormClosingEventHandler]{#При закрытии формы подчищаем за собой. Разрываем и удаляем все созданные соединения. foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Удаляем информацию о RPD-серверах из реестра$Global:Clients | ForEach-Object {$term = "TERMSRV/" + $_.RDPcompcmdkey /delete:$term}}$buttonПодключитьRDP_Click = {#Обработка кнопки ПодключитьRDP$RDPcomp = $comboboxRDP.Text$RDPuser = $textboxRDPLogin.Text$RDPpass = $textboxRdpPwd.Textcmdkey /generic:"TERMSRV/$RDPcomp" /user:"$RDPuser" /pass:"$RDPpass"mstsc /v:$RDPcomp}$buttonОтключитьVPN_Click = {#При отключении VPN подчищаем за собой и оповещаем о процессе в поле информацииforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force$buttonОтключитьVPN.Visible = $false$buttonПодключитьRDP.Enabled = $false$statusbar1.Text = $Global:Current.Name + ' отключен'}$buttonPingAll_Click={#Пингуем всех клиентов и оповещаем о результатах$I=0$richtextboxinfo.Clear()$richtextboxinfo.SelectionColor = 'Black'$clientscount = $Global:Clients.count$Global:Clients | ForEach-Object {if ((test-connection -Count 1 -computer $_.ServerAddress -quiet) -eq $True){$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($_.Name +' ('+ $_.ServerAddress +') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($_.Name + ' (' + $_.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}$richtextboxinfo.ScrollToCaret()$I = $I + 1Write-Progress -Activity "Ping in Progress" -Status "$i clients of $clientscount pinged" -PercentComplete ($i/$clientscount*100)}$richtextboxinfo.SelectionColor = 'Black'Write-Progress -Activity "Ping in Progress" -Status "Ready" -Completed}$buttonПеречитатьДанные_Click={#Перечитываем данные из таблицы Google$Global:Clients = Get-Clients ($Global:google_file)$listbox_clients.Items.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)}}$buttonВыход_Click = {#Выход$formКлиентыАльбус.Close()}$richtextboxinfo_LinkClicked=[System.Windows.Forms.LinkClickedEventHandler]{#Обработка нажатия на ссылку в окне информацииStart-Process $_.LinkText.ToString()}$buttonPing_Click={#Пингуем ip текущего клиента и выводим результат в поле информацииif ((test-connection -Count 1 -computer $Global:Current.ServerAddress -quiet) -eq $True){$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}}#----------------------------------------------#Описание объектов формы#----------------------------------------------## formКлиентыАльбус#$formКлиентыАльбус.Controls.Add($statusbar1)$formКлиентыАльбус.Controls.Add($groupboxTools)$formКлиентыАльбус.Controls.Add($groupboxRDP)$formКлиентыАльбус.Controls.Add($groupboxVPN)$formКлиентыАльбус.Controls.Add($richtextboxinfo)$formКлиентыАльбус.Controls.Add($listbox_clients)$formКлиентыАльбус.AutoScaleDimensions = '6, 13'$formКлиентыАльбус.AutoScaleMode = 'Font'$formКлиентыАльбус.AutoSize = $True$formКлиентыАльбус.ClientSize = '763, 446'$formКлиентыАльбус.FormBorderStyle = 'FixedSingle'$formКлиентыАльбус.MaximizeBox = $False$formКлиентыАльбус.Name = 'formКлиентыАльбус'$formКлиентыАльбус.SizeGripStyle = 'Hide'$formКлиентыАльбус.StartPosition = 'CenterScreen'$formКлиентыАльбус.Text = 'Клиенты Альбус'$formКлиентыАльбус.add_FormClosing($formКлиентыАльбус_FormClosing)$formКлиентыАльбус.add_Load($formКлиентыАльбус_Load)## statusbar1#$statusbar1.Location = '0, 424'$statusbar1.Name = 'statusbar1'$statusbar1.Size = '763, 22'$statusbar1.TabIndex = 17## groupboxTools#$groupboxTools.Controls.Add($buttonPing)$groupboxTools.Controls.Add($buttonВыход)$groupboxTools.Controls.Add($buttonWindox)$groupboxTools.Controls.Add($buttonПеречитатьДанные)$groupboxTools.Controls.Add($buttonPingAll)$groupboxTools.Location = '308, 258'$groupboxTools.Name = 'groupboxTools'$groupboxTools.Size = '147, 163'$groupboxTools.TabIndex = 10$groupboxTools.TabStop = $False$groupboxTools.Text = 'Tools'$groupboxTools.UseCompatibleTextRendering = $True## buttonPing#$buttonPing.Location = '7, 44'$buttonPing.Name = 'buttonPing'$buttonPing.Size = '133, 23'$buttonPing.TabIndex = 12$buttonPing.Text = 'Ping'$buttonPing.UseCompatibleTextRendering = $True$buttonPing.UseVisualStyleBackColor = $True$buttonPing.add_Click($buttonPing_Click)## buttonВыход#$buttonВыход.Location = '7, 125'$buttonВыход.Name = 'buttonВыход'$buttonВыход.Size = '133, 23'$buttonВыход.TabIndex = 15$buttonВыход.Text = 'Выход'$buttonВыход.UseCompatibleTextRendering = $True$buttonВыход.UseVisualStyleBackColor = $True$buttonВыход.add_Click($buttonВыход_Click)## buttonWindox#$buttonWindox.Enabled = $False$buttonWindox.Location = '7, 17'$buttonWindox.Name = 'buttonWindox'$buttonWindox.Size = '133, 23'$buttonWindox.TabIndex = 11$buttonWindox.Text = 'Windox'$buttonWindox.UseCompatibleTextRendering = $True$buttonWindox.UseVisualStyleBackColor = $True$buttonWindox.add_Click($buttonWindox_Click)## buttonПеречитатьДанные#$buttonПеречитатьДанные.Location = '7, 98'$buttonПеречитатьДанные.Name = 'buttonПеречитатьДанные'$buttonПеречитатьДанные.Size = '133, 23'$buttonПеречитатьДанные.TabIndex = 14$buttonПеречитатьДанные.Text = 'Перечитать данные'$buttonПеречитатьДанные.UseCompatibleTextRendering = $True$buttonПеречитатьДанные.UseVisualStyleBackColor = $True$buttonПеречитатьДанные.add_Click($buttonПеречитатьДанные_Click)## buttonPingAll#$buttonPingAll.Location = '7, 71'$buttonPingAll.Name = 'buttonPingAll'$buttonPingAll.Size = '133, 23'$buttonPingAll.TabIndex = 13$buttonPingAll.Text = 'Ping All'$buttonPingAll.UseCompatibleTextRendering = $True$buttonPingAll.UseVisualStyleBackColor = $True$buttonPingAll.add_Click($buttonPingAll_Click)## groupboxRDP#$groupboxRDP.Controls.Add($comboboxRDP)$groupboxRDP.Controls.Add($textboxRDPLogin)$groupboxRDP.Controls.Add($textboxRdpPwd)$groupboxRDP.Controls.Add($buttonПодключитьRDP)$groupboxRDP.Location = '308, 128'$groupboxRDP.Name = 'groupboxRDP'$groupboxRDP.Size = '147, 126'$groupboxRDP.TabIndex = 5$groupboxRDP.TabStop = $False$groupboxRDP.Text = 'RDP'$groupboxRDP.UseCompatibleTextRendering = $True## comboboxRDP#$comboboxRDP.FormattingEnabled = $True$comboboxRDP.Location = '7, 17'$comboboxRDP.Name = 'comboboxRDP'$comboboxRDP.Size = '133, 21'$comboboxRDP.TabIndex = 6$comboboxRDP.Text = 'IP RDP сервера'## textboxRDPLogin#$textboxRDPLogin.Location = '7, 44'$textboxRDPLogin.Name = 'textboxRDPLogin'$textboxRDPLogin.Size = '133, 20'$textboxRDPLogin.TabIndex = 7$textboxRDPLogin.Text = 'RDP-login'## textboxRdpPwd#$textboxRdpPwd.Location = '7, 69'$textboxRdpPwd.Name = 'textboxRdpPwd'$textboxRdpPwd.PasswordChar = '*'$textboxRdpPwd.Size = '133, 20'$textboxRdpPwd.TabIndex = 8$textboxRdpPwd.Text = 'RDP-Password'## buttonПодключитьRDP#$buttonПодключитьRDP.Enabled = $False$buttonПодключитьRDP.Location = '7, 94'$buttonПодключитьRDP.Name = 'buttonПодключитьRDP'$buttonПодключитьRDP.Size = '133, 20'$buttonПодключитьRDP.TabIndex = 9$buttonПодключитьRDP.Text = 'Подключить RDP'$buttonПодключитьRDP.UseCompatibleTextRendering = $True$buttonПодключитьRDP.UseVisualStyleBackColor = $True$buttonПодключитьRDP.add_Click($buttonПодключитьRDP_Click)## groupboxVPN#$groupboxVPN.Controls.Add($buttonПодключитьVPN)$groupboxVPN.Controls.Add($buttonОтключитьVPN)$groupboxVPN.Controls.Add($checkboxШлюзПоумолчанию)$groupboxVPN.Location = '308, 27'$groupboxVPN.Name = 'groupboxVPN'$groupboxVPN.Size = '147, 98'$groupboxVPN.TabIndex = 1$groupboxVPN.TabStop = $False$groupboxVPN.Text = 'VPN'$groupboxVPN.UseCompatibleTextRendering = $True## buttonПодключитьVPN#$buttonПодключитьVPN.Enabled = $False$buttonПодключитьVPN.Location = '7, 45'$buttonПодключитьVPN.Name = 'buttonПодключитьVPN'$buttonПодключитьVPN.Size = '133, 20'$buttonПодключитьVPN.TabIndex = 3$buttonПодключитьVPN.Text = 'Подключить VPN'$buttonПодключитьVPN.UseCompatibleTextRendering = $True$buttonПодключитьVPN.UseVisualStyleBackColor = $True$buttonПодключитьVPN.add_Click($buttonПодключитьVPN_Click)## buttonОтключитьVPN#$buttonОтключитьVPN.Enabled = $False$buttonОтключитьVPN.Location = '7, 67'$buttonОтключитьVPN.Name = 'buttonОтключитьVPN'$buttonОтключитьVPN.Size = '133, 20'$buttonОтключитьVPN.TabIndex = 4$buttonОтключитьVPN.Text = 'Отключить VPN'$buttonОтключитьVPN.UseCompatibleTextRendering = $True$buttonОтключитьVPN.UseVisualStyleBackColor = $True$buttonОтключитьVPN.Visible = $False$buttonОтключитьVPN.add_Click($buttonОтключитьVPN_Click)## checkboxШлюзПоумолчанию#$checkboxШлюзПоумолчанию.Location = '7, 19'$checkboxШлюзПоумолчанию.Name = 'checkboxШлюзПоумолчанию'$checkboxШлюзПоумолчанию.Size = '133, 24'$checkboxШлюзПоумолчанию.TabIndex = 2$checkboxШлюзПоумолчанию.Text = 'Шлюз по-умолчанию'$checkboxШлюзПоумолчанию.TextAlign = 'MiddleRight'$checkboxШлюзПоумолчанию.UseCompatibleTextRendering = $True$checkboxШлюзПоумолчанию.UseVisualStyleBackColor = $True## richtextboxinfo#$richtextboxinfo.Cursor = 'Default'$richtextboxinfo.ForeColor = 'WindowText'$richtextboxinfo.HideSelection = $False$richtextboxinfo.Location = '461, 27'$richtextboxinfo.Name = 'richtextboxinfo'$richtextboxinfo.ReadOnly = $True$richtextboxinfo.ScrollBars = 'ForcedVertical'$richtextboxinfo.ShowSelectionMargin = $True$richtextboxinfo.Size = '290, 394'$richtextboxinfo.TabIndex = 16$richtextboxinfo.Text = ''$richtextboxinfo.add_LinkClicked($richtextboxinfo_LinkClicked)## listbox_clients#$listbox_clients.FormattingEnabled = $True$listbox_clients.Location = '12, 27'$listbox_clients.Name = 'listbox_clients'$listbox_clients.Size = '290, 394'$listbox_clients.TabIndex = 0$listbox_clients.add_SelectedIndexChanged($listbox_clients_SelectedIndexChanged)#Save the initial state of the form$InitialFormWindowState = $formКлиентыАльбус.WindowState#Init the OnLoad event to correct the initial state of the form$formКлиентыАльбус.add_Load($Form_StateCorrection_Load)#Clean up the control events$formКлиентыАльбус.add_FormClosed($Form_Cleanup_FormClosed)#Store the control values when form is closing$formКлиентыАльбус.add_Closing($Form_StoreValues_Closing)#Show the Formreturn $formКлиентыАльбус.ShowDialog()}#Запуск приложения!Main ($CommandLine) 

Скрипт можно запускать как скрипт ps1 или скомпилировать в exe через ps2exe и использовать как полноценное приложение

Подробнее..

Производительность компилятора при работе с концептами в C20

20.06.2021 18:15:44 | Автор: admin

Привет, меня зовут Александр, я старший разработчик ПО в Центре разработкиOrionInnovation. Хочу признаться, я люблю рассказывать про C++ и не только на различных митапах и конференциях.Ивотядобрался доХабра. НаCppConfRussiaPiter2020 я рассказывал про концепты и послевыступленияполучилочень много вопросов про производительность компилятора при работе сними.Замеры производительности не были цельюмоегодоклада:мне было известно, что концепты компилируются с примерно такой же скоростью, что и обычные метапрограммы,адодетального сравнения я смог добраться совершенно недавно.Спешуподелиться результатом!

Несколько слов о концептах

Концептыпереосмыслениеметапрограммирования, аналогичноеconstexpr.Еслиconstexprэто про вычисление выраженийво время компиляции, будь то факториал, экспонента и так далее, то концептыэто про перегрузки, специализации, условия существования сущностей.Вобщем, про чистоеметапрограммирование. Иными словами, в C++20 появилась возможность писать конструкциибез единой, привычной для нас треугольной скобки, тем самым получая возможность быстро и читаемо описать какую-либо перегрузку или специализацию:

// #1void increment(auto & arg) requires requires { ++arg; }; // #2void increment(auto &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);  // Вызывается #1    increment(ni); // Вызывается #2}

О том, как всё это работает, есть море информации,например, отличный гайд "Концепты: упрощаем реализацию классов STD Utility" по мотивам выступления Андрея Давыдова на C++ Russia 2019. Ну а мы сфокусируемся на том, какой ценой достигается подобный функционал, чтобы убедиться, чтоэтонетолькопросто, быстро и красиво, ноещёи эффективно.

Описание эксперимента

Итак, мы будем наблюдать за следующими показателями:

  1. Время компиляции

  1. Размер объектного файла

  1. Количество символов в записи (или же количество кода), в некоторых случаях

Прежде чем мы начнём несколько важных уточнений:

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

  • Во-вторых, в данной статье мы посмотрим лишь на самые простые (буквально несколько строк) случаи, чтобы быть уверенными на 100%, что мы сравниваем абсолютно аналогичные фрагменты кода.

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

В замерах будут участвовать clang 12.0.0 и g++ 10.3.0, как с полной оптимизацией, так и без неё.

В качестве операционной системы выступит Ubuntu 16.04, запущенная на Windows 10 через WSL2.На всякий случай прилагаю характеристики ПК:

Характеристики ПК
------------------System Information------------------         Operating System: Windows 10 Enterprise 64-bit (10.0, Build 19043) (19041.vb_release.191206-1406)                 Language: Russian (Regional Setting: Russian)      System Manufacturer: Dell Inc.             System Model: Latitude 5491                     BIOS: 1.12.0 (type: UEFI)                Processor: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz (8 CPUs), ~2.3GHz                   Memory: 32768MB RAM      Available OS Memory: 32562MB RAM                Page File: 9995MB used, 27430MB available------------------------Disk & DVD/CD-ROM Drives------------------------      Drive: C: Free Space: 26.5 GBTotal Space: 243.0 GBFile System: NTFS      Model: SAMSUNG SSD PM871b M.2 2280 256GB

Эксперименты

Посленеобходимыхотступлениймы можем,наконец,начать эксперименты.

Эксперимент 1: Эволюция метапрограммирования

Для началапосмотрим на то, как компиляторы справляются с созданием перегрузки функции для инкрементируемых инеинкрементируемыхтипов данных аргумента. Компилируемый код для C++ 03, 17 и 20 представлены ниже. Один из показателей, а именнообъем кода, можно оценить уже сейчас: видно, что количество кода существенно сокращается по мере эволюции языка, уступая место читаемости и простоте.

Код
incrementable_03.cpp
// copied from boosttemplate<bool C, typename T = void>struct enable_if { typedef T type; };template<typename T>struct enable_if<false, T> {};namespace is_inc {    typedef char (&yes)[1]; typedef char (&no)[2];struct tag {};struct any { template &lt;class T&gt; any(T const&amp;); };tag operator++(any const &amp;);template&lt;typename T&gt;static yes test(T const &amp;);static no test(tag);template&lt;typename _T&gt; struct IsInc{    static _T &amp; type_value;    static const bool value = sizeof(yes) == sizeof(test(++type_value));};}template<typename T>struct IsInc : public is_inc::IsInc<T> {};template<class Ty>typename enable_if<IsInc<Ty>::value>::type increment(Ty &);template<class Ty>typename enable_if<!IsInc<Ty>::value>::type increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}
incrementable_17.cpp
#include <type_traits>template<class, class = std::void_t<>>struct IsInc : std::false_type {};template<class T>struct IsInc<T, std::void_t<decltype( ++std::declval<T&>() )>>    : std::true_type{};template<class Ty>std::enable_if_t<IsInc<Ty>::value> increment(Ty &);template<class Ty>std::enable_if_t<!IsInc<Ty>::value> increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}
incrementable_20.cpp
void increment(auto & arg) requires requires { ++arg; };void increment(auto &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}

Давайте взглянем на результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

Количество символов, шт

incrementable_03.cpp

clang

O0

43,02

1304

782

incrementable_17.cpp

clang

O0

67,46

1320

472

incrementable_20.cpp

clang

O0

43,42

1304

230

incrementable_03.cpp

clang

O3

47,21

1296

782

incrementable_17.cpp

clang

O3

77,77

1304

472

incrementable_20.cpp

clang

O3

45,70

1288

230

incrementable_03.cpp

gcc

O0

19,89

1568

782

incrementable_17.cpp

gcc

O0

34,71

1568

472

incrementable_20.cpp

gcc

O0

17,62

1480

230

incrementable_03.cpp

gcc

O3

18,44

1552

782

incrementable_17.cpp

gcc

O3

38,94

1552

472

incrementable_20.cpp

gcc

O3

18,57

1464

230

Как уже отмечалось ранее,количество кода существенно уменьшается по мере развития языка: c 782 до 472 и затем до 230.Разницапочти в 3,5 раза, если сравнитьС++20 и С++03 (на самом деле даже больше,т.к.порядка150170символов во всех примерахтестирующий код). Размеры объектного файла также постепенно уменьшаются. Что же современем компиляции? Странно, новремя компиляции 03 и 20 примерно равно, а вот в С++17в два раза больше. Давайте взглянем на код наших примеров: помимо всего прочего, в глаза бросается#includeв случае C++17. Давайте реализуемdeclval,enable_ifиvoid_tи проверим:

incrementable_no_tt.cpp
template<bool C, typename T = void>struct enable_if { typedef T type; };template<typename T>struct enable_if<false, T> {};template<bool B, typename T = void>using enable_if_t = typename enable_if<B, T>::type;template<typename ...>using void_t = void;template<class T>T && declval() noexcept;template<class, class = void_t<>>struct IsInc {    constexpr static bool value = false;};template<class T>struct IsInc<T, void_t<decltype( ++declval<T&>() )>>{    constexpr static bool value = true;};template<class Ty>enable_if_t<IsInc<Ty>::value> increment(Ty &);template<class Ty>enable_if_t<!IsInc<Ty>::value> increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}

И давайте обновим нашу таблицу:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

Количество символов, шт

incrementable_03.cpp

clang

O0

43,02

1304

782

incrementable_17_no_tt.cpp

clang

O0

44,498

1320

714

incrementable_20.cpp

clang

O0

43,419

1304

230

incrementable_03.cpp

clang

O3

47,205

1296

782

incrementable_17_no_tt.cpp

clang

O3

47,327

1312

714

incrementable_20.cpp

clang

O3

45,704

1288

230

incrementable_03.cpp

gcc

O0

19,885

1568

782

incrementable_17_no_tt.cpp

gcc

O0

21,163

1584

714

incrementable_20.cpp

gcc

O0

17,619

1480

230

incrementable_03.cpp

gcc

O3

18,442

1552

782

incrementable_17_no_tt.cpp

gcc

O3

19,057

1568

714

incrementable_20.cpp

gcc

O3

18,566

1464

230

Время компиляции на 17 стандарте нормализовалось и стало практически равно времени компиляции 03 и 20, однако количество кода стало близко к самому тяжёлому, базовому варианту. Так что, если у вас есть под рукой C++20 и нужно написать какую-то простую мета-перегрузку,смело можно использовать концепты. Это читабельнее, компилируется примерно с такой же скоростью, а результат компиляции занимает меньше места.

Эксперимент 2: Ограничения для методов

Давайте взглянем на еще одну особенность: ограничение для функции или метода (в том числе и для конструкторов и деструкторов) на примере типаOptionalLike, имеющего деструктор по умолчанию в случае, если помещаемый объект тривиален, а иначедеструктор, выполняющийдеинициализациюкорректно. Код представлен ниже:

Код
optional_like_17.cpp
#include <type_traits>#include <string>template<typename T, typename = void>struct OptionalLike {    ~OptionalLike() {        /* Calls d-tor manually */    }};template<typename T>struct OptionalLike<T, std::enable_if_t<std::is_trivially_destructible<T>::value>>{    ~OptionalLike() = default;};void later() {    OptionalLike<int>         oli;    OptionalLike<std::string> ols;}
optional_like_20.cpp
#include <type_traits>#include <string>template<typename T>struct OptionalLike{    ~OptionalLike() {        /* Calls d-tor manually */    }    ~OptionalLike() requires (std::is_trivially_destructible<T>::value) = default;};void later() {    OptionalLike<int>         oli;    OptionalLike<std::string> ols;}

Давайте взглянем на результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

Количество символов, шт

optional_like_17.cpp

clang

O0

487,62

1424

319

optional_like_20.cpp

clang

O0

616,8

1816

253

optional_like_17.cpp

clang

O3

490,07

944

319

optional_like_20.cpp

clang

O3

627,64

1024

253

optional_like_17.cpp

gcc

O0

202,29

1968

319

optional_like_20.cpp

gcc

O0

505,82

1968

253

optional_like_17.cpp

gcc

O3

205,55

1200

319

optional_like_20.cpp

gcc

O3

524,54

1200

253

Мы видим, что новый вариант выглядит более читабельным и лаконичным (253 символа против 319 у классического), однако платим за это временем компиляции: оба компилятора как с оптимизацией, так и без показали худшее время компиляции в случае с концептами. GCC аж в 22,5 раза медленнее. При этом размер объектного файла уgccне изменяется вовсе, а в случаеclangбольше для концептов. Классический компромисс: либо меньше кода, но дольше компиляция, либо больше кода, но быстрее компиляция.

Эксперимент 3: Влияние использования концептов на время компиляции

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

Код
inline.cpp
template<typename T>void foo() requires (sizeof(T) >= 4) { }template<typename T>void foo() {}void later() {    foo<char>();    foo<int>();}
concept.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo() requires IsBig<T> { }template<typename T>void foo() {}void later() {    foo<char>();    foo<int>();}

Сразу взглянем на результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

inline.cpp

clang

O0

38,666

1736

concept.cpp

clang

O0

39,868

1736

concept.cpp

clang

O3

42,578

1040

inline.cpp

clang

O3

43,610

1040

inline.cpp

gcc

O0

14,598

1976

concept.cpp

gcc

O0

14,640

1976

concept.cpp

gcc

O3

14,872

1224

inline.cpp

gcc

O3

14,951

1224

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

Эксперимент 4: Варианты ограничения функции

Теперь посмотрим на варианты наложения ограничения на шаблонные параметры на примере функций. Ограничить функцию можно аж четырьмя способами:

  • Имя концепта вместоtypename

  • Requires clauseпослеtemplate<>

  • Имя концепта рядом сauto

  • Trailing requiresclause

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

Код
instead_of_typename.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<IsBig T>void foo(T const &) { }template<typename T>void foo(T const &) {}void later() {    foo<char>('a');    foo<int>(1);}
after_template.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>    requires IsBig<T>void foo(T const &) { }template<typename T>void foo(T const &) {}void later() {    foo<char>('a');    foo<int>(1);}
with_auto.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo(IsBig auto const &) { }template<typename T>void foo(auto const &) {}void later() {    foo<char>('a');    foo<int>(1);}
requires_clause.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo(T const &) requires IsBig<T> { }template<typename T>void foo(T const &) {}void later() {    foo<char>('a');    foo<int>(1);}

А вот и результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

function_with_auto.cpp

clang

O0

40,878

1760

function_after_template.cpp

clang

O0

41,947

1760

function_requires_clause.cpp

clang

O0

42,551

1760

function_instead_of_typename.cpp

clang

O0

46,893

1760

function_with_auto.cpp

clang

O3

43,928

1024

function_requires_clause.cpp

clang

O3

45,176

1032

function_after_template.cpp

clang

O3

45,275

1032

function_instead_of_typename.cpp

clang

O3

50,42

1032

function_requires_clause.cpp

gcc

O0

16,561

2008

function_with_auto.cpp

gcc

O0

16,692

2008

function_after_template.cpp

gcc

O0

17,032

2008

function_instead_of_typename.cpp

gcc

O0

17,802

2016

function_requires_clause.cpp

gcc

O3

16,233

1208

function_with_auto.cpp

gcc

O3

16,711

1208

function_after_template.cpp

gcc

O3

17,216

1208

function_instead_of_typename.cpp

gcc

O3

18,315

1216

Как мы видим, время компиляции отличается незначительно, однако мы можем заметить следующее:

  • Вариант с использованием имени концепта вместоtypenameоказался самым медленным во всех случаях.

  • Вариантыtrailing requiresclauseили использование концепта рядом сautoоказались самыми быстрыми.

  • Варианты, где присутствуетtemplate<>на510% медленнее остальных.

  • Размерыобъектных файлов изменяются незначительно, однако вариант с именем концепта вместоtypenameоказался самым объемным в случаеgcc, а вариант сautoоказался наименее объемным в случаеclang.

Эксперимент 5: Влияние сложности концепта на время компиляции

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

Код
concept_complexity_1.cpp
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept TestedConcept = ConceptA<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() {    int i { 0 };    int * ip = &i;    foo(i);    foo(ip);}
concept_complexity_2.cpp
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept ConceptB =  requires(T i, int x) {    { i++     } noexcept -> ConceptA;    { ++i     } noexcept -> ConceptA;    { i--     } noexcept -> ConceptA;    { --i     } noexcept -> ConceptA;    { i + i   } noexcept -> ConceptA;    { i - i   } noexcept -> ConceptA;    { i += i  } noexcept -> ConceptA;    { i -= i  } noexcept -> ConceptA;    { i * i      } noexcept -> ConceptA;    { i / i      } noexcept -> ConceptA;    { i % i      } noexcept -> ConceptA;    { i *= i     } noexcept -> ConceptA;    { i /= i     } noexcept -> ConceptA;    { i %= i     } noexcept -> ConceptA;    { i |  i     } noexcept -> ConceptA;    { i &  i     } noexcept -> ConceptA;    { i |= i     } noexcept -> ConceptA;    { i &= i     } noexcept -> ConceptA;    { ~i          } noexcept -> ConceptA;    { i ^  i      } noexcept -> ConceptA;    { i << x      } noexcept -> ConceptA;    { i >> x      } noexcept -> ConceptA;    { i ^=  i      } noexcept -> ConceptA;    { i <<= x      } noexcept -> ConceptA;    { i >>= x      } noexcept -> ConceptA;};template<typename T>concept ConceptC =  requires(T i, int x) {    { i++     } noexcept -> ConceptB;    { ++i     } noexcept -> ConceptB;    { i--     } noexcept -> ConceptB;    { --i     } noexcept -> ConceptB;    { i + i   } noexcept -> ConceptB;    { i - i   } noexcept -> ConceptB;    { i += i  } noexcept -> ConceptB;    { i -= i  } noexcept -> ConceptB;    { i * i      } noexcept -> ConceptB;    { i / i      } noexcept -> ConceptB;    { i % i      } noexcept -> ConceptB;    { i *= i     } noexcept -> ConceptB;    { i /= i     } noexcept -> ConceptB;    { i %= i     } noexcept -> ConceptB;    { i |  i     } noexcept -> ConceptB;    { i &  i     } noexcept -> ConceptB;    { i |= i     } noexcept -> ConceptB;    { i &= i     } noexcept -> ConceptB;    { ~i          } noexcept -> ConceptB;    { i ^  i      } noexcept -> ConceptB;    { i << x      } noexcept -> ConceptB;    { i >> x      } noexcept -> ConceptB;    { i ^=  i      } noexcept -> ConceptB;    { i <<= x      } noexcept -> ConceptB;    { i >>= x      } noexcept -> ConceptB;};template<typename T>concept ConceptD =  requires(T i, int x) {    { i++     } noexcept -> ConceptC;    { ++i     } noexcept -> ConceptC;    { i--     } noexcept -> ConceptC;    { --i     } noexcept -> ConceptC;    { i + i   } noexcept -> ConceptC;    { i - i   } noexcept -> ConceptC;    { i += i  } noexcept -> ConceptC;    { i -= i  } noexcept -> ConceptC;    { i * i      } noexcept -> ConceptC;    { i / i      } noexcept -> ConceptC;    { i % i      } noexcept -> ConceptC;    { i *= i     } noexcept -> ConceptC;    { i /= i     } noexcept -> ConceptC;    { i %= i     } noexcept -> ConceptC;    { i |  i     } noexcept -> ConceptC;    { i &  i     } noexcept -> ConceptC;    { i |= i     } noexcept -> ConceptC;    { i &= i     } noexcept -> ConceptC;    { ~i          } noexcept -> ConceptC;    { i ^  i      } noexcept -> ConceptC;    { i << x      } noexcept -> ConceptC;    { i >> x      } noexcept -> ConceptC;    { i ^=  i      } noexcept -> ConceptC;    { i <<= x      } noexcept -> ConceptC;    { i >>= x      } noexcept -> ConceptC;};template<typename T>concept TestedConcept = ConceptA<T> && ConceptB<T> && ConceptC<T> && ConceptD<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() {    int i { 0 };    int * ip = &i;    foo(i);    foo(ip);}
concept_complexity_3.cpp
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept ConceptB =  requires(T i, int x) {    { i++     } noexcept -> ConceptA;    { ++i     } noexcept -> ConceptA;    { i--     } noexcept -> ConceptA;    { --i     } noexcept -> ConceptA;    { i + i   } noexcept -> ConceptA;    { i - i   } noexcept -> ConceptA;    { i += i  } noexcept -> ConceptA;    { i -= i  } noexcept -> ConceptA;    { i * i      } noexcept -> ConceptA;    { i / i      } noexcept -> ConceptA;    { i % i      } noexcept -> ConceptA;    { i *= i     } noexcept -> ConceptA;    { i /= i     } noexcept -> ConceptA;    { i %= i     } noexcept -> ConceptA;    { i |  i     } noexcept -> ConceptA;    { i &  i     } noexcept -> ConceptA;    { i |= i     } noexcept -> ConceptA;    { i &= i     } noexcept -> ConceptA;    { ~i          } noexcept -> ConceptA;    { i ^  i      } noexcept -> ConceptA;    { i << x      } noexcept -> ConceptA;    { i >> x      } noexcept -> ConceptA;    { i ^=  i      } noexcept -> ConceptA;    { i <<= x      } noexcept -> ConceptA;    { i >>= x      } noexcept -> ConceptA;};template<typename T>concept ConceptC =  requires(T i, int x) {    { i++     } noexcept -> ConceptB;    { ++i     } noexcept -> ConceptB;    { i--     } noexcept -> ConceptB;    { --i     } noexcept -> ConceptB;    { i + i   } noexcept -> ConceptB;    { i - i   } noexcept -> ConceptB;    { i += i  } noexcept -> ConceptB;    { i -= i  } noexcept -> ConceptB;    { i * i      } noexcept -> ConceptB;    { i / i      } noexcept -> ConceptB;    { i % i      } noexcept -> ConceptB;    { i *= i     } noexcept -> ConceptB;    { i /= i     } noexcept -> ConceptB;    { i %= i     } noexcept -> ConceptB;    { i |  i     } noexcept -> ConceptB;    { i &  i     } noexcept -> ConceptB;    { i |= i     } noexcept -> ConceptB;    { i &= i     } noexcept -> ConceptB;    { ~i          } noexcept -> ConceptB;    { i ^  i      } noexcept -> ConceptB;    { i << x      } noexcept -> ConceptB;    { i >> x      } noexcept -> ConceptB;    { i ^=  i      } noexcept -> ConceptB;    { i <<= x      } noexcept -> ConceptB;    { i >>= x      } noexcept -> ConceptB;};template<typename T>concept ConceptD =  requires(T i, int x) {    { i++     } noexcept -> ConceptC;    { ++i     } noexcept -> ConceptC;    { i--     } noexcept -> ConceptC;    { --i     } noexcept -> ConceptC;    { i + i   } noexcept -> ConceptC;    { i - i   } noexcept -> ConceptC;    { i += i  } noexcept -> ConceptC;    { i -= i  } noexcept -> ConceptC;    { i * i      } noexcept -> ConceptC;    { i / i      } noexcept -> ConceptC;    { i % i      } noexcept -> ConceptC;    { i *= i     } noexcept -> ConceptC;    { i /= i     } noexcept -> ConceptC;    { i %= i     } noexcept -> ConceptC;    { i |  i     } noexcept -> ConceptC;    { i &  i     } noexcept -> ConceptC;    { i |= i     } noexcept -> ConceptC;    { i &= i     } noexcept -> ConceptC;    { ~i          } noexcept -> ConceptC;    { i ^  i      } noexcept -> ConceptC;    { i << x      } noexcept -> ConceptC;    { i >> x      } noexcept -> ConceptC;    { i ^=  i      } noexcept -> ConceptC;    { i <<= x      } noexcept -> ConceptC;    { i >>= x      } noexcept -> ConceptC;};template<typename T>concept ConceptE =  requires(T i, int x) {    { i++     } noexcept -> ConceptD;    { ++i     } noexcept -> ConceptD;    { i--     } noexcept -> ConceptD;    { --i     } noexcept -> ConceptD;    { i + i   } noexcept -> ConceptD;    { i - i   } noexcept -> ConceptD;    { i += i  } noexcept -> ConceptD;    { i -= i  } noexcept -> ConceptD;    { i * i      } noexcept -> ConceptD;    { i / i      } noexcept -> ConceptD;    { i % i      } noexcept -> ConceptD;    { i *= i     } noexcept -> ConceptD;    { i /= i     } noexcept -> ConceptD;    { i %= i     } noexcept -> ConceptD;    { i |  i     } noexcept -> ConceptD;    { i &  i     } noexcept -> ConceptD;    { i |= i     } noexcept -> ConceptD;    { i &= i     } noexcept -> ConceptD;    { ~i          } noexcept -> ConceptD;    { i ^  i      } noexcept -> ConceptD;    { i << x      } noexcept -> ConceptD;    { i >> x      } noexcept -> ConceptD;    { i ^=  i      } noexcept -> ConceptD;    { i <<= x      } noexcept -> ConceptD;    { i >>= x      } noexcept -> ConceptD;};template<typename T>concept ConceptF =  requires(T i, int x) {    { i++     } noexcept -> ConceptE;    { ++i     } noexcept -> ConceptE;    { i--     } noexcept -> ConceptE;    { --i     } noexcept -> ConceptE;    { i + i   } noexcept -> ConceptE;    { i - i   } noexcept -> ConceptE;    { i += i  } noexcept -> ConceptE;    { i -= i  } noexcept -> ConceptE;    { i * i      } noexcept -> ConceptE;    { i / i      } noexcept -> ConceptE;    { i % i      } noexcept -> ConceptE;    { i *= i     } noexcept -> ConceptE;    { i /= i     } noexcept -> ConceptE;    { i %= i     } noexcept -> ConceptE;    { i |  i     } noexcept -> ConceptE;    { i &  i     } noexcept -> ConceptE;    { i |= i     } noexcept -> ConceptE;    { i &= i     } noexcept -> ConceptE;    { ~i          } noexcept -> ConceptE;    { i ^  i      } noexcept -> ConceptE;    { i << x      } noexcept -> ConceptE;    { i >> x      } noexcept -> ConceptE;    { i ^=  i      } noexcept -> ConceptE;    { i <<= x      } noexcept -> ConceptE;    { i >>= x      } noexcept -> ConceptE;};template<typename T>concept ConceptG =  requires(T i, int x) {    { i++     } noexcept -> ConceptF;    { ++i     } noexcept -> ConceptF;    { i--     } noexcept -> ConceptF;    { --i     } noexcept -> ConceptF;    { i + i   } noexcept -> ConceptF;    { i - i   } noexcept -> ConceptF;    { i += i  } noexcept -> ConceptF;    { i -= i  } noexcept -> ConceptF;    { i * i      } noexcept -> ConceptF;    { i / i      } noexcept -> ConceptF;    { i % i      } noexcept -> ConceptF;    { i *= i     } noexcept -> ConceptF;    { i /= i     } noexcept -> ConceptF;    { i %= i     } noexcept -> ConceptF;    { i |  i     } noexcept -> ConceptF;    { i &  i     } noexcept -> ConceptF;    { i |= i     } noexcept -> ConceptF;    { i &= i     } noexcept -> ConceptF;    { ~i          } noexcept -> ConceptF;    { i ^  i      } noexcept -> ConceptF;    { i << x      } noexcept -> ConceptF;    { i >> x      } noexcept -> ConceptF;    { i ^=  i      } noexcept -> ConceptF;    { i <<= x      } noexcept -> ConceptF;    { i >>= x      } noexcept -> ConceptF;};template<typename T>concept TestedConcept = ConceptA<T> && ConceptB<T> && ConceptC<T> && ConceptD<T> &&                                       ConceptE<T> && ConceptF<T> && ConceptG<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() {    int i { 0 };    int * ip = &i;    foo(i);    foo(ip);}

Давайте взглянем на результат:

Файл

Компиляция

Время, мс

Количество символов, шт

concept_complexity_1.cpp

clang

O0

37,441

201

concept_complexity_2.cpp

clang

O0

38,211

2244

concept_complexity_3.cpp

clang

O0

39,989

4287

concept_complexity_1.cpp

clang

O3

40,062

201

concept_complexity_2.cpp

clang

O3

40,659

2244

concept_complexity_3.cpp

clang

O3

43,314

4287

concept_complexity_1.cpp

gcc

O0

15,352

201

concept_complexity_2.cpp

gcc

O0

16,077

2244

concept_complexity_3.cpp

gcc

O0

18,091

4287

concept_complexity_1.cpp

gcc

O3

15,243

201

concept_complexity_2.cpp

gcc

O3

17,552

2244

concept_complexity_3.cpp

gcc

O3

18,51

4287

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

Заключение

В результате вышеописанных экспериментов мы можем сделать следующие выводы:

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

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

  • Время компиляции прямо пропорционально сложности концептов/constraint'ов.

Post Scriptum

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

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

Подробнее..

Принцип работы EditorScript

21.06.2021 00:15:47 | Автор: admin

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


EditorScript - это такой скрипт, который можно запустить по нажатию комбинации Ctrl+Shift+X прямо из движка. Он может исполнять различные служебные функции. К примеру создать необходимую структуру каталогов в проекте.

toolextends EditorScriptvar folders: PoolStringArray = [# Наши папки к созданию"res://assets/textures/",# Их определяет разработчик"res://assets/fonts/",# Просто напишите сюда все конечные пути, что хотите создать"res://resources/","res://addons/","res://scenes/","res://scripts/singletons/","res://scripts/resources/","res://scripts/editor_scripts/"]var placeholder: Resource = load("res://placeholder.tres")# Ресурс-пустышка для сохранения структуры файлов. По желаниюfunc _run() -> void: # Входная функция этого скриптаvar dir = Directory.new()for folder in folders: # проход по списку папокif !dir.dir_exists(folder):var err = OKerr = dir.make_dir_recursive(folder)if err != OK:prints("Error", err)returnelse:if !dir.file_exists(folder.plus_file("placeholder.tres")):# Создаём файл плейсхолдера для гита если не существуетerr = ResourceSaver.save(folder.plus_file("placeholder.tres"), placeholder)if err != OK:prints("Error", err) returnelse:# Говорим что создали (не обязательно)prints("Making", folder)prints("Making", folder.plus_file("placeholder.tres"))print("Successful. Structure created or already exists.")return

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

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

Спасибо за внимание. Не думаю, что буду писать статьи в дальнейшем очень часто, но у меня есть идеи, о чём можно написать.

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru