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

Блог компании typeable

Перевод Восемь забавных вещей, которые могут с вами произойти, если у вас нет защиты от CSRF-атак

12.04.2021 18:13:34 | Автор: admin

Восемь забавных вещей, которые могут с вами произойти, если у вас нет защиты от CSRF-атак



Введение


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


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


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


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


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


Однако с помощью CSRF-уязвимостей умный злоумышленник может сделать очень многое. Вот восемь очевидных действий для разных ситуаций:


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


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


2. Сделать запрос и узнать его продолжительность


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


Однако никто серьезно не защищается от атак по времени, скажем, при поиске. Например, предположим, что у вас есть сайт службы знакомств, а злоумышленник делает запрос через браузер жертвы, скажем, Сары, по маршрутам messages/search?query=kevin%20mitchell и messages/search?query=blurghafest. Если достоверно известно, что первый запрос занимает больше времени, чем второй (причем необязательно намного), может пролиться кровь. Или, как минимум, где-то далеко произойдет неприятность и появится недовольный клиент.


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


3. Заставить пользователя выйти из системы


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


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


4. Заставить пользователя выйти из системы и снова войти


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


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


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


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


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


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


6. Превратить уязвимость Self-XSS в XSS


CSRF-атака может превратить уязвимость self-XSS, которая является мелкой проблемой, в XSS, которая является огромной проблемой.


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


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


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


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


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


7. Замести следы


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


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


8. Распространять вредоносное ПО


Позволяет ли ваш сервис пользователям хранить файлы? Многие типы файлов, такие как .docx, pdf, jpeg и другие исторически используются для сокрытия вредоносного ПО из-за багов программ, которые их читают.


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


Заключительные замечания


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

Подробнее..

Перевод Почему я считаю Haskell хорошим выбором с точки зрения безопасности ПО?

31.05.2021 18:12:13 | Автор: admin


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


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


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


   Чисто техническая            Уязвимость, относящаяся        уязвимость                исключительно к предметной области                                                                                           Инструментарий Инструментарий  Нужно     должен исправить может помочь  подумать

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


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


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


В таких сервисах обычно есть соблазн записать файл пользователя непосредственно в файловую систему сервера. Однако под каким именем файла? Использовать непосредственно имя файла пользователя верный путь к катастрофе, так как оно может выглядеть как ../../../etc/nginx/nginx.conf, ../../../etc/passwd/ или любые другие файлы, к которым сервер имеет доступ, но не должен их изменять.


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


Использование шкалы


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


В идеале современный инструментарий должен практически полностью устранять чисто технические уязвимости. Например, большинство современных языков, таких как Haskell, C# и Java, по большей части обеспечивают защиту содержимого памяти и в целом предотвращают переполнение буфера, попытки дважды освободить одну и ту же ячейку, а также другие технические проблемы. Однако от правильного инструментария можно получить еще больше пользы. Например, легко представить себе систему, в которой имеется техническая возможность разделить абсолютный и относительный пути к файлу, что упрощает контроль атак с обходом каталога (path traversal), таких как загрузка пользователем файла поверх какого-нибудь важного конфигурационного файла системы.


Haskell нижняя часть шкалы


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


// From imaginary CSRF token protection:if ($tokenHash == $hashFromInternet->{'tokenHash'}) {  echo "200 OK - Request accepted", PHP_EOL;}else { echo "403 DENIED - Bad CSRF token", PHP_EOL;};

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


