Приведет ли автоматизация к экономическому кризису?

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

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

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

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

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

Что происходит с технологической безработицей сегодня?

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

Еще один показатель, который следует учитывать, — это ВВП . До 1000 г. н.э. ВВП на душу населения везде был ниже 1000 долларов с поправкой на инфляцию. В западных странах сегодня это около 50000 долларов. Поскольку врожденные способности человека не изменились, это должно быть результатом инноваций (в образовании, процессах, инструментах, оборудовании и т. д.), которые позволяют людям производить больше ценности за каждый час работы. С одной стороны, это означает, что на каждого человека в этих странах уже приходится 49 «роботов» автоматизации. Тем не менее занятость остается примерно на том же уровне, что и всегда.

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

Есть ли вещи, в которых компьютеры никогда не будут хороши?

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

Какие рабочие места будут автоматизированы?

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

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

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

  • производство товаров ручной работы

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

  • некоторые виды приготовления пищи

  • перформансы (актерское мастерство, танцы, комедия и т. д.)

  • работа по дому (личные слуги, такие как садовники и т. д.)

  • спорт

  • терапия

  • парикмахерское дело и тому подобное

  • массаж

  • некоторые аспекты медицинской помощи (ощущение заботы)

  • некоторые аспекты обучения (мотивация, наставничество)

  • некоторые аспекты войны (решения о том, когда применять насильственную силу)

  • работа духовенства

  • услуги морга

  • некоторые виды продаж

  • политика

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

По мере того, как все больше рабочих мест будет автоматизировано, какие экономические эффекты мы должны ожидать?

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

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

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

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

Заключение

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


ссылка на оригинал статьи https://habr.com/ru/post/714042/

Как программисту снизить когнитивную нагрузку: три способа

Предотвращение когнитивной перегрузки программиста — это ключ к недопущению ошибок и ускорению разработки.

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

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

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

Известные мне три базовых способа решения проблемы когнитивной перегрузки программиста можно резюмировать так: «Разделяй», «Упрощай» и «Делегируй».

Разделяй

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

Есть два отличных друг от друга способа разделения: вертикальное и горизонтальное.

Вертикальное разделение — наиболее распространённое. Оно происходит в четком соответствии с бизнес-правилами и бизнес-требованиями. Его проще всего применять, потому что оно может даже соответствовать естественной структуре организации по отделам. Здесь в игру вступают техники а-ля Domain-Driven Design, позволяющие вокруг каждой проблемы построить свой четко очерченный домен.

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

Общая рекомендация — разбивать команды, как только они разрастутся, а внутри групп появятся свои подгруппы узкой специализации. Логику, лежащую в основе техники разделения, совсем недавно озвучили авторы книги «Топологии команд». У них нашлись весомые аргументы в пользу создания команд вокруг концепции когнитивного воздействия. Я предлагаю вам посмотреть видео, в котором изложены некоторые основные концепции:

Упрощай

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

Это очень хорошо сочетается с концепцией связанности. Вот едва ли не самое примитивное определение связанности: А и В связаны, если изменение в А может повлечь за собой изменение в В. Если архитектура кода подобрана правильно, связанность между объектами низка, и программисту не нужно рассматривать больший набор компонентов, чем необходимо; таким образом, когнитивная нагрузка снижается.

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

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

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

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

Делегируй

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

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

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

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

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

Есть и другие способы снизить когнитивную нагрузку. Может быть, не такие действенные, как описанные выше, но все они важны по-своему. Например, в эпизоде «Если все ненавидят собрания, почему у нас их так много?» говорится о том, что «Есть два типа расписаний […] расписание руководителя и расписание исполнителя». Выходит, что если менеджеры разбивают дни на часовые интервалы, то программисты и прочие «настоящие» работники разбивают дни на половинки. Поэтому нам не стоит отвлекать программистов совещаниями, это позволит не перегружать их лишней информацией в тот момент, когда они занимаются решением конкретных проблем.

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

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


ссылка на оригинал статьи https://habr.com/ru/company/ispsystem/blog/712372/

Kotlin/Golang работа в двух языках

Сразу дисклеймер, статья больше про Golang, но мой «родной» и основной на протяжении уже 6 лет — Kotlin — буду рад если будут замечания по Golang части в комментариях

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

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

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

  • портирования кода (в обе стороны)

  • реализации каких-то компонентов сразу на 2-х языках (в основном это внутренние SDK)

Сразу скажу — почему эта задача вообще в целом для нас легкая и подъемная — наши бэкенды на Kotlin строятся на микрофреймворках типа Ktor, а не на Spring или не дай бог JavaEE, соответственно тяжелых вопросов соответствия каких-то лютых Enterprise монструозных JAR каким-то решениям в Golang не стоит.

Естественно, что мы сейчас говорим про языки и соответствие КОНСТРУКЦИЙ , а не про библиотеки или фреймворки.

Ну и некоторых ставит поначалу в ступор, что Kotlin/JVM это «про классы» и «не натив», а Golang это вроде как «процедурный стиль» и «натив».

На деле практически все довольно органично воспроизводится. В этой статье приведу некоторые примеры взаимозаменяемых конструкций и хаков. Материал в основном для тех кто пишет на Kotlin и для кого Golang — второй язык.

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

Простые:

  1. перенос один в один или очень близко в той же структуре кода (без учета непосредственно синтаксиса языка) с сохранением канона (каноничного стиля кода GoLang) — да почти все

  2. перенос один в один или через хаки, но без сохранения канона — код и API в итоге очень похоже на Kotlin, но при этом код не каноничен для GoLang — например статические методы, синглтоны, компаньоны

  3. нельзя перенести один в один, но есть канонические легко осваиваемые паттерны которые «по духу» и смыслу аналогичны Kotlin — как ни странно — почти все ООП воспроизводится без особых потерь на структурах без классов

Тяжелые:

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

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

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

List<T>.filter vs List<T>.map

Начнем с примера в котором сразу будут показаны некоторые типовые переносы и один невозможный перенос.

Итак мы хотим перенести в Go функциональную обработку коллекций (а там этого явно не хватает, понятно есть какие-то внешние пакеты, но допустим хотим свое)

fun <T> List<T>.filter(condition : (T)->Boolean): List<T> {     return buildList {       for (item in this) {         if (condition(item)){           add(item)         }       }     } } fun <T,R> List<T>.map(mapper : (T)->R): List<R> {     return buildList {       for (item in this) {           add(mapper(item))       }     } } 

И вот мы начинаем воспроизводить

Во-первых мы хотим это исполнить именно как метод, а не как функцию, чтобы их делать в цепь l.Filter().Filter().Map().First(), а не вкладывать First( Map( Filter ( Filter(l)))

Пробуем решить в лоб (не получится)

// пробуем навесить метод прямо на срез func (s []any) Filter(condition func(item any) bool) []any 

Сразу куча проблем — во-первых так нельзя — навешивать функции на чужие типы, во-вторых у нас резко теряется информация о типе!

// пробуем сделать generic-метод func (s []T) Filter[T any](condition func(item T) bool) []any

а так тем более нельзя — потому что вообще нет GENERIC методов в Golang, не завезли, функции есть, а методов — нет!

Но тут на помощь приходит то, что по своей природе Golang — это в своей основе C, где нет аьясов типа, а есть создание типа на основе данного. Вот так можно:

type List[T any] []T func (l List[T]) Filter(condition func(item T) bool) List[T]

Итак — первое, что уже можно выучить — нельзя навесить «расширение» на уже кем-то в другом пакете написанную структуру, но можно сделать тип в своем пакете, эквивалентный целевому и сделать метод уже у него!

но так просто это использовать не получится, потребуется:

// так не получится ([]int{1,2,3}).Filter(func(item int) bool {return item > 1}) // а вот так да: List[int]([]int{1,2,3}).Filter(func(item int) bool {return item > 1})

не красивый повтор параметра типа…, немного усовершенствуем:

// сделали а-ля приватную структуру, которую снаружи в явном виде создать нелья // но в отличие от Kotlin можно ВОЗВРАЩАТЬ type _ListType[T any] []T // навесили на нее наш метод func (l _ListType[T]) Filter(condition func(item T) bool) _ListType[T] {...} // сделали "конструктор" func List[T](l []T) _ListType[T] { return _ListType[T](l) }  // теперь сработает автовывод типа mylist := List([]int{1,2,3}).Filter(...) // _ListType[int]

Заодно приведем вариант реализации этого Filter, вдруг она кому-то не очевидна

func (l _ListType[T]) Filter(condition func(item T) bool) _ListType[T] {     var result []T     for _, item := range l { // _ListType[T] все еще []T         if condition(item) {             result = append(result, item)         }     }     return result  // автоматический апкаст до _ListType[T] автоматически }

Окрыленные своим успехом, мы без проблем реализуем такие методы как Take, TakeLast, Drop, DropLast , First, FirstOrDefault…

Кстати а как сделать FirstOrDefault()?

И тут как это ни странно в Java/Kotlin, при всем богатстве рефлексии — это сложно, так как не очень понятно как именно в общем случае (не в частном, а общем) получить дефолтный экземпляр некоего типа T !!! Вот, что примерно бы было в Kotlin:

fun <T: Any> List<T>.firstOrDefault(): T {     if (this.size > 0) return this[0] // тут все просто, а вот дальше...     // все, приплыли } // немного переделаем fun <T: Any> List<T>.firstOrDefault(clazz : KClass<T> ): T {     if (this.size > 0) return this[0] // тут все просто, а вот дальше...     return clazz.createInstance()      // ну и мы понимаем, что это ни разу не общее решение и с кучей типов      // это не сработает как надо !!! } // добавим сахара inline fun <reified T:Any> List<T>.firstOrDefault(): T =      this.firstOrDefault(T::class)

В Golang это решается проще и можно запомнить идиому:

func (l _ListType[T]) FirstOrDefault() T {     if len(l) > 0 { return l[0] }     var def T // просто определяем переменную!      // и так как в GO все переменные инициализируются дефолтным значением,     // например 0, "", nil, пустая структура - то все, вуаля - можно возвращать     return def }

Зато сложнее получить в общем случае поведение DefaultOrNil(), которое в Kotlin несколько проще достигается… ну это уже совсем нюансы

Итак — второй «хак» — в Golang легко получить дефолт любого типа ,

просто определив переменную этого типа

И вот мы очень все еще окрылены нашим успехом переноса функциональщины, частично генериков и расширений и все идет как надо….

Более того все переносы они даже и канонов каких-то особых не нарушают и читаются легко.

Но тут мы резко и без предупреждения споткнемся о такой простой метод как List.map, напомню его код:

fun <T,R> List<T>.map(mapper : (T)->R): List<R> {     return buildList {       for (item in this) {           add(mapper(item))       }     } }

Пытаемся в лоб:

func (l _ListType[T]) Map[R any] (mapper func(src T) R) _ListType[R] {     var result []R     for _, item := range l {       result = append(result, mapper(item))     }     return result }

И тут мы упремся в короткое и лаконичное сообщение компилятора Golang:

syntax error: method must have no type parameters

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

И вот тут мы напарываемся на первую преграду действительно серьезную:

Шаблоны (генерики) в Golang намного слабее и не идут ни в какое сравнение по мощности и выразительности ни с Java/Kotlin ни тем более с Rust или с теми же шаблонами C++. Если ваше решение сильно завязано на генерики и они есть как у классов, так и у методов или расширений — скорее всего это та грань и та черта проекта, которая будет практически невозможно перенести на Golang без потерь в эргономике или семантике!!!

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

Соответственно мы можем реализовать Map , Fold, Reduce только в варианте с тем же типом, но не в обобщенной форме, то есть на вход List<T> и на выход List<T> или T, но не List<R>, R:

func (l _ListType[T]) Map (mapper func(item T) T) _ListType[T] {     var result []T     for _, item := range l {         result = append(result, mapper(item))     }     return result }

В таком виде естественно будет работать — но очевидно что это не тот Map о котором мы джва года уже мечтали…

Соответственно какие выводы можно сделать:

  • в целом нет сложности в переносе «функциональщины», «ламбд» и расширений, со своими нюансами, плюсами и минусами — но примерно понятно и комфортно

  • но если решение все построено на шаблонах, на косвенной типизации — то в Golang надо будет переработать саму модель использования этого кода или пойти на уйму компромиссов

И кстати частный случай компромисса, достаточно простой, в Kotlin мы имеем перегрузку методов по сигнатуре на одно и то же имя (как это было еще заведено в Pascal):

fun myFun(s: String) {...} fun myFun(i: Int) {...} ...

В golang так нельзя, потребуется

func MyFunS(s string) {} func MyFunI(i int) {}

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

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

func MyFun[T string|int] MyFun() {}

но тогда внутри придется делать switch по типу, в общем не факт, что это так уж хорошо

Свойства, конструкторы, инициализация

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

Возьмем какой-то такой код на Kotlin, несколько синтетический, но полный всяких фич, которые кажутся не поддерживаемыми на Golang

// интерфейс со свойством, в go нет свойств interface IMyInterface {    val x : Int    // в go нет никаких компаньонов или статических методов    // для интерфейсов, вообще статических нет методов   companion object {     fun createDefault() : IMyInterface = MyDefaultImpl()   } }  // явная типизация интерфейсом, а не утиная, в go нельзя явно указать // что структура держит интерфейс!  // у параметров не бывает дефолтов private class MyDefaultImpl (i: Int = 10) : IMyInterface {    // в структуре нельзя прямо прописать связанность полей   private val d: Double = i.toDouble()   // нет аналога init в golang   init {     require(d > -1.0) {"d должно быть больше -1.0"}   }   // ну какие в golang свойства и lazy   override val x by lazy { return (d * 2.13).toInt() } } 

Как ни странно — практически все из этого на Golang выполнимо практически без пересмотра семантики и даже без особых изменений в API

package my // или GetX() - ну да, свойств нет, но getter никто не отменял! type IMyInterface interface {   X() int    // тему "статических" методов и компаньонов оставим на конец } // все что не на большие буквы - private, точнее package private type _MyDefaultImpl struct {   d float64 // поле которое при создании заполняется   x int     // кэшированный lazy результат   lazy_x bool // признак, что lazy уже вызывался }  // определим дефолт, его будем потом уже при вызове использовать const _DEFAULT_I = 10 // приватный конструктор ну и раз в kotlin по ссылке все, // то и тут вренем по ссылке func newMyDefaultImpl(i int) *_MyDefaultImpl {   // и сейчас некоторый аналог init   d:=float64(i)   // некоторое воспроизведение require   if d < 1.0 {     panic("d должно быть больше -1.0")   }   // собственно вернули структуру   return &_MyDefaultImpl{d: d} } // ну и реализуем интерфейс func (d *_MyDefaultImpl) X() int {   // собственно lazy getter и можно в принципе и по синхронизации   // порешать через mutex, в данном примере особой нужды делать это не видел   if !d.lazy_x {     d.x = int(d * 2.13)     d.lazy_x = true   }   return d.x }   // тему статического факторизующего метода, можно сделать канонично для GO func IMyInterface_CreateDefault() IMyInterface {   // или я видел имена CreateDefaultIMyInterface, что более в каноне   return newMyDefaultImpl(_DEFAULT_I) // вот собственно наш дефолт }  ///////////////////////////////////////////////////////////////// // НА ПРАВАХ ХАКА - как все же заставить golang  // иметь "компаньоны"  // а можно даже исполнить синтаксически схоже c Kotlin // можно будет вызывать именно как IMyInterface_().createDefault() // напомню, что это `package private` - все что не с больших букв type _myInterfaceCompanion struct {} // пустая структура как псевдо тип func (_ _myInterfaceCompanion) CreateDefault() IMyInterface {   return newMyDefaultImpl(10) // вот собственно наш дефолт } func IMyInterface_() _myInterfaceCompanion { return _myInterfaceCompanion{}} // все, теперь снаружи можно так my:= IMyInterface_().CreateDefault() ///////////////////////////////////////////////////////////////  // а вот это НЕ УТИНАЯ типизация - явное требование компилятору // еще при сборке проверить, что *_MyDefaultImpl поддерживает IMyInterface var _ IMyInterface = &_MyDefaultImpl{}

Теперь в клиентском коде мы получаем поведение, подобное Kotlin классу

В kotlin:

import my val x : IMyInterface = IMyInterface.createDefault()
import "my"  var x my.IMyInterface = my.IMyInterface_().CreateDefault() //или  var x my.IMyInterface = my.IMyInterface_CreateDefault() 

Более того, можно довести совсем до Kotlin-стайла, что правда резко расходится с каноном:

... type _myInterfaceCompanion struct {} // пустая структура как псевдо тип func (_ _myInterfaceCompanion) CreateDefault() IMyInterface {   return newMyDefaultImpl(10) // вот собственно наш дефолт } // полный антипаттерн - глобальная переменная!  // но синтаксически можно var IMyInterface_  _myInterfaceCompanion = _myInterfaceCompanion{}

Ну и тогда вообще до смешения

// именно так, с точкой, по аналогии с импортами Java чтобы было поведение import . "my"  // все отличие от Kotlin только что подчеркивание вставили, иначе // коллизия имен var x IMyInterface = IMyInterface_.CreateDefault()

Итак резюмирую:

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

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

Тот случай, когда лучше не пытаться переносить подход Java/Kotlin в Golang и от этого все выиграют.

Может когда-то напишу про это, но я точно из лагеря тех, кто считает, что с появлением исключений и особенно их структурированной обработки (try/catch) и особенно с finally блоком в этой структурной обработке — эволюция пошла не туда. И меня нисколько не удивляет, что в новых языках типа Golang или Rust, есть паники, есть ошибки, паники могут тоже развертывать стек и так или иначе перехватывать (recovery в golang и catchUnwinded в Rust) — тем не менее там нет и близко try/catch/finally

Соответственно при переносах обработки ошибок можно действовать так

Перенос throw

Тут 2 ситуации — определитесь — это действительно «исключение», которое сигнализирует, что программа загнала себя в ситуацию, с которой не может справиться и может проще ее завершить чем продолжать работу. В этом случае эквивалентом будет panic

fun myFun() {     if (callSomething() == null) {        throw Exception("все у нас вообще null, такого не может быть!")           } } fun main() {     myFun() // никакой обработки try/catch }
funс MyFun() {     if (СallSomething() == nil) {        panic("все у нас вообще nil, такого не может быть!")           } } funс main() {     MyFun() // никакой обработки try/catch }

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

fun myFun() {     if (callSomething() == null) {        throw Exception("все у нас вообще null, такого не может быть!")           } } fun main() {    try{     myFun()    }catch(e: Throwable) { // тут у нас реакция с легким сайдэффектом и игнором      println(e.Message)    }  }

то лучше использовать внятное

func MyFun() error {   if (CallSomething() == null) {        return fmt.Errorf("все у нас вообще null, такого не может быть!")           }   return nil }  func main() {    err := MyFun()    if err != nil {      fmt.Println(e.Error())    } }

Главный совет. Обработка ошибок — это то, что в Java/Kotlin/C++ и еще много где исполнено на «исключениях» и их структурной обработке. Эта история собирает все больше критики и от нее все чаще отказываются (в новых языках). Golang не пригоден работать в модели и делать panic (a-ka throw) чтобы потом делать recover (a-ka catch) и где надо и не надо писать defer (a-ka finally) — это будет самый плохой пример попытки натянуть ежа на уже, хуже чем с приведенными выше «компаньонами».

Мало того, что если вы переносите в Golang — лучше чуть поработать и сделать в его модели. Если вы наоборот ИЗ Golang переносите в Kotlin лучше СОХРАНИТЬ эту модель обработки — благо в Kotlin из коробки есть Result<T> и в целом нет проблемы сделать обработку ошибок и без выбрасывания исключений.

Что лучше не пытаться переносить а лучше упростить

Как мы увидели выше — Golang в принципе позволяет работать близко к ООП, да и вообще не требует такой массы компромиссов c Java/Kotlin как например порты на С. Но есть вещи, которые идеологически отличают Golang и Kotlin — причем настолько, что при переносе из Kotlin в Golang мы практически гарантировано будем исключать некоторые вещи, как не поддерживаемые, а при переносе из Golang в Koltin наоброт переписывать или добавлять.

Главное что отличает Golang от Kotlin, прямо в их манифестах:

  • Kotlin — это про всеядность и сахар (нет единого стиля, расширения, делегаты, перегрузка операторов, инфикс функции, DSL, tail function arg, компаньоны, объекты, смарткасты, условия как выражения, no-return, ….)

  • Golang — это про унификацию и простоту (один формат, один вариант инструкции, решения в лоб, про все)

По факту это языки с диаметрально разной идеологией!

Соответственно из Kotlin в golang не переносятся вещи, которые заведомо сделаны для сахара и хитрых решений. И если обобщить — то это все, что связано с DSL и всякими «котлинскими штучками»

  • не будет никакого tailrec — если надо рубите «хвосты» сами

  • никакой перегрузки операторов — заменяйте просто функциями

  • никаких «псевдоблоков» новых в языке за счет функций последним параметров

  • никаких инфиксов

  • более менее можно играть в «делегаты» — lazy например был выше показан — но язык Вас в этом не поддержит, скорее всего вы не будете делегировать ради делегирования, а сделаете более в лоб (благо композиция как паттерн в golang как раз на высоте)

Рефлексия…

Чем меньше рефлексии тем лучше. Всюду. Точка.
P.S. Особенно в голанге

Если все решение у вас в Kotlin опирается на рефлексию, с учетом полиморфизма, с анализом метаданных свойств, методов и прочего — скорее всего вы это никак не перенесете. И в целом в go рефлексия это примитивные апкасты до интерфейса и не более того (точнее там есть еще всякий typeOf и прочее), но будем честны — рефлексия в Java и в Kotlin особенно — на порядок просто сложнее и многофункциональнее.

Если допустим вы пытаетесь (а у нас такая история есть) портировать какие-то валидаторы, расширенные сериализаторы, какие-то ORM-подобные штуки, которые как правило сильно опираются на аннотации, на рефлексию, — то скорее всего это обречено на провал.

В golang тоже есть свои аннотации (не объекты, а такие скорее строки структурированные), есть немного рефлексии — но ее Вам скорее не хватит.

Скорее всего придется все планировать с нуля или вообще альтернативно к вопросу подойти — упростить логику или на кодогенерации построить или еще как-то

Корутины vs Горутины

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

В целом имея уже некоторый опыт переноса я бы свел к двум вещам

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

  • В Kotlin корутины — с одной стороны часть языка (suspend) но это именно «приостановка» и асинхронщина в чистом своем виде, а корутины — это отдельная библиотека, надостройка, которая уже занимается парарлеллизмом и там собственно оркестрацией как таковой. В итоге все сложнее, многословнее, зато можно кучу всяких финтов делать

Так вот

  • Golang относительно легко переносится в Kotlin, только в Kotlin больше кода обвязки возникает

  • Из Kotlin же в Golang переносится только с серьезным упрощением и перепланированием

То есть если у вас просто GlobalScope.launch {... } и в лучшем случае потом join , то скорее всего вы просто это перепишите на go func() {...}() и там WaitGroup

Но если у вас собственные пулы потоков, связанные скоупы, донастроенные диспетчеры, ConflateChannel и тому подобное… то простите — простым и в лоб Ваш порт быть не может.

В таком случае точно придется пойти на какие-то упрощения, компромиссы и перепланирование.

Ссылки, значения, копии

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

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

Естественно, что для тех кто пишет на C/C++/Rust нет никакой сложности в том, чтобы понять что такое & и * и что такое передача по значению по указателю или по ссылке (и кстати в чем отличия указателей и ссылок)

Но для тех, кто привык что все по ссылке (или вообще об этом не задумаывается), а это в обещм и целом Java, Kotlin, C# (в общем случае), JavaScript, Python при начале работы на Go лучше к чему приучиться:

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

  • переводите на передачу по значению и на использование значений — только если четко понимаете зачем, почему

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

То есть вот так лучше не делать (если не уверены)

 type Foo stuct {    X int } type Bar struct {     F Foo     Y int } func NewFoo () Foo {...} func (b Bar) Do(f Foo) {...} 

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

type Foo stuct {    X int } type Bar struct {     F *Foo     Y int } func NewFoo () *Foo {...} func (b *Bar) Do(f *Foo) {...}

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

При этом сделать копию в golang ВООБЩЕ НЕ ПРОБЛЕМА

func I_Will_Return_Copy(foo *Foo) *Foo {      var cpy Foo = *foo // вот и все снятие копии      return &cpy // вернули от нее ссылку }

собственно никаких data class и не надо для go для copy, так как нет «классов», а структуры — это то, что спокойно копируется по памяти «из коробки»

Утиная типизация (интерфейсы)

Вторая многим непонятная концепция — утиная типизация при имплементации интерфейсов.

Ну тут не только джависты, но и почти все немного поначалу недоумевают… Такой концепции интерфейсов как в Golang почти нигде нет, только вот в Python и Golang. Но когда речь идет про Python — то там все легко это воспринимают «динамический же язык, что с него взять, понятно все там как-то налету кастуется» и в языке со строгой типизацией это тяжело воспринимается.

Кто не знает «утиная» это следующее: утка не знает, что она «утка», она вообще ничего может не знать ни о чем, но она при этом КРЯКАЕТ (func (u *Me) Kryack()), и ПЛАВАЕТ (func (u *Me) Swim()) — поэтому для орнитолога (вызывающая сторона) она УТКА (type IDuck interface { Kryack() ; Swim() }) — независимо от знаний утки. То есть нечто имеет интерфейс «утка» не потому что в ней это определено, а потому что кто-то решил что она соответствует… как -то так

Вот собственно и вся концепция. Владелец интерфейса — его клиент. Он определяет там, что должен уметь тип. Если какой-то тип соответствует — значит он реализует интерфейс. Сам тип при этом может ничего про существование интерфейса не знать.

Свежо, методично, модно, молодежно… но непривычно.

Для разработчиков на Java, особенно на Java EE или Spring мир выглядит совсем не так, а наоборот:

  • все нужные интерфейсы за тебя давно написаны — смотри Spring Reference (с) Bloody Enterprise

  • твоя задача их строго выполнить (c) Bloody Enterprise

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

И для начала введу вас обратно в зону комфорта, напомню, что на Go можно и не по-утиному а вполне и по-спринговому:

//   /enterprise/core/ifaces.go package ifaces type IRepository interface {    GetAll() []any } type IRefreshable interface {   IsObsolete() bool } --------------------------------------------- //  /myplugin/plugins.go package plugins import "enterprise/core/ifaces"  // это наш компонент type MyRefreshableRepositoryImpl struct {} // вот вполне пока утиная реализация - мы тут нигде не упоминаем  // интерфейсы и не можем быть уверены что все что хотели реализовали func (r *MyRefreshableRepositoryImpl) GetAll() []any {...} func (r *MyRefreshableRepositoryImpl) IsObsolete() bool {...}  // а вот это по сути "указание" компилятору провести  // на этапе компиляции, что мы соответствуем нужным интерфейсам // получается что-то вроде class MyRefreshableRepositoryImpl: IRepository, IRefreshable var _ ifaces.IRepository = &MyRefreshableRepositoryImpl{} var _ ifaces.IRefreshable = &MyRefreshableRepositoryImpl{} 

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

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

Тесты

Тут все просто. Если вы используете в своем решении на Kotlin стандартные тестовые фреймворки с базой на JUnit, — Kotest, Kotlintest, то скорее всего никаких сложностей переносить на Golang тесты у вас не будет.

Обратное тоже верно — тесты на testing переносятся без проблем в целевые фреймворки на JVM

Что нужно наверное учесть

  1. Практически всегда надо затаскивать в проект библиотеку https://pkg.go.dev/github.com/stretchr/testify/assert — это из тех пакетов, которым место в стандартном наборе пакетов, но исторически живет где-то отдельно

  2. Многие не смотрят что там внутри testing.T и очень обедняют свои тесты при переносе, а вообще-то в этой структуре есть метод Run который позволяет стартовать дочерние тест — соответственно вы можете спокойно обеспечить себе дизайн с пре- и пост- перехватчиками, с иерархией тестов, с порождением тестов или табличными тестами — все это ИЗ КОРОБКИ

  3. Многие не знают, что кроме тестов в Golang встроены и бенчмарки — поэтому если у вас использовались какие-то бенчмарки под JVM в тестах или какая-то кустарщина — в принципе тоже легко переносится

Заключение

Ну вот такая вышла статейка на нашу местную злобу дня.

Уверен, что многое еще чего можно написать на эту тему, но подкопилась пока именно такая подборка замечаний, зато по горячим так сказать следам….

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


ссылка на оригинал статьи https://habr.com/ru/post/714050/

Bittorrent с нуля на Go

BitTorrent — протокол загрузки и распространения файлов через Интернет. В отличие от традиционных отношений клиент/сервер, когда загрузчики подключаются к центральному серверу (например, для просмотра фильма на Netflix или загрузки веб-страницы), участники сети BitTorrent, называемые одноранговыми узлами, загружают фрагменты файлов друг с друга. Это то, что делает BitTorrent одноранговым протоколом. Исследуем, как он работает, и создадим собственный клиент, который сможет находить одноранговые узлы и обмениваться с ними данными.

диаграмма, показывающая разницу между отношениями клиент/сервер (все клиенты подключаются к одному серверу) и одноранговыми (одноранговые узлы подключаются друг к другу)

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

Я воспользуюсь Debian ISO, 350 МБ. Это популярный дистрибутив Linux, поэтому у нас будет множество быстрых и совместимых одноранговых узлов, к которым мы сможем подключиться. Получится также избежать юридических и этических проблем, связанных с загрузкой пиратского контента.

Поиск пиров

Вот проблема: мы хотим загрузить файл с помощью BitTorrent, но это одноранговый протокол, и мы понятия не имеем, где найти одноранговые узлы для его загрузки. Это очень похоже на переезд в новый город и попытку завести друзей — может быть, мы зайдём в местный паб или встретимся! Централизованные местоположения — главная идея трекеров, которые представляют собой центральные серверы, знакомящие одноранговые узлы друг с другом. Это просто веб-серверы, работающие по протоколу HTTP. Некоторые трекеры используют UDP — двоичный протокол для экономии полосы пропускания.

иллюстрация настольного компьютера и ноутбука, сидящих в пабе

Конечно, эти центральные серверы могут подвергнуться рейду федералов, если они способствуют обмену незаконным контентом. Возможно, вы читали о том, что такие трекеры, как TorrentSpy, Popcorn Time и KickassTorrents, закрыты. Новые методы устраняют посредников, превращая в распределённый процесс даже обнаружение одноранговых узлов. Мы не будем их внедрять, но если вам интересно, некоторые термины, которые вы можете изучить, — DHT, PEX и магнитные ссылки.

Разбор файла .torrent

Файл .torrent описывает содержимое файла, доступного для торрента, и информацию для подключения к трекеру. Это всё необходимое, чтобы запустить загрузку торрента. Торрент-файл Debian выглядит так:

d8:announce41:http://bttracker.debian.org:6969/announce7:comment35:"Debian CD from cdimage.debian.org"13:creation datei1573903810e9:httpseedsl145:https://cdimage.debian.org/cdimage/release/10.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-10.2.0-amd64-netinst.iso145:https://cdimage.debian.org/cdimage/archive/10.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-cd/debian-10.2.0-amd64-netinst.isoe4:infod6:lengthi351272960e4:name31:debian-10.2.0-amd64-netinst.iso12:piece lengthi262144e6:pieces26800:PS^ (binary blob of the hashes of each piece)ee

Этот беспорядок закодирован в формате Bencode (произносится bee-encode), и нам нужно декодировать его.

Bencode может кодировать примерно те же типы структур, что и JSON — строки, целые числа, списки и словари. Закодированные bencode данные не так удобны для чтения / записи человеком, как JSON, но они могут эффективно обрабатывать двоичные данные, и их действительно просто анализировать из потока. Строки поставляются с префиксом длины и выглядят как 4:spam. Целые числа располагаются между маркерами start и end, поэтому 7 кодируется как i7e. Списки и словари работают аналогичным образом: l4:spami7ee представляет ['spam', 7], тогда как d4:spami7ee означает {spam: 7}.

В формате покрасивее .torrent выглядит так:

d   8:announce     41:http://bttracker.debian.org:6969/announce   7:comment     35:"Debian CD from cdimage.debian.org"   13:creation date     i1573903810e   4:info     d       6:length         i351272960e       4:name         31:debian-10.2.0-amd64-netinst.iso       12:piece length         i262144e       6:pieces         26800:PS^ (binary blob of the hashes of each piece)

По этому файлу можно отследить URL трекера, дату создания (в виде временной метки Unix), имя и размер файла. Кроме этого, в файле есть большой бинарный блоб, в котором содержаться SHA-1 хэши каждой части файла. Эти части равны по размеру в пределах файла для скачивания. У разных торрентов размер части может быть разным, но обычно находится в пределах от 256 КБ до 1 МБ. Таким образом, крупный файл может состоять из тысяч частей. Эти части мы скачаем у пиров, проверим их по хэшам нашего торрент-файла и соберём их воедино. Всё. Файл готов!

изображение файла, разрезаемого ножницами на части, пронумерованные с piece 0

Такой механизм позволяет проверить отдельно целостность каждой части файла в ходе процесса. При этом, BitTorrent устойчив к случайному и намеренному повреждению торрент-файла (torrent poisoning). Если хакеру не удалось взломать SHA-1 при помощи атаки праобраза (preimage attack), мы получим ровно тот контент, который запросили.

Здорово было бы написать свой bencode-парсер, но статья посвящена не парсерам. Парсер на 50 строк кода от Фредерика Лунда кажется мне наиболее интересным. Для этого проекта я пользовался github.com/jackpal/bencode-go:

import (     "github.com/jackpal/bencode-go" )  type bencodeInfo struct {     Pieces      string `bencode:"pieces"`     PieceLength int    `bencode:"piece length"`     Length      int    `bencode:"length"`     Name        string `bencode:"name"` }  type bencodeTorrent struct {     Announce string      `bencode:"announce"`     Info     bencodeInfo `bencode:"info"` }  // Open parses a torrent file func Open(r io.Reader) (*bencodeTorrent, error) {     bto := bencodeTorrent{}     err := bencode.Unmarshal(r, &bto)     if err != nil {         return nil, err     }     return &bto, nil }

смотреть в контексте

Мне нравится делать свои структуры относительно плоскими и отделять структуры приложения от структур сериализации (serialization structs). Поэтому я применил другую, более плоскую структуру TorrentFile и написал несколько вспомогательных функций для их взаимопреобразования.

Обратите внимание, что pieces (изначально, это была строковая переменная) я разбил на хэш-слайсы (slice of hashes) по [20] байт на хэш. Это упросит доступ у отдельным хэшам. Кроме того, я рассчитал общий хэш SHA-1 всего кодированного в bencode словаря info (где содержатся имя, размер и хэш каждой части). Этот хэш известен нам как инфохэш (infohash), уникальный идентификатор файлов для передачи данных трекерам и пирам. Подробнее об этом поговорим позже.

именная табличка 'Hello my name is 86d4c80024a469be4c50bc5a102cf71780310074'

type TorrentFile struct {     Announce    string     InfoHash    [20]byte     PieceHashes [][20]byte     PieceLength int     Length      int     Name        string }

смотреть в контексте

Получение пиров через трекер

Теперь, имея информацию о файле и его трекере, давайте обратимся к трекеру для анонсирования нашего присутствия среди пиров и для получения списка других пиров. Для этого нужно составить запрос GET на URL announce в файле .torrent с несколькими параметрами запроса:

func (t *TorrentFile) buildTrackerURL(peerID [20]byte, port uint16) (string, error) {     base, err := url.Parse(t.Announce)     if err != nil {         return "", err     }     params := url.Values{         "info_hash":  []string{string(t.InfoHash[:])},         "peer_id":    []string{string(peerID[:])},         "port":       []string{strconv.Itoa(int(Port))},         "uploaded":   []string{"0"},         "downloaded": []string{"0"},         "compact":    []string{"1"},         "left":       []string{strconv.Itoa(t.Length)},     }     base.RawQuery = params.Encode()     return base.String(), nil }

смотреть в контексте

Важные элементы кода:

  • info_hash: идентифицирует файл, который мы хотим скачать. Это инфохэш (infohash), который мы рассчитывали ранее по кодированному bencode словарю info. Трекеру нужно знать этот хэш, чтобы показать нам именно тех пиров, которые нужны.
  • peer_id: имя на 20 байт, которое идентифицирует нас на трекерах и для пиров. Для этого мы просто берём 20 случайных байтов. Реальные клиенты BitTorrent имеют ID вида -TR2940-k8hj0wgej6ch, где закодированы программное обеспечение и его версия. В данном случае TR2940 означает Transmission client 2.94.

файл с именной табличной 'info_hash' и человечек с именной табличкой 'peer_id'

##Парсинг ответа трекера

Мы получаем ответ, закодированный bencode:

d   8:interval     i900e   5:peers     252:(another long binary blob) e

interval указывает как часто мы можем делать запрос на сервер для обновления списка пиров. Значение 900 означает, что мы можем переподключаться раз в 15 минут (900 секунд).

peers — большой бинарный блоб, где содержатся IP-адреса каждого пира. Он образован группами по 6 байтов. Первые 4 байта — это IP адрес пира, а каждый байт показывает числовое значение в IP. Последние 2 байта — порт, в кодировке big-endian это uint16. Big-endian или сетевой порядок (network order) предполагает, что группу байтов можно представлять в виде целочисленного значения, двигаясь по порядку слева направо. Например, байты 0x1A, 0xE1 кодируются в 0x1AE1 или 6881 в десятичном формате.*Интерпретация тех же байтов в порядке little-endian дала бы 0xE11A = 57626

схема интерпретации 192, 0, 2, 123, 0x1A, 0xE1 в виде 192.0.1.123:6881

// Peer encodes connection information for a peer type Peer struct {     IP   net.IP     Port uint16 }  // Unmarshal parses peer IP addresses and ports from a buffer func Unmarshal(peersBin []byte) ([]Peer, error) {     const peerSize = 6 // 4 for IP, 2 for port     numPeers := len(peersBin) / peerSize     if len(peersBin)%peerSize != 0 {         err := fmt.Errorf("Received malformed peers")         return nil, err     }     peers := make([]Peer, numPeers)     for i := 0; i < numPeers; i++ {         offset := i * peerSize         peers[i].IP = net.IP(peersBin[offset : offset+4])         peers[i].Port = binary.BigEndian.Uint16(peersBin[offset+4 : offset+6])     }     return peers, nil }

смотреть в контексте

Скачивание у пиров

Теперь, имея список пиров, пора подключиться к ним и скачивать файл частями! Для каждого пира мы хотим сделать следующее:

  1. Начало подключения к пиру по TCP. Это всё равно, что снять трубку и набрать номер.
  2. Двустороннее BitTorrent-рукопожатие. Это всё равно, что сказать «Алло» и услышать «Алло» в ответ.
  3. Обмен сообщениями (messages) для скачивания частей файла. «Мне, пожалуйста, 231-ю часть».

Начало подключения по TCP

conn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second) if err != nil {     return nil, err }

смотреть в контексте

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

##Хэндшейк

Мы только что подключились к пиру. Теперь нужно рукопожатие, чтобы убедиться, что этот пир:

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

Два общающихся компьютера. Один справивает: 'do you speak BitTorrent and have this file?', другой отвечает: 'I speak BitTorrent and have that file'

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

  1. Длина идентификатора протокола всегда должна быть равна 19 (0x13 в hex)
  2. Идентификатор протокола, который называется pstr, — это всегда строка BitTorrent protocol
  3. 8 **зарезервированных байтов, выставленных в 0. Некоторые из них переводятся в состояние 1 для указания поддержки отдельных расширений (extensions). Так как этого не произошло, мы оставляем им значение 0.
  4. Инфохэш (infohash), который мы рассчитали ранее для желаемого файла
  5. ID пира (Peer ID), которым мы идентифицировали себя

Собираем это всё вместе и получаем хэндшейк:

\x13BitTorrent protocol\x00\x00\x00\x00\x00\x00\x00\x00\x86\xd4\xc8\x00\x24\xa4\x69\xbe\x4c\x50\xbc\x5a\x10\x2c\xf7\x17\x80\x31\x00\x74-TR2940-k8hj0wgej6ch

Отправив хэндшейк пиру, мы ожидаем аналогичный хэндшейк в том же формате. Инфохэш, который мы получим в ответе, должен совпасть с нашим. Тогда будет понятно, что говорим об одном и том же файле. Если всё идёт по плану, едем дальше. Если нет, мы можем разъединиться. «Алло». «Чже ши шей Нин сян яо шеньме» «Ой, извините, ошибся номером».

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

// A Handshake is a special message that a peer uses to identify itself type Handshake struct {     Pstr     string     InfoHash [20]byte     PeerID   [20]byte }  // Serialize serializes the handshake to a buffer func (h *Handshake) Serialize() []byte {     buf := make([]byte, len(h.Pstr)+49)     buf[0] = byte(len(h.Pstr))     curr := 1     curr += copy(buf[curr:], h.Pstr)     curr += copy(buf[curr:], make([]byte, 8)) // 8 reserved bytes     curr += copy(buf[curr:], h.InfoHash[:])     curr += copy(buf[curr:], h.PeerID[:])     return buf }  // Read parses a handshake from a stream func Read(r io.Reader) (*Handshake, error) {     // Do Serialize(), but backwards     // ... }

смотреть в контексте

Отправка и получение сообщений

По завершении начального хэндшейка мы можем отправлять и принимать сообщения. Конечно, если другой пир не готов принимать сообщения, мы не сможем это делать, пока он не скажет, что готов. В этом состоянии мы, с точки зрения другого пира, «заглушены» (choked). Они должны отправить нам сообщение unchoke, сообщающее, что мы можем приступить к запросу данных. По умолчанию мы считаем, что всегда «заглушены», пока не убеждаемся в обратном.

Получив сообщение unchoked, мы можем приступать к отправке запросов (requests) на части файла, а они — присылать в ответ сообщения с такими частями.

Рисунок, где один стикмен говорит другому 'hello I would like piece number—', а второй душит его за шею и говорит '00 00 00 01 00 (choke)'

###Интерпретация сообщений

У сообщения есть длина, ID и полезная нагрузка (payload). Это выглядит так:

Сообщение имеет указатель длины 4 байта, 1 байт занимает ID, остальное — опциональная полезная нагрузка

Сообщение начинается с указателя длины. Он показывает, сколько байт составит длина сообщения. Это целочисленная переменная в 32 бита, то есть 4 байта в кодировке big-endian. Следующий байт, ID, сообщает, какой тип сообщения мы получили — например, байт 2 означает «interested». Оставшуюся часть сообщения занимает полезная нагрузка.

type messageID uint8  const (     MsgChoke         messageID = 0     MsgUnchoke       messageID = 1     MsgInterested    messageID = 2     MsgNotInterested messageID = 3     MsgHave          messageID = 4     MsgBitfield      messageID = 5     MsgRequest       messageID = 6     MsgPiece         messageID = 7     MsgCancel        messageID = 8 )  // Message stores ID and payload of a message type Message struct {     ID      messageID     Payload []byte }  // Serialize serializes a message into a buffer of the form // <length prefix><message ID><payload> // Interprets `nil` as a keep-alive message func (m *Message) Serialize() []byte {     if m == nil {         return make([]byte, 4)     }     length := uint32(len(m.Payload) + 1) // +1 for id     buf := make([]byte, 4+length)     binary.BigEndian.PutUint32(buf[0:4], length)     buf[4] = byte(m.ID)     copy(buf[5:], m.Payload)     return buf }

смотреть в контексте

Чтобы прочитать сообщение из потока (stream), мы просто следуем формату сообщения. Мы считываем 4 байта и интерпретируем их как uint32, чтобы узнать длину сообщения. Затем узнаём число байт, получаем ID (первый байт) и полезную нагрузку — оставшиеся байты.

// Read parses a message from a stream. Returns `nil` on keep-alive message func Read(r io.Reader) (*Message, error) {     lengthBuf := make([]byte, 4)     _, err := io.ReadFull(r, lengthBuf)     if err != nil {         return nil, err     }     length := binary.BigEndian.Uint32(lengthBuf)      // keep-alive message     if length == 0 {         return nil, nil     }      messageBuf := make([]byte, length)     _, err = io.ReadFull(r, messageBuf)     if err != nil {         return nil, err     }      m := Message{         ID:      messageID(messageBuf[0]),         Payload: messageBuf[1:],     }      return &m, nil }

смотреть в контексте

Битовые поля

Один из интереснейших типов сообщений — битовое поле (bitfield). Это структура данных, с помощью которой пиры эффективно кодируют те фрагменты, которые могут отправить. Битовое поле похоже на байтовый массив (byte array). Чтобы проверить, какие части уже скачались, нужно только посмотреть на положения битов, значение которых равно 1. Можете считать это цифровым эквивалентом карты лояльности какой-нибудь кофейни. Вначале карта пуста, и все биты равны 0. Мы меняем значения на 1, чтобы «проштамповать» все позиции на карте.

карта лояльности кофейни с отметками на 4 первых и предпоследней позиции, что соответствует двоичному коду 11110010

Благодаря работе с битами вместо байтов, эта структура данных намного компактнее. Мы можем закодировать информацию о 8 частях в одном байте — это размер типа bool [уточнить]. Но цена такого подхода — большая сложность. Самый маленький размер для адресации — байт. Поэтому для работы с битами нужны некоторые манипуляции:

// A Bitfield represents the pieces that a peer has type Bitfield []byte  // HasPiece tells if a bitfield has a particular index set func (bf Bitfield) HasPiece(index int) bool {     byteIndex := index / 8     offset := index % 8     return bf[byteIndex]>>(7-offset)&1 != 0 }  // SetPiece sets a bit in the bitfield func (bf Bitfield) SetPiece(index int) {     byteIndex := index / 8     offset := index % 8     bf[byteIndex] |= 1 << (7 - offset) }

смотреть в контексте

Собираем всё вместе

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

Управление одновременной работой: каналы в качестве очередей

В Go память память распределяется через коммуникацию. При этом канал Go можно считать малозатратной потокобезопасной очередью.

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

// Init queues for workers to retrieve work and send results workQueue := make(chan *pieceWork, len(t.PieceHashes)) results := make(chan *pieceResult) for index, hash := range t.PieceHashes {     length := t.calculatePieceSize(index)     workQueue <- &pieceWork{index, hash, length} }  // Start workers for _, peer := range t.Peers {     go t.startDownloadWorker(peer, workQueue, results) }  // Collect results into a buffer until full buf := make([]byte, t.Length) donePieces := 0 for donePieces < len(t.PieceHashes) {     res := <-results     begin, end := t.calculateBoundsForPiece(res.index)     copy(buf[begin:end], res.buf)     donePieces++ } close(workQueue)

смотреть в контексте

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

диаграмма стратегии скачивания

func (t *Torrent) startDownloadWorker(peer peers.Peer, workQueue chan *pieceWork, results chan *pieceResult) {     c, err := client.New(peer, t.PeerID, t.InfoHash)     if err != nil {         log.Printf("Could not handshake with %s. Disconnecting\n", peer.IP)         return     }     defer c.Conn.Close()     log.Printf("Completed handshake with %s\n", peer.IP)      c.SendUnchoke()     c.SendInterested()      for pw := range workQueue {         if !c.Bitfield.HasPiece(pw.index) {             workQueue <- pw // Put piece back on the queue             continue         }          // Download the piece         buf, err := attemptDownloadPiece(c, pw)         if err != nil {             log.Println("Exiting", err)             workQueue <- pw // Put piece back on the queue             return         }          err = checkIntegrity(pw, buf)         if err != nil {             log.Printf("Piece #%d failed integrity check\n", pw.index)             workQueue <- pw // Put piece back on the queue             continue         }          c.SendHave(pw.index)         results <- &pieceResult{pw.index, buf}     } }

смотреть в контексте

Управление состоянием пиров

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

type pieceProgress struct {     index      int     client     *client.Client     buf        []byte     downloaded int     requested  int     backlog    int }  func (state *pieceProgress) readMessage() error {     msg, err := state.client.Read() // this call blocks     switch msg.ID {     case message.MsgUnchoke:         state.client.Choked = false     case message.MsgChoke:         state.client.Choked = true     case message.MsgHave:         index, err := message.ParseHave(msg)         state.client.Bitfield.SetPiece(index)     case message.MsgPiece:         n, err := message.ParsePiece(state.index, state.buf, msg)         state.downloaded += n         state.backlog--     }     return nil }

смотреть в контексте

Пора делать запросы!

Файлами, частями и хэшами частей дело не кончится. Мы можем пойти дальше и разбить части на блоки. Блок — это фрагмент части. Его можно определить по индексу части, в которую он входит, байтовому смещению внутри части и длине. У пиров обычно запрашиваются блоки. Размер блока, как правило, 16 КБ, поэтому для получения части в 256 KB может понадобится 16 запросов.

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

Конвейерная обработка

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

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

Клиенты BitTorrent традиционно поддерживают очередь из 5 конвейерных запросов (pipelined requests), и я возьму то же значение. Как я увидел, увеличение этого значения может удвоить скорость скачивания. Более современные клиенты используют адаптивный размер очереди (adaptive queue size), что лучше соответствует скоростям и условиям современных сетей. С этим параметром, определённо, стоит поиграть. Это простой и очевидный способ оптимизации производительности программы.

// MaxBlockSize is the largest number of bytes a request can ask for const MaxBlockSize = 16384  // MaxBacklog is the number of unfulfilled requests a client can have in its pipeline const MaxBacklog = 5  func attemptDownloadPiece(c *client.Client, pw *pieceWork) ([]byte, error) {     state := pieceProgress{         index:  pw.index,         client: c,         buf:    make([]byte, pw.length),     }      // Setting a deadline helps get unresponsive peers unstuck.     // 30 seconds is more than enough time to download a 262 KB piece     c.Conn.SetDeadline(time.Now().Add(30 * time.Second))     defer c.Conn.SetDeadline(time.Time{}) // Disable the deadline      for state.downloaded < pw.length {         // If unchoked, send requests until we have enough unfulfilled requests         if !state.client.Choked {             for state.backlog < MaxBacklog && state.requested < pw.length {                 blockSize := MaxBlockSize                 // Last block might be shorter than the typical block                 if pw.length-state.requested < blockSize {                     blockSize = pw.length - state.requested                 }                  err := c.SendRequest(pw.index, state.requested, blockSize)                 if err != nil {                     return nil, err                 }                 state.backlog++                 state.requested += blockSize             }         }          err := state.readMessage()         if err != nil {             return nil, err         }     }      return state.buf, nil }

смотреть в контексте

###main.go

Это не займёт много времени. Мы почти закончили.

package main  import (     "log"     "os"      "github.com/veggiedefender/torrent-client/torrentfile" )  func main() {     inPath := os.Args[1]     outPath := os.Args[2]      tf, err := torrentfile.Open(inPath)     if err != nil {         log.Fatal(err)     }      err = tf.DownloadToFile(outPath)     if err != nil {         log.Fatal(err)     } }

смотреть в контексте

Это не всё

Для краткости я включил сюда лишь несколько основных фрагментов кода. Замечу, что весь связующий код (glue code), синтаксический анализ (parsing), юнит-тесты и другие скучные строительные элементы программы я опустил. Полную реализацию смотрите на Github.


ссылка на оригинал статьи https://habr.com/ru/company/skillfactory/blog/714044/

Home Assistant: Яндекс Алиса говорит, какие окна у вас не закрыты

О чем тут вообще?

В своей реализации умного дома я сделал скрипт, который называется “Я ухожу”. По задумке он вызывается перед уходом из дома, когда никого не остается. Скрипт выключает свет и разные устройства. Его можно активировать фразой “Алиса, я ухожу”.

Затем я приобрел контактный сенсор Zigbee и повесил его на окно. Мне захотелось, чтобы Алиса сообщала, в каких комнатах открыты окна. Все таки дома есть кот, а погодные условия бывают разными. Когда такой сенсор один, то никаких проблем не возникает: проверяем, что он в статусе “Открыто” и воспроизводим нужную фразу. Когда же таких сенсоров много, то хочется, чтобы Алиса говорила красивое предложение о том, в каких комнатах окна открыты.

Задумка

По моей задумке, если окно открыто лишь в одной комнате, то Алиса должна говорить, в какой именно комнате. Если в двух, то “там И там”. А если же в трех и более, то нужно через запятую сообщить — в каких комнатах окно не закрыто, а последнюю воспроизвести с приставкой И (там, там и там).

Итак, приступим к реализации.

Реализация

Для начала определим объект (в шаблонитизаторе Jinja2 это называется dictionary), в котором ключ — это кусочек фразы с названием комнаты, а значение — состояние окна в виде boolean.

{% set window_sensors = {   "в спальне": is_state("binary_sensor.bedroom_window_contact", "on"),   "на кухне":  is_state("binary_sensor.kitchen_window_contact", "on"),   "в гостиной":  is_state("binary_sensor.guestroom_window_contact", "on") } %}

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

{% set data = namespace(text=[]) %}

Ну а дальше просто пробегаемся по объекту и с помощью условия берем только те сенсоры, у которых состояние «Открыто»:

{% for k in window_sensors if window_sensors[k] == true %} {% if loop.first %}   {% set data.text = data.text + [["Кстати,", "Не забудьте, что", "Напоминаю, что"]|random] + [" у вас"] + [[" открыто", " не закрыто"]|random] + [" окно"] %} {% endif %}  {% if not loop.first and not loop.last %}   {% set data.text = data.text + [", "] + [k] %} {% elif loop.last and not loop.first %}   {% set data.text = data.text + [" и "] + [k] %} {% else %}   {% set data.text = data.text + [" "] + [k] %} {% endif %} {% endfor %}

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

В результате получается вот такой массив:

['Напоминаю, что', ' у вас', ' открыто', ' окно', ' ', 'в спальне', ' и ', 'в гостиной']

Собираем с помощью join в строку:

{{ data.text|join("") }}

Результат: «Напоминаю, что у вас открыто окно в спальне и в гостиной».

Готово! Остается только попросить Алису воспроизвести текст с помощью вызова службы media_player.play_media согласно документации AlexxIT/YandexStation.

Финальный код

service: media_player.play_media target:   entity_id: media_player.yandex_station_ # ID вашей станции data:   media_content_id: >-     {% set window_sensors = {       "в спальне": is_state("binary_sensor.bedroom_window_contact", "on"),       "на кухне":  is_state("binary_sensor.kitchen_window_contact", "on"),       "в гостиной":  is_state("binary_sensor.guestroom_window_contact", "on")     } %}          {% set data = namespace(text=[]) %}          {% for k in window_sensors if window_sensors[k] == true %}       {% if loop.first %}         {% set data.text = data.text + [["Кстати,", "Не забудьте, что", "Напоминаю, что"]|random] + [" у вас"] + [[" открыто", " не закрыто"]|random] + [" окно"] %}       {% endif %}            {% if not loop.first and not loop.last %}         {% set data.text = data.text + [", "] + [k] %}       {% elif loop.last and not loop.first %}         {% set data.text = data.text + [" и "] + [k] %}       {% else %}         {% set data.text = data.text + [" "] + [k] %}       {% endif %}     {% endfor %}          {{ data.text|join("") }}   media_content_type: text


ссылка на оригинал статьи https://habr.com/ru/post/714064/