Аналогичная проблема возникла с Java (и другим языками, см. https://frohoff.github.io/appseccali-marshalling-pickles/). Java предложил исключительно удобный способ сериализации любого объекта на диск и восстановления этого объекта в исходной форме. Единственной досадной проблемой стало отсутствие способа сказать, какой объект вы ждете! Это позволяет злоумышленникам пересылать вам объекты, которые после десериализации в вашей программе превращаются во вредоносный код, сеющий разрушения и крадущий данные.


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


data Request = Request {csrfToken :: Token, ... other fields}doSomething :: Session -> Request -> Handler ()doSomething session request  | csrfToken session == csrfToken request = ... do something  | otherwise = throwM BadCsrfTokenError

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


Haskell середина шкалы


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


Прежде всего, в Haskell имеется возможность моделировать данные более точно по сравнению с такими языками, как как C, Javascript или даже Java. В основном это обусловлено удобством его синтаксиса и наличием типов-сумм. Точное моделирование данных имеет значение для безопасности, поскольку код домена в основном представляет собой модель некоторого реального явления. Чем меньше ее точность, тем больше возможностей имеют злоумышленники.


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


data SSN = Unknown | Redacted | SSN Text

А теперь сравним моделирование той же идеи с использованием строковых величин "", "<REDACTED>" и "191091C211A". Что произойдет, если пользователь введет "<REDACTED>" в поле ввода SSN? Может ли это в дальнейшем привести к проблеме? В Haskell об этом можно не беспокоиться.


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


storeFileUpload :: Path Abs File -> ByteString -> IO ()storeFileUpload path = ...

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


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


Haskell и ошибки домена


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


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


Однако это все догадки. Сообщество Haskell до сих пор достаточно мало, чтобы не быть объектом атак, а специалисты по Haskell в общем случае еще не так сильно озабочены проблемами безопасности, как разработчики на Javascript или Python.


Заключение


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

Подробнее..

Сильные стороны функционального программирования

15.02.2021 16:13:26 | Автор: admin


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

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

Как ФП улучшает программирование


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

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

  1. Код станет более лаконичным и выразительным. Выразительность можно определить как количество идей на единицу кода, и в целом функциональные языки, будучи более высокоуровневыми, оказываются и более выразительными. Например, преобразование каждого элемента в массиве или списке реализуется функциональным однострочником (используя map/foreach/whatever и анонимную функцию), в то время как в императивном стиле пришлось бы организовывать цикл, объявлять переменную для счётчика или итератора и использовать явное присваивание. Для более сложных примеров различие в выразительности только усиливается.
  2. Декомпозиция кода будет происходить более естественно. Принцип Разделяй и властвуй уже прочно закрепился в разработке и является базовым принципом борьбы со сложностью ПО. Речь идёт не о способе построения алгоритмов, а о более общем понятии. Например, даже простую программу, которая сначала читает целиком текст, разбивает его на слова и что-то делает с каждым словом, можно разбить на логические части: само чтение, разбиение прочитанного текста на слова (например, по пробелам) и создание структуры для хранения слов, обход этой структуры с преобразованием слов и печать результата. Каждое из этих действий можно реализовать отдельно и достаточно абстрактно, чтобы затем переиспользовать для решения других подобных задач. Декомпозиция кода на более мелкие и более общие части делает его гораздо более понятным (в том числе и для самого автора кода в будущем), позволяет избежать ошибок копипаста и упрощает дальнейший рефакторинг. Думаю, многим разработчикам приходилось копаться в своей или чужой простыне неструктурированного кода, написанного наспех, чтобы скорее заработало. Человеческому мозгу тяжело удерживать внимание на большом количестве сущностей одновременно и решать одну глобальную задачу сразу (working memory), поэтому для нас вполне естественно разбивать задачи на более мелкие, решать их по отдельности и комбинировать результат. В функциональном программировании эти мелкие задачи выражаются как небольшие вспомогательные функции, каждая из которых делает своё дело и её работу можно описать одним коротким предложением. А построение итогового результата это композиция таких функций. Конечно, разбить код на отдельные переиспользуемые части можно и в ООП, и в чисто императивном низкоуровневом языке типа C, и для этого уже есть известные принципы типа SOLID и GoF-паттерны, но, когда сам язык заставляет программиста думать в терминах функций, декомпозиция кода происходит гораздо более естественно.

  3. Побочные эффекты будут отделены от чистых функций. Чистая функция это функция в математическом смысле, результат работы которой зависит только от входных данных. В ходе вычисления такой функции не происходит ничего лишнего: не меняются значения переменных, ничего не читается и не печатается, не пишется в БД и не выполняются запросы к внешним сервисам. Действительно, вы же не будете ожидать таких действий от, скажем, тригонометрических функций? С помощью чистых функций можно реализовать большую часть логики работы с данными. Не все языки позволяют контролировать отсутствие побочных эффектов и проверять чистоту функций, но сам по себе функциональный подход мотивирует использовать чистые функции, которые работают без неожиданностей.
  4. Код станет проще отлаживать и тестировать. Этот пункт вытекает из двух предыдущих: у нас имеется набор небольших функций, часть из которых чистые, т.е. мы знаем, что их результат зависит только от входных данных. Код становится удобнее отлаживать достаточно проверить, что возвращают используемые функции по отдельности, чтобы понять, как они будут работать вместе. Так же легко пишутся юнит-тесты для чистой логики вашего приложения.

Как ФП улучшает программиста





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

  1. Изучение альтернативной парадигмы само по себе полезно для мозга, поскольку в процессе освоения программирования в функциональном стиле вы научитесь смотреть на привычные вещи по-другому. Кому-то такой способ мышления покажется гораздо более естественным, чем императивный. Можно долго спорить о том, что нужно и не нужно в индустриальном программировании, но там в любом случае нужны хорошие мозги, а их нужно тренировать. Осваивайте то, что не используете в работе: Лиспы, Пролог, Haskell, Brainfuck, Piet. Это поможет расширить кругозор и может стать для вас увлекательной головоломкой. Со временем вы сможете начать применять более элегантные решения в функциональном стиле, даже если пишете на императивном языке.
  2. За функциональными языками стоит серьёзная теория, которая тоже может стать частью ваших увлечений или даже исследований, если вы в той или иной степени хотите связать свою жизнь с computer science. Учиться никогда не поздно, особенно когда перед вами будут наглядные примеры использования довольно занятной теории для решения повседневных задач. Я бы никогда не подумала, что уже после окончания университета буду смотреть лекции по теории категорий, которую мой мозг когда-то отказывался воспринимать, и решать задачки из курса ручкой на бумаге, просто потому что мне это интересно.
  3. Помимо расширения кругозора увлечение ФП поможет вам расширить и круг общения. Возможно, за тусовкой функциональщиков закрепилась репутация академических снобов, которые спрашивают у вас определение монады перед тем, как продолжить общение. Я тоже так раньше думала, пока меня не вытащили на первые функциональные митапы и конференции. Я была совсем неопытным джуном и не знала определение монады, но не встретила по отношению к себе никакого негатива. Напротив, я познакомилась с интересными людьми, увлечёнными своим делом и готовыми делиться опытом, рассказывать и объяснять. Разумеется, в любом комьюнити есть совершенно разные люди, кто-то вам понравится больше, кто-то покажется токсичным и отталкивающим, и это совершенно нормально. Гораздо важнее то, что у вас появится возможность обмениваться идеями с теми, кто смотрит на мир разработки немного иначе и обладает другим опытом.
  4. Самый неожиданный для меня пункт: мне было легче всего найти работу именно на Haskell! На данный момент мой опыт работы чуть больше пяти лет, за это время на двух из трёх местах работы я писала на Haskell, и это был наиболее комфортный и безболезненный опыт трудоустройства. Более того, начинала я тоже с позиции Haskell-разработчика, о чём ни разу не пожалела. На первой работе я получила базовые навыки клиент-серверной разработки и работы с БД. Мы занимались такими же приземлёнными и ненаучными задачами, как и компании, использующие более распространённые языки. На популярных сайтах с вакансиями вы, скорее всего, почти не найдёте ничего по запросу Haskell-разработчик. В лучшем случае найдутся вакансии, где указано, что знание альтернативных парадигм будет преимуществом. Однако, это не значит, что таких вакансий нет. В Твиттере и тематических каналах в Телеграме вакансии появляются регулярно. Да, их мало, нужно знать, где искать, но и хороших специалистов такого узкого профиля тоже немного. Разумеется, вас не возьмут сразу и везде, но свою востребованность вы почувствуете значительно сильнее, чем при поиске работы на более распространённых языках. Возможно, компании могут быть готовы вкладываться в развитие программистов в нужном направлении: не можешь найти хаскелиста вырасти его сам!

Заключение


Появление элементов ФП в популярных языках индустриальной разработки, таких как Python, C++, Kotlin, Swift и т.д., подтверждает, что этот подход действительно полезен и обладает сильными сторонами. Применение функционального стиля позволяет получить более надёжный код, который проще разбивать на части, обобщать и тестировать, независимо от языка программирования. Разумеется, функциональный язык позволяет использовать все перечисленные преимущества по максимуму, предоставляя естественные конструкции с высокой степенью выразительности.

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

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

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

24.02.2021 20:16:16 | Автор: admin

Введение


Всем привет! Меня зовут Никита, и мы в Typeable для разработки фронтенда для части проектов используем FRP-подход, а конкретно его реализацию на Haskell веб-фреймоворк reflex. На русскоязычных ресурсах отсутствуют какие-либо руководства по данному фреймворку (да и в англоязычном интернете их не так много), и мы решили это немного исправить.


В этой серии статей будет рассмотрено создание веб-приложения на Haskell с использованием платформы reflex-platform. reflex-platform предоставляет пакеты reflex и reflex-dom. Пакет reflex является реализацией Functional reactive programming (FRP) на языке Haskell. В библиотеке reflex-dom содержится большое число функций, классов и типов для работы с DOM. Эти пакеты разделены, т.к. FRP-подход можно использовать не только в веб-разработке. Разрабатывать мы будем приложение Todo List, которое позволяет выполнять различные манипуляции со списком задач.



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

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


  • Behavior a реактивная переменная, изменяющаяся во времени. Представляет собой некоторый контейнер, который на протяжении всего своего жизненного цикла содержит значение.
  • Event a событие в системе. Событие несет в себе информацию, которую можно получить только во время срабатывания события.

Пакет reflex предоставляет еще один новый тип:


  • Dynamic a является объединением Behavior a и Event a, т.е. это контейнер, который всегда содержит в себе некоторое значение, и, подобно событию, он умеет уведомлять о своем изменении, в отличие от Behavior a.

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


Подготовка


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


Чтобы ускорить процесс сборки, имеет смысл настроить кэш nix. В случае, если вы не используете NixOS, то вам нужно добавить следующие строки в файл /etc/nix/nix.conf:


binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.orgbinary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=binary-caches-parallel-connections = 40

Если используете NixOS, то в файл /etc/nixos/configuration.nix:


nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];

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


  • todo-client клиентская часть;
  • todo-server серверная часть;
  • todo-common содержит общие модули, которые используются сервером и клиентом (например типы API).

Далее необходимо подготовить окружение для разработки. Для этого надо повторить действия из документации:


  • Создать директорию приложения: todo-app;
  • Создать проекты todo-common (library), todo-server (executable), todo-client (executable) в todo-app;
  • Настроить сборку через nix (файл default.nix в директории todo-app);
    • Также надо не забыть включить опцию useWarp = true;;
  • Настроить сборку через cabal (файлы cabal.project и cabal-ghcjs.project).

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


{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {    owner = "reflex-frp";    repo = "reflex-platform";    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";    })}:(import reflex-platform {}).project ({ pkgs, ... }:{  useWarp = true;  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"];  };})

Примечание: в документации предлагается вручную склонировать репозиторий reflex-platform. В данном примере мы воспользовались средствами nix для получения платформы из репозитория.

Во время разработки клиента удобно пользоваться инструментом ghcid. Он автоматически обновляет и перезапускает приложение при изменении исходников.


Чтобы убедиться, что все работает, добавим в todo-client/src/Main.hs следующий код:


{-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidget $ el "h1" $ text "Hello, reflex!"

Вся разработка ведется из nix-shell, поэтому в самом начале необходимо войти в этот shell:


$ nix-shell . -A shells.ghc

Для запуска через ghcid требуется ввести следующую команду:


$ ghcid --command 'cabal new-repl todo-client' --test 'Main.main'

Если все работает, то по адресу localhost:3003 вы увидите приветствие Hello, reflex!



Почему 3003?


Номер порта ищется в переменной окружения JSADDLE_WARP_PORT. Если эта переменная не установлена, то по умолчанию берется значение 3003.


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


Вы можете заметить, мы использовали при сборке не GHCJS, а обычный GHC. Это возможно благодаря пакетам jsaddle и jsaddle-warp. Пакет jsaddle предоставляет интерфейс для JS для работы из-под GHC и GHCJS. С помощью пакета jsaddle-warp мы можем запустить сервер, который посредством веб-сокетов будет обновлять DOM и играть роль JS-движка. Как раз для этого и был установлен флаг useWarp = true;, иначе по умолчанию использовался бы пакет jsaddle-webkit2gtk, и при запуске мы бы увидели десктопное приложение. Стоит отметить, что еще существуют прослойки jsaddle-wkwebview (для iOS приложений) и jsaddle-clib (для Android приложений).


Простейшее приложение TODO


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


Добавим следующий код в todo-client/src/Main.hs.


{-# LANGUAGE MonoLocalBinds #-}{-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidgetWithHead headWidget rootWidgetheadWidget :: MonadWidget t m => m ()headWidget = blankrootWidget :: MonadWidget t m => m ()rootWidget = blank

Можно сказать, что функция mainWidgetWithHead представляет собой элемент <html> страницы. Она принимает два параметра head и body. Существуют еще функции mainWidget и mainWidgetWithCss. Первая функция принимает только виджет с элементом body. Вторая первым аргументом принимает стили, добавляемые в элемент style, и вторым аргументом элемент body.


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

Функция blank равносильна pure () и она ничего не делает, никак не изменяет DOM и никак не влияет на сеть событий.


Теперь опишем элемент <head> нашей страницы.


headWidget :: MonadWidget t m => m ()headWidget = do  elAttr "meta" ("charset" =: "utf-8") blank  elAttr "meta"    (  "name" =: "viewport"    <> "content" =: "width=device-width, initial-scale=1, shrink-to-fit=no" )    blank  elAttr "link"    (  "rel" =: "stylesheet"    <> "href" =: "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"    <> "integrity" =: "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"    <> "crossorigin" =: "anonymous")    blank  el "title" $ text "TODO App"

Данная функция сгенерирует следующее содержимое элемента head:


<meta charset="utf-8"><meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"><link crossorigin="anonymous" href="http://personeltest.ru/aways/stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"  integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" rel="stylesheet"><title>TODO App</title>

Класс MonadWidget позволяет строить или перестраивать DOM, а также определять сеть событий, которые происходят на странице.


Функция elAttr имеет следующий тип:


elAttr :: forall t m a. DomBuilder t m => Text -> Map Text Text -> m a -> m a

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


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


type MonadWidgetConstraints t m =  ( DomBuilder t m  , DomBuilderSpace m ~ GhcjsDomSpace  , MonadFix m  , MonadHold t m  , MonadSample t (Performable m)  , MonadReflexCreateTrigger t m  , PostBuild t m  , PerformEvent t m  , MonadIO m  , MonadIO (Performable m)#ifndef ghcjs_HOST_OS  , DOM.MonadJSM m  , DOM.MonadJSM (Performable m)#endif  , TriggerEvent t m  , HasJSContext m  , HasJSContext (Performable m)  , HasDocument m  , MonadRef m  , Ref m ~ Ref IO  , MonadRef (Performable m)  , Ref (Performable m) ~ Ref IO  )class MonadWidgetConstraints t m => MonadWidget t m

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


newtype Todo = Todo  { todoText :: Text }newTodo :: Text -> TodonewTodo todoText = Todo {..}

Тело будет иметь следующую структуру:


rootWidget :: MonadWidget t m => m ()rootWidget =  divClass "container" $ do    elClass "h2" "text-center mt-3" $ text "Todos"    newTodoEv <- newTodoForm    todosDyn <- foldDyn (:) [] newTodoEv    delimiter    todoListWidget todosDyn

Функция elClass на вход принимает название тэга, класс (классы) и содержимое. divClass это сокращенная версия elClass "div".


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


foldDyn :: (Reflex t, MonadHold t m, MonadFix m) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)

Она похожа на foldr :: (a -> b -> b) -> b -> [a] -> b и, по сути, выполняет такую же роль, только в роли списка здесь событие. Результирующее значение обернуто в контейнер Dynamic, т.к. оно будет обновляться после каждого события. Процесс обновления задаётся функцией-параметром, которая принимает на вход значение из возникшего события и текущее значение из Dynamic. На их основе формируется новое значение, которое будет находиться в Dynamic. Это обновление будет происходить каждый раз при возникновении события.


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


Функция newTodoForm строит ту часть DOM, в которой будет форма ввода описания задания, и возвращает событие, которое несет в себе новое Todo. Именно при возникновении этого события будет обновляться список заданий.


newTodoForm :: MonadWidget t m => m (Event t Todo)newTodoForm = rowWrapper $  el "form" $    divClass "input-group" $ do      iEl <- inputElement $ def        & initialAttributes .~          (  "type" =: "text"          <> "class" =: "form-control"          <> "placeholder" =: "Todo" )      let        newTodoDyn = newTodo <$> value iEl        btnAttr = "class" =: "btn btn-outline-secondary"          <> "type" =: "button"      (btnEl, _) <- divClass "input-group-append" $        elAttr' "button" btnAttr $ text "Add new entry"      pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Первое нововведение, которое мы встречаем тут, это функция inputElement. Ее название говорит само за себя, она добавляет элемент input. В качестве параметра она принимает тип InputElementConfig. Он имеет много полей, наследует несколько различный классов, но в данном примере нам наиболее интересно добавить нужные атрибуты этому тегу, и это можно сделать при помощи линзы initialAttributes. Функция value является методом класса HasValue и возвращает значение, которое находится в данном input. В случае типа InputElement оно имеет тип Dynamic t Text. Это значение будет обновляться при каждом изменении, происходящем в поле input.


Следующее изменение, которое тут можно заметить, это использование функции elAttr'. Отличие функций со штрихом от функций без штриха для построения DOM заключается в том, что эти функции вдобавок возвращают сам элемент страницы, с которым мы можем производить различные манипуляции. В нашем случае он необходим, чтобы мы могли получить событие нажатия на этот элемент. Для этого служит функция domEvent. Эта функция принимает название события, в нашем случае Click и сам элемент, с которым связано это событие. Функция имеет следующую сигнатуру:


domEvent :: EventName eventName -> target -> Event t (DomEventType target eventName)

Ее возвращаемый тип зависит от типа события и типа элемента. В нашем случае это ().


Следующая функция, которую мы встречаем tagPromptlyDyn. Она имеет следующий тип:


tagPromptlyDyn :: Reflex t => Dynamic t a -> Event t b -> Event t a

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


Тут следует сказать про то, что функции, которые содержат в своём названии слово promptly, потенциально опасные они могут вызывать циклы в сети событий. Внешне это будет выглядеть так, как будто приложение зависло. Вызов tagPromplyDyn valDyn btnEv, по возможности, надо заменять на tag (current valDyn) btnEv. Функция current получает Behavior из Dynamic. Эти вызовы не всегда взаимозаменяемые. Если обновление Dynamic и событие Event в tagPromplyDyn возникают в один момент, т.е. в одном фрейме, то выходное событие будет содержать те данные, которые получил Dynamic в этом фрейме. В случае, если мы будем использовать tag (current valDyn) btnEv, то выходное событие будет содержать те данные, которыми исходный current valDyn, т.е. Behavior, обладал в прошлом фрейме.


Здесь мы подошли к еще одному различию между Behavior и Dynamic: если Behavior и Dynamic получают обновление в одном фрейме, то Dynamic будет обновлен уже в этом фрейме, а Behavior приобретет новое значение в следующем. Другими словами, если событие произошло в момент времени t1 и в момент времени t2, то Dynamic будет обладать значением, которое принесло событие t1 в промежутке времени [t1, t2), а Behavior (t1, t2].


Задача функции todoListWidget заключается в выводе всего списка Todo.


todoListWidget :: MonadWidget t m => Dynamic t [Todo] -> m ()todoListWidget todosDyn = rowWrapper $  void $ simpleList todosDyn todoWidget

Здесь встречается функция simpleList. Она имеет следующую сигнатуру:


simpleList  :: (Adjustable t m, MonadHold t m, PostBuild t m, MonadFix m)  => Dynamic t [v]  -> (Dynamic t v -> m a)  -> m (Dynamic t [a])

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


todoWidget :: MonadWidget t m => Dynamic t Todo -> m ()todoWidget todoDyn =  divClass "d-flex border-bottom" $    divClass "p-2 flex-grow-1 my-auto" $      dynText $ todoText <$> todoDyn

Функция dynText отличается от функции text тем, что на вход принимает текст, обернутый в Dynamic. В случае, если элемент списка будет изменен, то это значение также обновится в DOM.


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


rowWrapper :: MonadWidget t m => m a -> m arowWrapper ma =  divClass "row justify-content-md-center" $    divClass "col-6" ma

Функция delimiter просто добавляет элемент-разделитель.


delimiter :: MonadWidget t m => m ()delimiter = rowWrapper $  divClass "border-top mt-3" blank


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


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

Подробнее..

Как мы выбираем языки программирования в Typeable

26.04.2021 18:04:48 | Автор: admin

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

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

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

  1. Baseline по скорости работы ПО.
  2. Особенности распространения и эксплуатации программы, например требование интерпретатора или возможность статической линковки.
  3. Экосистема библиотек и компонентов, которые можно переиспользовать. Отмечу что важно не только количество библиотек, но и качество релевантных для вас.
  4. Возможности параллельного/конкурентного/асинхронного исполнения программ, что может быть важным для многих систем.
  5. Сложность обучения людей выбранной технологии, что значительно влияет и на сообщество языка, и на перепрофилирование разработчиков.
  6. Выразительность языка.

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

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

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

  1. Статическая типизация должна поддерживаться языком программирования. Это позволяет сократить длительность для каждой итерации цикла изменения и валидации кода для разработчика. Также это позволяет существенно снизить количество багов, как со стороны функциональных требований, так и безопасности ПО.
  2. Алгебраические типы данных очень сложно переоценить влияние этой фичи, после того, как начинаешь ей пользоваться. Очень доступная и абсолютно необходимая при моделировании инвариантов вещь. Те же типы-суммы настолько незаменимы, что выбрать язык, в котором их нужно моделировать через другие конструкции, это ставить себе преграды уже на первом шаге.
  3. Гибкая возможности для поддержки и исполнения многопоточных программ. Языки с GIL (Global Interpreter Lock) сразу же не удовлетворяют этому требованию. Хочется иметь возможность хорошо утилизировать возможности железа и иметь достаточно высокоуровневые абстракции для работы.
  4. Достаточная экосистема библиотек, оцениваем субъективно их качество в том числе. Не считаем необходимым все подключать в виде библиотек, но наиболее базовые вещи, вроде биндингов к популярным базам данных, должны быть в наличии.
  5. Светлые головы в коммьюнити разработчиков на этом языке программирования. Профиль разработчика, которого бы мы хотели видеть своим сотрудником это заинтересованный в CS и разработке человек. В противопоставлении этому можно поставить статус легких в освоении технологий, которые привлекают людей в IT ради легкой наживы, что сильно размывает рынок спецов.
  6. В нашем распоряжении должны быть языки программирования, которые позволяют реализовывать ПО с жесткими требованиями ко времени обработки и потребления памяти.

В результате всего вышесказанного в нашем ящичке с инструментами есть достаточно блоков, которые позволяют нам занять уверенную позицию во многих проектах. Возвращаясь к шахматной аналогии, это наши принципы, которые позволяют вести позиционную игру. Позиционная игра игра направленная на создание долгосрочной позиции, открывающей игроку возможности и минимизирующей слабости. Она противопоставляется игре атакующей, острой, где есть большая доля риска, а атакующий игрок пытается эту игру завершить до того, как его противник сможет занять хорошую оборонительную позицию. Острая разработка это олимпиадное программирование, MVP для маркетинговых экспериментов, многие задачи в data science, да и зачастую создание ПО, сопровождающее Computer Science публикации. Их объединяет то, что долгой поддержки они зачастую не требуют, им только нужно отработать в определенный момент времени. Позиционная же игра это игра вдолгую, где ключевыми показателями являются поддерживаемость и обновляемость. Именно этим мы и занимаемся, и нам нужен хороший фундамент, для того чтобы быть уверенными в долгосрочной работе ПО, которое мы пишем и обновляем. Такие проекты тоже могут начинаться с MVP, но они делаются с совсем другими предпосылками.

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

Во-вторых, мы ищем реализацию наибольшего количества взятых нами принципов в языках программирования и компиляторах. По этой причине вдобавок к нашему любимому Haskell, в нашем ящичке инструментов стал появляться Rust. С real-time требованиями и жесткими ограничениями на использование памяти нам нужно что-то достаточно низкоуровневое. Строгость типизации в C все же оставляет желать лучшего, поэтому если есть возможность использовать Rust для такой задачи, мы предпочтем это сделать.

Третья причина: мы делаем ПО в первую очередь для наших клиентов, и мы бы хотели, чтобы они были защищены от наших предрассудков. Поэтому при выборе инструмента риск не может превышать некий уровень, который должен быть оговорен с клиентом. Но даже с такими условиями у нас появились довольно маргинальные технологии вроде GHCJS, т.к. при совокупном разборе сильных и слабых сторон картинка все равно была привлекательной для нас и наших клиентов. Про то, как мы пришли к этому решению, мы уже писали: Elm vs Reflex.

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

Nix воспроизводимая сборка

11.05.2021 20:09:12 | Автор: admin


Привет, Хаброюзеры!


Сегодня мы продолжим наш цикл статей о Nix и как мы в Typeable его используем.


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


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


Весь код для этой статьи можно найти в репозитарии на Github.


Проблема


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


Наше приложение


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


Наш Main.hs:


module Main wheremain :: IO ()main = putStrLn "Hello, World!"

Для сборки проекта без Nix мы используем утилиту stack (подробнее с ней можно ознакомиться здесь). В качестве описания проекта для stack требуется файл stack.yaml, содержащий список наших пакетов и resolver. Последнее это стабильный срез Hackage, базы пакетов для языка Haskell, в котором гарантируется, что все пакеты собираются и дружат друг с другом (NB подобных срезов крайне не хватает в других языках ): ).


stack.yaml:


resolver: lts-17.11packages:- hello-world

Рецепт сборки конкретного пакета находится в hello-world.cabal:


cabal-version:      2.4name:               hello-worldversion:            1.0synopsis:           Hello Worldlicense:            MITlicense-file:       LICENSEauthor:             Nickexecutable hello-world    main-is:          Main.hs    build-depends:    base >= 4 && < 5    hs-source-dirs:   src    default-language: Haskell2010    ghc-options:      -Wall -O2

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


$ stack run hello-worldHello, World!

Come to the dar^Wnix side, we have cookies!


Сам по себе stack отличное средство для сборки проектов на Haskell, но в нём не хватает многих возможностей. Для сборки программ на Haskell для Nix есть библиотека haskell.nix, разработанная компанией IOHK. Её-то мы и будем здесь использовать. Для начала, сделаем так, чтобы наш проект собирался с помощью Nix.


Haskell.nix позволяет нам в несколько строчек преобразовать всю информацию о сборке нашего проекта из .cabal-файлов и stack.yaml в derivation для Nix.


nix/stackyaml.nix:


{  # Импортируем последнюю версию haskell.nix с GitHub и инициализируем Nixpkgs с её использованием.  haskellNix ? import (builtins.fetchTarball "https://github.com/input-output-hk/haskell.nix/archive/b0d03596f974131ab64d718b98e16f0be052d852.tar.gz") {}  # Здесь мы используем последнюю стабильную версию Nixpkgs. Версия 21.05 скоро выйдет :), nixpkgsSrc ? haskellNix.sources.nixpkgs-2009, nixpkgsArgs ? haskellNix.nixpkgsArgs, pkgs ? import nixpkgsSrc nixpkgsArgs}:let  # Создаём проект на базе stack. Для проектов Cabal есть функция cabalProject.  project = pkgs.haskell-nix.stackProject {    name = "hello-world";    # Derivation с исходным кодом проекта.    # Функция cleanGit копирует для сборки проекта только файлы, присутствующие в нашем git-репозитарии.    src = pkgs.haskell-nix.haskellLib.cleanGit {      name = "hello-world";      # Параметр src должен указывать на корневую директорию, содержащую stack.yaml.      src = ../.;      # keepGitDir оставляет директорию .git при сборке.      # Это может быть полезно, например, чтобы вставить хэш коммита в код.      keepGitDir = true;    };    # В параметре modules можно указать параметры сборки как для всех модулей сразу, так и для каждого в отдельности.    modules = [{      # doCheck отвечает за запуск юнит-тестов при сборке проекта, в том числе содержащихся во всех зависимостях.      # Здесь мы этого хотим избежать, поэтому этот параметр лучше всего ставить false и включить только для нужных      # пакетов.      doCheck = false;      # Добавим для нашего Hello World флаг -Werror.      packages.hello-world.components.exes.hello-world.ghcOptions = [ "-Werror" ];    }];  };# Наружу из этого файла мы выставляем project -- наш проект, а также pkgs -- срез nixpkgs, который мы будем использовать дальше.in { inherit project; inherit pkgs; }

Давайте проверим, что наш проект теперь можно собрать через Nix. Для этого достаточно команды nix build. Как и всегда, в текущей директории будет создана символическая ссылка result, содержащая результаты сборки.


$ nix build project.hello-world.components.exes$ ./result/bin/hello-worldHello, World!

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


Dockerfile? Какой Dockerfile?


Сейчас 2021 год, и очень многие компании используют Docker для деплоя и запуска сервисов. Typeable здесь не будет исключением. В составе nixpkgs есть весьма удобный инструментарий для сборки контейнеров под названием dockerTools. Более подробно с его возможностями можно ознакомиться по ссылке, я лишь покажу, как мы с его помощью упаковываем наш код в контейнеры. Полностью код можно посмотреть в файле nix/docker.nix.


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


sourceImage = dockerTools.pullImage {  imageName = "centos";  imageDigest = "sha256:e4ca2ed0202e76be184e75fb26d14bf974193579039d5573fb2348664deef76e";  sha256 = "1j6nplfs6999qmbhjkaxwjgdij7yf31y991sna7x4cxzf77k74v3";  finalImageTag = "7";  finalImageName = "centos";};

Здесь всё очевидно для всех, кто когда-либо работал с Docker. Мы говорим Nix, какой образ из публичного Docker Registry мы хотим использовать и что дальше мы будем на него ссылаться как на sourceImage.


Для сборки самого образа в dockerTools есть функция buildImage. У неё довольно много параметров, и часто проще написать свою обёртку над ней, что мы и сделаем:


makeDockerImage = name: revision: packages: entryPoint:  dockerTools.buildImage {    name = name;    tag = revision;    fromImage = sourceImage;    contents = (with pkgs; [ bashInteractive coreutils htop strace vim ]) ++ packages;    config.Cmd = entryPoint;  };

Наша функция makeDockerImage принимает четыре параметра: имя контейнера, его версия (в Typeable мы обычно используем хэш коммита из git в качестве тега), пакеты, которые мы хотим включить, и точку входа при запуске контейнера. Внутри же мы ссылаемся на образ с CentOS как основу (fromImage), плюс добавляем всякие утилиты, крайне полезные при экстренных случаях.


И, наконец, создадим образ с нашим великолепным приложением.


hello-world = project.hello-world.components.exes.hello-world;helloImage = makeDockerImage "hello"   (if imageTag == null then "undefined" else imageTag)  [ hello-world ]  [ "${hello-world}/bin/hello-world"  ];

Для начала мы создадим алиас для нужного нам пакета, чтобы не писать project.hello-world... повсюду. Дальше, вызвав написанную ранее функцию makeDockerImage, мы создаём образ контейнера с пакетом hello-world. В качестве тэга будет указан параметр imageTag, передаваемый снаружи, либо "undefined" если ничего не передано.


Проверим сборку:


$ nix build --argstr imageTag 1.0 helloImage[4 built, 0.0 MiB DL] $ ls -l resultlrwxrwxrwx 1 user users 69 May 11 13:12 result -> /nix/store/56qqhiwahyi46g6mf355fjr1g6mcab0b-docker-image-hello.tar.gz

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


$ docker load < result 76241b8b0c76: Loading layer [==================================================>]  285.9MB/285.9MBLoaded image: hello:1.0$ docker run hello:1.0Hello, World!

Заключение


В итоге, с помощью сравнительно небольшого количества кода, у нас получилось сделать воспроизводимую сборку нашего проекта на Haskell. Точно так же, заменив haskell.nix на что-то другое, можно поступить с проектами на других языках: в nixpkgs есть встроенные средства для C/C++, Python, Node и других популярных языков.


В следующей статье цикла я расскажу о частых проблемах, которые возникают при работе с Nix. Stay tuned!

Подробнее..

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

17.05.2021 18:06:47 | Автор: admin

Часть 1.


Часть 2.


Всем привет! В этой части мы рассмотрим использование класса EventWriter и библиотеки ghcjs-dom.



Использование EventWriter


Сейчас, для того, чтобы прокинуть события с более глубоких уровней, мы передаем их в качестве возвращемых значений. Это не всегда удобно, особенно, когда надо возвращать что-то, помимо события (например, форма ввода может возвращать одновременно и событие нажатия кнопки, и данные из формы). Гораздо удобнее было бы использовать механизм, который может "прокинуть" события наверх автоматически, не задумываясь о том, что надо их постоянно возвращать. И такой механизм есть EventWriter. Этот класс позволяет записывать события, наподобие стандартной монады Writer. Перепишем наше приложение с использованием EventWriter.


Для начала рассмотрим сам класс EventWriter:


class (Monad m, Semigroup w) => EventWriter t w m | m -> t w where  tellEvent :: Event t w -> m ()

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


Существует трансформер, являющийся экземпляром этого класса EventWriterT, для его запуска используется функция runEventWriterT.


Далее переходим к изменению функций. Наибольшие изменения ожидают функцию rootWidget.


rootWidget :: MonadWidget t m => m ()rootWidget =  divClass "container" $ mdo    elClass "h2" "text-center mt-3" $ text "Todos"    (_, ev) <- runEventWriterT $ do      todosDyn <- foldDyn appEndo mempty ev      newTodoForm      delimiter      todoListWidget todosDyn    blank

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


Изменения в newTodoForm не такие большие, но все же, стоит их отметить:


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)  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

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


Функция todoListWidget сильно упростилась.


todoListWidget  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Dynamic t Todos -> m ()todoListWidget todosDyn = rowWrapper $  void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget

Нас теперь вообще не интересует возвращаемое событие, и, соответственно, отпала необходимость в извлечении Event из Dynamic.


В функции todoWidget также произошли заметные изменения. Больше нет необходимости работать с возвращаемым типом преобразовывать Event t (Event t TodoEvent). Отличие функции dyn_ от функции dyn, в том, что она игнорирует возвращаемое значение.


todoWidget  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Dynamic t Todo -> m ()todoWidget ix todoDyn' = do  todoDyn <- holdUniqDyn todoDyn'  dyn_ $ ffor todoDyn $ \td@Todo{..} -> case todoState of    TodoDone         -> todoDone ix todoText    TodoActive False -> todoActive ix todoText    TodoActive True  -> todoEditable ix todoText

Единственное изменение в функциях todoDone, todoActive и todoEditable это новый тип и запись события вместо его возврата.


todoActive  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoActive ix todoText = divClass "d-flex border-bottom" $ do  divClass "p-2 flex-grow-1 my-auto" $    text todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Done"    (editEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Edit"    (delEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Drop"    tellEvent $ Endo <$> leftmost      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl      , update (Just . startEdit) ix  <$ domEvent Click editEl      , delete ix <$ domEvent Click delEl      ]todoDone  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoDone ix todoText = divClass "d-flex border-bottom" $ do  divClass "p-2 flex-grow-1 my-auto" $    el "del" $ text todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Undo"    (delEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Drop"    tellEvent $ Endo <$> leftmost      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl      , delete ix <$ domEvent Click delEl      ]todoEditable  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoEditable ix todoText = divClass "d-flex border-bottom" $ do  updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $    editTodoForm todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Finish edit"    let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix    tellEvent $      tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)

Применение класса EventWriter упростило код и сделало его более читаемым.


ghcjs-dom


reflex позволяет нам только модифицировать DOM, но зачастую от JS-приложений требуется больше. Например, если требуется копировать текст по нажатию на кнопку, то reflex не предоставляет нужных нам для этого средств. На помощь приходит библиотека ghcjs-dom. По сути, это реализация JS API на Haskell. В ней можно найти все те же самые типы и функции, которые есть в JS.


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


function toClipboard(txt){  var inpEl = document.createElement("textarea");  document.body.appendChild(inpEl);  inpEl.value = txt  inpEl.focus();  inpEl.select();  document.execCommand('copy');  document.body.removeChild(inpEl);}

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


{-# LANGUAGE OverloadedStrings #-}{-# LANGUAGE MonoLocalBinds #-}module GHCJS whereimport Control.Monadimport Data.Functor (($>))import Data.Text (Text)import GHCJS.DOMimport GHCJS.DOM.Document  (createElement, execCommand, getBodyUnchecked)import GHCJS.DOM.Element as Element hiding (scroll)import GHCJS.DOM.HTMLElement as HE (focus)import GHCJS.DOM.HTMLInputElement as HIE (select, setValue)import GHCJS.DOM.Node (appendChild, removeChild)import GHCJS.DOM.Types hiding (Event, Text)import Reflex.Dom as RtoClipboard :: MonadJSM m => Text -> m ()toClipboard txt = do  doc <- currentDocumentUnchecked  body <- getBodyUnchecked doc  inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc    ("textarea" :: Text)  void $ appendChild body inpEl  HE.focus inpEl  HIE.setValue inpEl txt  HIE.select inpEl  void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text)  void $ removeChild body inpEl

Почти каждой строке из haskell функции toClipboard есть соответствие из JS функции. Стоит отметить, что здесь нет привычного класса MonadWidget, а используется MonadJSM это та монада, в которой производятся вся работы с помощью ghcjs-dom. Класс MonadWidget наследует класс MonadJSM. Рассмотрим, как осуществляется привязка обработчика к событию:


copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()copyByEvent txt ev =  void $ performEvent $ ev $> toClipboard txt

Здесь мы видим новую для нас функцию performEvent, и с помощью нее осуществляется привязка обработчика к событию. Она является методом класса PerformEvent:


class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where  type Performable m :: * -> *  performEvent :: Event t (Performable m a) -> m (Event t a)  performEvent_ :: Event t (Performable m ()) -> m ()

Теперь изменим виджет невыполненного задания, предварительно не забыв добавить импорт import GHCJS:


todoActive  :: (EventWriter t TodoEvent m, MonadWidget t m) => Int -> Todo -> m ()todoActive ix Todo{..} =  divClass "d-flex border-bottom" $ do    divClass "p-2 flex-grow-1 my-auto" $      text todoText    divClass "p-2 btn-group" $ do      (copyEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Copy"      (doneEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Done"      (editEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Edit"      (delEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Drop"      copyByEvent todoText $ domEvent Click copyEl      tellEvent $ leftmost        [ ToggleTodo ix <$ domEvent Click doneEl        , StartEditTodo ix <$ domEvent Click editEl        , DeleteTodo ix <$ domEvent Click delEl        ]

Была добавлена новая кнопка Copy и вызов определенной функции copyByEvent. Эти же самые действия можно проделать с виджетами для других состояний задания.


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


В следующей части рассмотрим использование JSFFI (JS Foreign Function Interface).

Подробнее..

Создаем веб-приложение на 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.

Подробнее..

Как мы строили параллельные вселенные для нашего (и вашего) CICD пайплайна в Octopod

08.02.2021 18:21:29 | Автор: admin

Как мы строили параллельные вселенные для нашего (и вашего) CI/CD пайплайна в Octopod



Привет, Хабр! Меня зовут Денис и я вам расскажу как нас надоумило сделать техническое решение для оптимизации процесса разработки и QA у себя в Typeable. Началось с общего ощущения, что вроде делаем все правильно, но все равно можно было бы двигаться быстрее и эффективнее принимать новые задачки, тестировать, меньше синхронизироваться. Это все нас привело к дискуссиям и экспериментам, результатом которых стало решение, которое я опишу ниже.

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

  • Production основное рабочее окружение, куда попадают пользователи системы.
  • Pre-Production окружение для тестирования релиз-кандидатов (версий, которые будут использованы в production, если пройдут все этапы тестирования; их также называют RC), максимально схожее с production, где используются production доступы для интеграции с внешними сервисами. Цель тестирования на Pre-production получить достаточную уверенность в том, что на Production проблем не будет.
  • Staging окружение для черновой проверки, как правило тестирование последних изменений, по возможности использует тестовые интеграции со сторонними системами, может отличаться от Production, используется для проверки правильности реализации новых функциональностей.

Что и как нам хотелось улучшить


C Pre-production все достаточно понятно: туда последовательно попадают релиз-кандидаты, история релизов такая же, как на Production. Со Staging же есть нюансы:

  1. ОРГАНИЗАЦИОННЕ. Тестирование критических частей может потребовать задержки публикации новых изменений; изменения могут взаимодействовать непредсказуемым образом; отслеживание ошибок становится трудным из-за большого количества активности на сервере; иногда возникает путаница, что в какой версии реализовано; бывает непонятно, какое из накопившихся изменений вызвало проблему.
  2. СЛОЖНОСТЬ УПРАВЛЕНИЯ ОКРУЖЕНИЯМИ. Нужны разные окружения для тестирования разных изменений: для одной внешней системы может потребоваться production доступ, а работать с другой нужно в тестовой среде вместо боевой. А если staging один, то эти настройки распространяются на все реализованные фичи до следующего деплоймента. Приходится всё время об этом помнить и предостерегать сотрудников. Ситуация в целом похожа на работу многопоточного приложения с единым разделяемым ресурсом: он блокируется одними потребителями, остальные ждут. Например, один QA инженер ждет возможности проверки платежного шлюза с production интеграцией, пока другой проверяет все на интеграции тестовой.
  3. ДЕФЕКТ. Критичный дефект может заблокировать тестирование всех новых изменений разом

  4. ИЗМЕНЕНИЯ СХЕМ БД. Тяжело управлять изменениями схемы баз данных на одном стенде в периоды её активной разработки. Откати туда-сюда. Упс, тут не ревертится. А тут отревертили, но данные тестировщиков потеряли. Хочется для тестирования разных функциональностей иметь разные, изолированные друг от друга базы.
  5. УВЕЛИЧЕННОЕ ВРЕМЯ ПРИЁМКИ. Из-за того, что комбинация прошлых пунктов иногда приводит к ситуациям, когда часть фичей становится недоступна тестировщикам вовремя, или настройки окружения не позволяют приступить к тестированию сразу, происходят задержки на стороне разработки и тестирования. Допустим, в фиче обнаруживается дефект, и она возвращается на доработку разработчику, который уже вовсю занят другой задачей. Ему было бы удобнее получить её на доработку скорее, пока контекст задачи не потерян. Это приводит к увеличению отрезка времени от разработки до релиза в production для этих фич, что увеличивает так называемые time-to-production и time-to-market метрики.

Каждый из этих пунктов так или иначе решается, но все это привело к вопросу, а получится ли упростить себе жизнь, если мы уйдем от концепции одного staging-стенда к их динамическому количеству. Аналогично тому, как у нас есть проверки на CI для каждой ветки в git, мы можем получить и стенды для проверки QA для каждой ветки. Единственное, что нас останавливает от такого хода недостаток инфраструктуры и инструментов. Грубо говоря, для принятия фичи создается отдельный staging с выделенным доменным именем, QA тестирует его, принимает или возвращает на доработку. Примерно вот так:


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


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

Наш путь к решению


Первое, что мы попробовали это прототип, собранный нашим DevOps: комбинация из docker-compose (оркестрация), rundeck (менеджмент) и portainer (интроспекция), которая позволила протестировать общее направление мысли и подход. С удобством были проблемы:

  1. Для любого изменения требовался доступ к коду и rundeck, которые были у разработчиков, но их не было, например, у QA инженеров.
  2. Поднято это было на одной большой машине, которой вскоре стало недостаточно, и для следующего шага уже был нужен Kubernetes или что-то аналогичное.
  3. Portainer давал информацию не о состоянии конкретного staging, а о наборе контейнеров.
  4. Приходилось постоянно мерджить файлик с описанием стейджингов, старые стенды надо было удалять.

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

  1. Использовать Kubernetes, чтобы масштабироваться на любое количество staging-окружений и иметь стандартный для современного DevOps набор инструментов.
  2. Решение, которое было бы просто интегрировать в инфраструктуру, уже использующую Kubernetes.
  3. Простой и удобный графический интерфейс для сотрудников на таких ролях как Руководители проектов, Product менеджеры и QA-инженеры. Они могут не работать с кодом напрямую, но инструменты для интроспекции и возможности задеплоить новый стейджинг у них должны быть. Результат не дергаем разработчиков на каждый чих.
  4. Решение, которое удобно интегрируется со стандартными CI/CD пайплайнами, чтобы его можно было использовать в разных проектах. Мы начали с проекта, который использует Github Actions как CI.
  5. Оркестрацию, детали которой сможет гибко настраивать DevOps инженер.
  6. Возможность скрывать логи и внутренние детали кластера в графическом интерфейсе, если проектная команда не хочет, чтобы они были доступны всем и/или есть какие-то опасения на этот счет.
  7. Полная информация и список действий должны быть доступны суперпользователям в лице DevOps инженеров и тимлидов.

И мы приступили к разработке Octopod. Названием послужило смешение нескольких мыслей про K8S, который мы использовали для оркестрации всего на проекте: множество проектов в этой экосистеме отражает морскую эстетику и тематику, а нам представлялся эдакий осьминог, щупальцами оркестрирующий множество подводных контейнеров. К тому же Pod один из основополагающих объектов в Kubernetes.

По техническому стеку Octopod представляет из себя Haskell, Rust, FRP, компиляцию в JS, Nix. Но вообще рассказ не об этом, поэтому я подробнее на этом останавливаться не буду.

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


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

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

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

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

В результате


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

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


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

Open-source


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

Весь исходный код с примерами настройки и документацией доступен в репозитории на Github.
Подробнее..

Перевод Язык моделирования Alloy и приключения с параллельными запросами к базе данных

10.03.2021 16:14:14 | Автор: admin

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



О качестве программного обеспечения и инструментарии


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


  1. Анализ и создание спецификаций
  2. Устранение простых ошибок с использованием системы типов Haskell
  3. Стандартные юнит-тесты и интеграционные тесты
  4. Непрерывная интеграция
  5. Обязательные ревью кода
  6. Тестирование на стендах, проводимое QA инженерами
    (мы используем Octopod для оптимизации процесса разработки и QA)
  7. Тестирование в pre-production среде
  8. Ведение логов и контроль ошибок на этапе эксплуатации

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


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


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


Итак, каков самый ранний этап, на котором мы можем выявить ошибки? Удивительно, но мы можем существенно повысить шансы на выявление ошибок ещё до того, как будет написана первая строка кода!


Alloy выходит на сцену


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


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


Давайте приведем пример. Недавно у нас возникла неприятная проблема со следующим куском кода:


newAuthCode  :: (MonadWhatever m)  => DB.Client  -> DB.SessionId  -> m DB.AuthorizationCodenewAuthCode clid sid = do  let codeData = mkAuthCodeFor clid sid  void $ DB.deleteAllCodes clid sid  void $ DB.insertAuthCode codeData  return code

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


Как это получилось?


Моделирование


Проблема, указанная выше, представляет собой хороший пример задачи для Alloy. Давайте попробуем представить ее, построив модель. Обычно мы начинаем моделирование конкретной проблемы с описания нашего представления об операциях newAuthCode для Alloy. Иными словами, необходимо сначала построить модель операций, затем дополнить ее, построив модель базы данных и привязав поведение базы данных к операциям.


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


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


open util/time  // Импортируем предопределённые объекты Timesig Operation       // У нас есть операции...  { delete : Time   // ...которые удаляют в какой-то момент времени  , insert : Time   // ...и производят вставку в какой-то другой  }  { lt[delete,insert]  // Удаления происходят до вставок    lt[first,delete]   // По техническим причинам в первый                        // момент времени ничего не происходит  }  run {some Operation} for 4 // Показать произвольный пример модели                             // с <= 4 операциями

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


Если вы хотите проследить этот процесс, скачайте alloy и скопируйте в него приведенный выше фрагмент кода. Затем нажмите 'execute' и 'show', чтобы получить модель следующего вида:



Чтобы Alloy показал другие модели, можно нажать 'next'.


Вот один из таких случайных экземпляров, представленный в виде таблицы отношений (нужно несколько раз нажать 'next и выбрать вид 'Table'):


this/OperationdeleteinsertOperation    Time Time   Operation удаляет в момент Time и   вставляет в момент TimeOperation    Time Time   Operation удаляет в момент Time и   и вставляет в момент Time                                                 ОЙ!

Как правило, на данном этапе мы начинаем моделировать таблицы базы данных и семантику операций, но оказалось, что Alloy уже смог показать, почему наши логи содержат нарушение требований!


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


Проблема найдена!


Давайте её исправим!


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


code <- run $ do  handleJust constraintViolation    (launchPG $ selectCodeForSession clid scope sid    (launchPG . pgWithTransaction $ newAuthCode clid scope sid)

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


Будет ли это работать сейчас?


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


open util/time // Импортируем Timesig Token {} // Объекты с названием Tokenone sig DBState // База данных с токенами {userToken : Token lone -> Time}    // В БД не более одного токена в каждый момент врвмени    // (т.к. ограничения БД не позволяют хранить больше одного)sig Operation {   delete : Time , insert : Time , select : Time // Наши операции теперь могут выполнять select}{  lt[first,delete]   // Ничего не происходит в первый момент времени                     // по техническим причинам  lt[delete,insert]  // Первой выполняется операция delete  lte[insert,select] // select выполняется после или во время insert'а  no userToken.(insert.prev) // Если вставка сработала (т.е. таблица  => insert = select         // была пустой во время выполнения),                             // получаем значение в тот же самый                              // момент времени (т.е. у нас запрос                             // 'INSERT RETURNING').                             // В противном случае вызываем обработчик                             // исключения, и select выполняется чуть позже}

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


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


fact Trace {                           // Факт Trace описывает поведение системы all t : Time - first | {              // на всех шагах, кроме первого:   some delete.t => no userToken.t       // Если происходит удаление, таблица пуста   some insert.t => some userToken.t     // Если происходит вставка, таблица не пуста   no delete.t and no insert.t           // Если не происходит ни вставок, ни удалений,    => userToken.t = userToken.(t.prev)  // таблица не меняется  }}

То есть мы описываем, как состояние базы данных изменяется в зависимости от некоторых происходящих событий.


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


Давайте примем это за утверждение и попросим Alloy проверить его.


assert selectIsGood {         // То, что мы хотим проверить all s : Operation.select |   // Всегда, когда выполняется select,  some userToken.s            // в базе присутствуем токен}check selectIsGood for 6 // Проверить, что selectIsGood всегда истинно

К сожалению, запуск этой проверки дает нам следующий контрпример:


DBState userToken   DBStateTokenTime                 Token находится в БД в моменты Time и Time              Time                TokenTime   Token в БД в момент Time.                                    Токены есть в таблице только                 моменты Time, Time и Time                 Заменит, что в момент                 Time токенов нет!Operation     deleteinsertselectOperation     TIME Time TimeOperation     Time Time TIME    Таблица пуста в момент Time и     select не работает для Operation!Operation     Time Time Time                                               Это моменты времени, когда                происходят соответствующие действия

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


Итак, предлагаемое исправление, которое было проверено на согласование типов, протестировано, прошло интеграцию и проверку коллегами, оказалось ошибочным!


Параллельная обработка легко не дается.


Мои выводы


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


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


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


Где взять Alloy?


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





P.S. Правильное решение этой проблемы заключается в том, чтобы использовать сериализуемые транзакции и не возиться с параллельной обработкой.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru