<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Сделано-В-ЭРЦИТ on ЭРЦИТ Технопоинт</title>
        <link>https://blog.ercit.ru/tags/%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D0%BD%D0%BE-%D0%B2-%D1%8D%D1%80%D1%86%D0%B8%D1%82/</link>
        <description>Recent content in Сделано-В-ЭРЦИТ on ЭРЦИТ Технопоинт</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>ru</language>
        <copyright>ЭРЦИТ ИТИ ХГУ</copyright>
        <lastBuildDate>Sun, 11 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.ercit.ru/tags/%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D0%BD%D0%BE-%D0%B2-%D1%8D%D1%80%D1%86%D0%B8%D1%82/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>От урока - к проду</title>
        <link>https://blog.ercit.ru/p/ot-uroka-k-produ/</link>
        <pubDate>Sun, 11 Oct 2026 00:00:00 +0000</pubDate>
        
        <guid>https://blog.ercit.ru/p/ot-uroka-k-produ/</guid>
        <description>&lt;img src="https://blog.ercit.ru/p/ot-uroka-k-produ/pokoriteli-kosmosa.webp" alt="Featured image of post От урока - к проду" /&gt;&lt;h2 id=&#34;чему-учим---на-том-и-работаем&#34;&gt;Чему учим - на том и работаем
&lt;/h2&gt;&lt;p&gt;Когда студенты садятся за «Функциональное программирование», вопрос обычно один и тот же: «А это вообще где-то реально используют, или это просто про типы и чистые функции ради тренировки?» В ЭРЦИТ и в ИТИ ответ простой: да, используют. И не как эксперимент, а как часть производственного стека.&lt;/p&gt;
&lt;p&gt;Программа дисциплины «Функциональное программирование» выстраивается так, что студенты осваивают ровно те инструменты, на которых затем пишутся производственные системы. Не «академический Haskell ради понимания типов», а Haskell, на котором работает бэкенд. Не «Elm ради знакомства с функциональным фронтендом», а Elm, на котором собирается форма регистрации проектов.&lt;/p&gt;
&lt;p&gt;Программа пересматривается каждый год — чтобы соответствовать текущим задачам. В мейнстрим-разработке паттерны проектирования часто компенсируют ограничения языка, а не решают предметные задачи. Это не лабораторные работы, где Haskell нужен для тренировки, а потом никуда не идёт. Тот же Haskell, те же типы и та же явная обработка ошибок, которые студенты разбирают на занятиях, оказываются в коде, обслуживающем реальную задачу — с заказчиком, сроками и людьми, которые этой системой пользуются.&lt;/p&gt;
&lt;p&gt;В этот раз реальная задача нашлась сама — система регистрации проектов для форума «Грани Будущего». Это межрегиональный молодёжный форум креативного мышления и изобретательства, который проводят совместно ИТИ ХГУ, Российская инженерная академия и Хакасское региональное отделение ВОИР. В этом году форум получил статус международного — в нём участвуют коллеги из других стран. Вместе с охватом выросли и требования к цифровой части: заявок больше, стран больше, данные разнороднее. В этом году уже с индексом 2026 (без малого четвёртый год!), а значит и в стеке тоже нужно «подрасти».&lt;/p&gt;
&lt;p&gt;ЭРЦИТ здесь — технологический партнёр: проектирование, разработка и поддержка цифровых сервисов форума. Система регистрации проектов — один из таких. И написан он на том самом Haskell, который студенты разбирают на занятиях. Это не отдельный случай, а обычный порядок вещей: учебный процесс устроен так, что инструменты, которые студенты видят на занятиях, — те же самые, на которых делаются рабочие проекты. Круг задач общий — от учебных, до сервисов, которыми пользуются люди. Система регистрации проектов — ещё один шаг в ту же сторону – чему учим — на том и работаем.&lt;/p&gt;
&lt;h2 id=&#34;от-паттернов-к-языку&#34;&gt;От паттернов к языку
&lt;/h2&gt;&lt;p&gt;В мейнстрим-разработке паттерны проектирования часто компенсируют ограничения языка, а не решают предметные задачи. Питер Норвиг в докладе «Design Patterns in Dynamic Languages» (1996) показал, что из 23 паттернов «банды четырёх» 16 исчезают или становятся тривиальными в языках с более мощной абстракцией&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. С тех пор этот тезис только подтверждался. В «Fluent Python» Лучано Рамальо (2-е издание, O&amp;rsquo;Reilly, 2022), в главе 10 «Design Patterns with First-Class Functions», на примере паттернов Strategy и Command показано, как функции первого класса заменяют целые иерархии классов меньшим количеством кода. Критика паттернов как «заплаток для отсутствующих фич языка» стала общим местом.&lt;/p&gt;
&lt;p&gt;Если паттерны — это действительно заплатки для того, чего в языке не хватает, то выбор языка становится важнее выбора паттернов. В языке с более мощными абстракциями многие паттерны просто не возникают: то, что в одном языке требует отдельного класса и иерархии, в другом выражается одной функцией.&lt;/p&gt;
&lt;p&gt;Речь именно об абстракциях, а не о синтаксисе. Дело не в том, что в Haskell другой способ писать циклы или объявлять переменные. Дело в том, какие средства язык даёт для сборки логики. Функции как значения — то есть функция может быть аргументом, результатом, элементом списка. Алгебраические типы данных — то есть тип собирается из вариантов: «или то, или это», — и компилятор требует обработать каждый. Параметрический полиморфизм — то есть функция работает для любого типа, не зная о нём ничего конкретного. Именно поэтому паттерны, которые в мейнстриме существуют как отдельные сущности, здесь растворяются в обычном коде. Strategy сводится к аргументу функции. Command — к значению, потому что команда и есть функция. Норвиг в своём докладе прямо сгруппировал паттерны по тому, чем именно их заменяет язык: функции первого класса снимают Command, Strategy, Template-Method, Visitor; комбинация методов — Mediator и Observer.&lt;/p&gt;
&lt;p&gt;Практический смысл в том, что программист не тратит время на конструкции, которые язык мог бы дать бесплатно. В языке без алгебраических типов «функция может не вернуть результат» выражается через null, исключение или sentinel-значение — и каждый раз это риск, что кто-то забудет проверить. В Haskell это как минимум &lt;code&gt;Maybe&lt;/code&gt; и компилятор не даст вам этого забыть. Когда абстракции встроены в язык, паттерн перестаёт быть отдельной сущностью и становится частью обычного кода.&lt;/p&gt;
&lt;p&gt;Именно поэтому в основе курса — Haskell: не как экзотика ради расширения кругозора, а как язык, в котором средства для таких систем встроены в саму модель программирования. Как этот выбор выглядит в реальном проекте — покажем на примере системы регистрации. Но прежде — очевидный вопрос: почему не привычный стек?&lt;/p&gt;
&lt;h2 id=&#34;почему-не-django-не-rails-не-aspnet-и-не-spring&#34;&gt;Почему не Django, не Rails, не ASP.NET и не Spring
&lt;/h2&gt;&lt;p&gt;Он и возникает первым у любого, кто услышит про систему регистрации на Haskell и Elm. Привычный стек — Python с Django, Ruby on Rails, C# с ASP.NET Core или Java со Spring Framework. Всё это зрелые, проверенные инструменты. Но у них есть общая черта, которая для нас стала решающей: &lt;strong&gt;в каждом из них безопасность приходится покупать&lt;/strong&gt;. Либо платой оказываются compile-time гарантии — как в динамических языках, — либо простота и прозрачность — как в платформенных фреймворках. Третьего варианта в привычном стеке нет.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Динамические языки (Python, Ruby)&lt;/em&gt; ловят ошибки в момент выполнения. Django и Rails дают высокую скорость разработки, но цена — отсутствие compile-time гарантий из коробки. Опечатка в имени поля, несоответствие типов, забытая ветка в обработке ошибок — всё это проявляется в лучшем случае на тестах, в худшем — в продакшене. Дополнительные проверяльщики — mypy и django-stubs для Python, Sorbet для Ruby — частично закрывают разрыв, но это опциональные внешние инструменты: их нужно отдельно внедрять и поддерживать, а без них проект живёт без статических проверок вовсе. Для системы, где данные формы уходят одновременно в виртуальную галерею, в базу для печатной продукции, в рассылки и в карточки для жюри, «поймать в проде» означает испорченный буклет или потерянные заявки. Кроме того, первые версии нашего регистратора уже были написаны на Python с Django. Решение получилось сложнее, чем хотелось: неявности и отдельные ошибки усложняли логику пользовательского интерфейса, а исправления приходилось вносить без полного понимания внутренней работы фреймворка.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Платформенные фреймворки (ASP.NET Core, Spring Framework)&lt;/em&gt; статически типизированы, но платят за это сложностью. Конфигурационный слой, dependency injection, многослойная архитектура, аннотации — типичный Spring-проект обрастает конфигурацией раньше, чем появляется первая бизнес-логика; на смену XML пришли аннотации и &lt;code&gt;application.yml&lt;/code&gt;, но сам слой никуда не делся. ASP.NET Core проще, однако остаётся в парадигме «сначала настрой фреймворк, потом пиши код». Важнее другое: null-безопасность ни в ASP.NET Core, ни в Spring не является свойством системы типов — это опция. В C# с версии 8 она даёт лишь предупреждения компилятора, которые можно подавить или отключить. В Java защита от null остаётся делом внешних анализаторов и аннотаций. Целые классы ошибок — неполная обработка вариантов, несогласованность контрактов между модулями — и там, и там остаются вне контроля компилятора, как и та степень композиции, которая естественна для функциональных языков.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Haskell&lt;/em&gt; предлагет другой путь. Порог входа при этом выше — функциональная парадигма требует привыкать к мышлению без мутабельного состояния, к рекурсии вместо циклов, к композиции вместо наследования. Экосистема меньше — в Hackage десятки тысяч пакетов, а не миллионы, как в npm, и не сотни тысяч, как в PyPI.&lt;/p&gt;
&lt;p&gt;Но взамен — три вещи, которые в мейнстриме существуют как опции, а здесь являются фундаментом.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Компилятор как обязательная страховка.&lt;/em&gt; Статическая типизация есть в C#, Kotlin, Rust, TypeScript, Go. Но там её можно ослабить, отключить или обойти. В Haskell статическая система типов гарантирует, что все ошибки типов обнаруживаются на этапе компиляции. Если тип сходится — код компилируется. Если не сходится — не компилируется.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Отсутствие null как части языка.&lt;/em&gt; В Haskell нет null-ссылок, как в Java или C#. Нет значения, которое можно присвоить переменной любого ссылочного типа и получить «ничего». Если функция может не вернуть результат, это видно в типе: &lt;code&gt;Maybe a&lt;/code&gt;, &lt;code&gt;Either e a&lt;/code&gt;. Компилятор заставит обработать оба случая при pattern matching — но не помешает использовать &lt;code&gt;fromJust&lt;/code&gt; или &lt;code&gt;undefined&lt;/code&gt;, если программист решит обойти систему. В Rust (&lt;code&gt;Option&lt;/code&gt;), Kotlin (nullable-типы с &lt;code&gt;?&lt;/code&gt;), Swift (optionals) эта проблема тоже решена на уровне языка. Разница в том, как именно. В Haskell &lt;code&gt;Maybe&lt;/code&gt; — это не встроенное ключевое слово, а обычный алгебраический тип данных из стандартной библиотеки. Нет особого синтаксиса, нет специальной семантики. Это позволяет работать с ним теми же средствами, что и с любым другим типом: композиция через &lt;code&gt;Functor&lt;/code&gt;/&lt;code&gt;Applicative&lt;/code&gt;/&lt;code&gt;Monad&lt;/code&gt;, обобщённые функции — и не требует отдельных конструкций в языке.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Композиция как основной способ сборки логики.&lt;/em&gt; C# LINQ, Java Stream API, JavaScript pipe/compose — всё это композиция функций. Разница в том, как она устроена. В Haskell и Elm композиция — это идиома, к которой прибегают постоянно, а не библиотечная возможность. Бизнес-логика собирается из маленьких чистых функций, каждая из которых делает что-то одно. Их можно тестировать отдельно, комбинировать по-разному, переиспользовать.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ничего из перечисленного само по себе не создаёт систему: из чистых функций и явных типов ещё нужно собрать API, хранение данных и интерфейс. Система регистрации проектов — тот случай, когда все три принципа применены к конкретной инженерной задаче, и выбор каждого инструмента диктовался именно ими. Начнём с бэкенда.&lt;/p&gt;
&lt;h2 id=&#34;haskell-servant--warp-строгий-контракт-и-надёжный-транспорт&#34;&gt;Haskell Servant + Warp: строгий контракт и надёжный транспорт
&lt;/h2&gt;&lt;p&gt;Для бэкенда выбран стек Haskell Servant + Warp.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Servant&lt;/em&gt; описывает API как типы: маршрут, метод, тело запроса, формат ответа — всё это часть типа. Если тип сходится, контракт между фронтендом и бэкендом формально согласован. Для системы регистрации, где важна полнота данных, это страховка от «тихих» ошибок.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Warp&lt;/em&gt; — быстрый и лёгкий HTTP-сервер для WAI. В независимых замерах Warp — самый быстрый из Haskell-серверов&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;, а Mighttpd2, построенный на Warp, по описанию пакета в Debian «в тесте httperf Ping-Pong быстрее nginx»&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;. Для системы регистрации проектов, где пиковая нагрузка — несколько десятков заявок в час, это не критично. Но это показывает класс инструмента: мы не платим производительностью за типобезопасность.&lt;/p&gt;
&lt;p&gt;Но почему именно эта комбинация? В экосистеме есть несколько зрелых вариантов.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Scotty&lt;/em&gt; — самый простой. Он вдохновлён Ruby-фреймворком Sinatra и даёт минимум церемоний. Для прототипов и небольших сервисов это удобный выбор. Но API в нём описывается набором функций-обработчиков, а не одним контрактом: нет типа, который связывал бы маршруты, тела запросов и форматы ответов в одно целое. Пока сервис маленький — это незаметно. Когда фронтенд и бэкенд должны сходиться в каждом поле, разрозненное описание становится слабым местом.&lt;/p&gt;
&lt;p&gt;Есть и более общий вопрос. Scotty повторяет идеологию Sinatra — а Sinatra написана на Ruby, языке без статической типизации и без функций как полноценных значений в том смысле, в каком они есть в Haskell. То, что хорошо работает в Ruby, — минимализм, отсутствие церемоний, маршруты как обычные блоки кода — в Haskell переносится один в один, но при этом перестаёт быть преимуществом. В Ruby за эту простоту платят отсутствием проверок на этапе компиляции, и там это осознанный размен. В Haskell та же простота означает, что мы не используем то, ради чего взяли язык.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Yesod&lt;/em&gt; — полноценный фреймворк «под ключ». Opinionated, навязывающий мнение. Он даёт шаблоны, маршрутизацию, работу с БД, аутентификацию — всё сразу. Yesod активно использует Template Haskell и QuasiQuotes: маршруты описываются внутри Haskell-кода через специальные DSL-конструкции. Это работает, но цена — жёсткая привязка к парадигме фреймворка. Для нас это было бы избыточно: у нас Elm на фронте, JSON API, и мы не хотим, чтобы фреймворк диктовал, как нам организовывать наш код.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Servant&lt;/em&gt; занимает особую позицию. Его главная идея — &lt;u&gt;&lt;em&gt;описание API на уровне типов&lt;/em&gt;&lt;/u&gt;. Вы пишете не набор функций-обработчиков, а тип, который описывает весь API — маршруты, методы, параметры, тела запросов, форматы ответов. Из этого типа компилятор выводит всё остальное: проверку соответствия обработчиков, автоматическую сериализацию JSON, генерацию клиентов и документации. &lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Ключевое преимущество Servant для нас — &lt;em&gt;композиция и декларативность&lt;/em&gt;. API описывается как один тип, а не как разрозненные куски кода. Если нужно добавить эндпоинт, вы добавляете его в тип. Если нужно поменять формат ответа — меняете тип, и компилятор показывает все места, которые нужно поправить. Это способ держать контракт между фронтендом и бэкендом в одном месте, где его невозможно случайно нарушить.&lt;/p&gt;
&lt;h2 id=&#34;что-это-даёт-на-практике-django-rest-framework-и-servant&#34;&gt;Что это даёт на практике: Django REST Framework и Servant
&lt;/h2&gt;&lt;p&gt;Чтобы показать, почему выбран именно Servant, сравним его с самым популярным инструментом для API на Python — Django REST Framework. Задача одна: принять контактные данные из формы регистрации проекта. Проверяем &lt;code&gt;email&lt;/code&gt; и &lt;code&gt;phone&lt;/code&gt;. У email — длина не больше 200 символов и формат вида &lt;code&gt;name@domain.tld&lt;/code&gt;. У phone — только длина, не больше 30 символов (формат не проверяем: пользователи из разных стран). Плюс правило: хотя бы одно из двух полей должно быть заполнено.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Django REST Framework&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# urls.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;api/projects/&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ProjectListCreateView&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;api/projects/&amp;lt;int:pk&amp;gt;/&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ProjectDetailView&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# views.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ProjectListCreateView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;generics&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ListCreateAPIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;queryset&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Project&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;objects&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;all&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;serializer_class&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ProjectSerializer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;17
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;18
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;19
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;20
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;21
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;22
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# serializers.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;django.core.validators&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EmailValidator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ProjectSerializer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;serializers&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ModelSerializer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;serializers&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CharField&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;max_length&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;200&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;required&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;False&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;allow_blank&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;validators&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;EmailValidator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;step6.email: неправильный формат&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;)],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Meta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;model&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;fields&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;phone&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;validate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;attrs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;attrs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;phone&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;attrs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;phone&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;and&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;phone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;raise&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;serializers&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ValidationError&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;non_field_errors&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;step6: email or phone required&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;attrs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Маршрут живёт в &lt;code&gt;urls.py&lt;/code&gt;. Класс представления — в &lt;code&gt;views.py&lt;/code&gt;. Поля и правила — в &lt;code&gt;serializers.py&lt;/code&gt;. В Django REST Framework существует чёткое разделение ответственности: модели, сериализаторы, представления и URL-конфигурация логически разделены по своим модулям. Три файла, между ними нет языковой связи: &lt;code&gt;views.py&lt;/code&gt; ссылается на &lt;code&gt;ProjectSerializer&lt;/code&gt; по имени, &lt;code&gt;urls.py&lt;/code&gt; ссылается на &lt;code&gt;ProjectListCreateView&lt;/code&gt; по имени. Переименуй класс — получишь ошибку не при сборке, а при импорте.&lt;/p&gt;
&lt;p&gt;В DRF имена методов &lt;code&gt;validate&lt;/code&gt; и &lt;code&gt;validate_&amp;lt;field_name&amp;gt;&lt;/code&gt; не проверяются при загрузке кода. DRF ищет их динамически, во время выполнения. Если в имени есть опечатка, метод просто не будет найден. Это не приведёт к немедленной ошибке, но и валидация не сработает. В результате в систему могут попасть некорректные данные, что создаст проблемы позже — при обработке, печати или в других сервисах. Это не «тихий пропуск», а отложенная ошибка, которая может быть опаснее.&lt;/p&gt;
&lt;p&gt;В Servant такая ситуация исключена. Контракт между фронтендом и бэкендом описан одним типом. Компилятор проверяет соответствие обработчика этому типу до запуска. Если правило валидации не применяется, это видно в коде: guards либо есть, либо их нет. Промежуточного состояния — «метод написан, но не вызывается» — здесь не существует.&lt;/p&gt;
&lt;p&gt;Внутри &lt;code&gt;serializers.py&lt;/code&gt; правила тоже заданы разными механизмами: длина — параметр &lt;code&gt;max_length&lt;/code&gt; у поля; формат — объект &lt;code&gt;EmailValidator&lt;/code&gt; в списке &lt;code&gt;validators&lt;/code&gt;; «хотя бы одно поле» — императивный метод &lt;code&gt;validate&lt;/code&gt;. Стандартный сериализатор DRF выполняет валидацию в три этапа: проверка на уровне поля (required, blank, validators), кастомная проверка на уровне поля (метод &lt;code&gt;validate_&amp;lt;fieldname&amp;gt;()&lt;/code&gt;) и общая проверка (метод &lt;code&gt;validate()&lt;/code&gt;). Порядок проверок определяется DRF, а не тем, как они записаны в коде.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Haskell + Servant&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;17
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;18
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;19
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;20
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;21
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;22
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;23
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;24
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;25
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;26
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-haskell&#34; data-lang=&#34;haskell&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;ContactInput&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;ContactInput&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;Maybe&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;Maybe&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Contact&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Contact&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Int&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;Maybe&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;Maybe&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- API — это тип. Маршрут, метод, тело запроса, формат ответа.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;ContactAPI&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;       &lt;span class=&#34;s&#34;&gt;&amp;#34;api&amp;#34;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;:&amp;gt;&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;contacts&amp;#34;&lt;/span&gt;                       &lt;span class=&#34;c1&#34;&gt;-- путь /api/contacts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;kt&#34;&gt;:&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;ReqBody&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;&amp;#39;[JSON]&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;ContactInput&lt;/span&gt;         &lt;span class=&#34;c1&#34;&gt;-- тело запроса: ContactInput, JSON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;kt&#34;&gt;:&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Post&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;&amp;#39;[JSON]&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Contact&lt;/span&gt;                 &lt;span class=&#34;c1&#34;&gt;-- ответ на POST: Contact, JSON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;badRequest&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;::&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Text&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Handler&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;badRequest&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;throwError&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;$&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;err400&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;errBody&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;encodeUtf8&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Обработчик. Вход разбирается прямо в заголовке.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Пять guards — пять правил, сверху вниз.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;createContact&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;::&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;ContactInput&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Handler&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Contact&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;createContact&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;ContactInput&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;phone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Nothing&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Nothing&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;phone&lt;/span&gt;    &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;badRequest&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;step6: email or phone required&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Just&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;e&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;length&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;e&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;200&lt;/span&gt;     &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;badRequest&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;step6.email: размер превышен&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Just&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;e&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;isValidEmail&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;e&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;badRequest&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;step6.email: неправильный формат&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Just&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;p&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;phone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;length&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;p&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;      &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;badRequest&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;step6.phone: размер превышен&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;otherwise&lt;/span&gt;                             &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pure&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;Contact&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;phone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;-- единичка тут &amp;#34;просто какой-то &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Тип сервера ВЫВОДИТСЯ из типа API.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- createContact обязан ему соответствовать. Не совпадёт — код не соберётся.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;server&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;::&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;Server&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;ContactAPI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;server&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;createContact&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;ContactAPI&lt;/code&gt; — один тип. Он описывает весь контракт: путь &lt;code&gt;/api/contacts&lt;/code&gt;, метод POST, входной JSON &lt;code&gt;ContactInput&lt;/code&gt;, выходной JSON &lt;code&gt;Contact&lt;/code&gt;. Это не «конфигурация» и не «декоратор» — это полноценный Haskell-тип. Строковые литералы &lt;code&gt;&amp;quot;api&amp;quot;&lt;/code&gt; и &lt;code&gt;&amp;quot;contacts&amp;quot;&lt;/code&gt; — сегменты пути. &lt;code&gt;:&amp;gt;&lt;/code&gt; — оператор, соединяющий части API в цепочку. &lt;code&gt;ReqBody &#39;[JSON] ContactInput&lt;/code&gt; — тело запроса, парсится в &lt;code&gt;ContactInput&lt;/code&gt;. &lt;code&gt;Post &#39;[JSON] Contact&lt;/code&gt; — метод POST, ответ сериализуется в JSON из &lt;code&gt;Contact&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Server ContactAPI&lt;/code&gt; — type family. Это не «одна из реализаций» — это type family, функция, которая принимает типы как входные данные и возвращает типы. Компилятор вычисляет из &lt;code&gt;ContactAPI&lt;/code&gt; тип обработчика. &lt;code&gt;createContact&lt;/code&gt; обязан этому типу соответствовать. Не совпадёт — сборка упадёт. Не «тест не пройдёт», не «в проде вылезет» — вообще не соберётся.&lt;/p&gt;
&lt;p&gt;Все пять правил — это пять guards, идущих подряд, легко читаются, самодокументированы. Каждая строка: условие → результат. &lt;code&gt;Just e &amp;lt;- email, T.length e &amp;gt; 200&lt;/code&gt; читается слева направо: «если email не пустой и длиннее 200». &lt;code&gt;badRequest&lt;/code&gt; определён один раз и применяется четыре раза. Последняя строка — &lt;code&gt;otherwise&lt;/code&gt;: всё прошло, возвращаем &lt;code&gt;Contact&lt;/code&gt;. Убери строку — правило исчезнет и это видно сразу.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Разница в одной таблице&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Django&lt;/th&gt;
&lt;th&gt;Servant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Где живёт контракт&lt;/td&gt;
&lt;td&gt;в трёх файлах, связанных по именам&lt;/td&gt;
&lt;td&gt;в одном типе &lt;code&gt;ContactAPI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кто проверяет соответствие&lt;/td&gt;
&lt;td&gt;вручную, утилиты, тесты, если повезёт&lt;/td&gt;
&lt;td&gt;компилятор&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Правила валидации&lt;/td&gt;
&lt;td&gt;три разных механизма в одном классе&lt;/td&gt;
&lt;td&gt;пять guards подряд&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Опечатка в имени метода&lt;/td&gt;
&lt;td&gt;неясно, где выстрелит&lt;/td&gt;
&lt;td&gt;ошибка сборки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Изменение поля &lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;правится вручную в нескольких местах&lt;/td&gt;
&lt;td&gt;компилятор показывает все места&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Разница в целом здесь не в том, что «Haskell строже». Разница в том, &lt;em&gt;где живёт контракт&lt;/em&gt;. В Django он распределён по файлам и поддерживается вручную. В Servant он описан одним типом и проверяется компилятором до запуска. Всё остальное — следствие этого.&lt;/p&gt;
&lt;p&gt;Контракт, который проверяется компилятором, меняет и требования к остальным частям стека: каждый следующий выбор делается по той же логике — не «что привычнее», а « что даёт гарантии при минимальной цене». Следующая часть системы — хранение данных.&lt;/p&gt;
&lt;h2 id=&#34;sqlite-вместо-postgresql-осознанный-минимализм&#34;&gt;SQLite вместо PostgreSQL: осознанный минимализм
&lt;/h2&gt;&lt;p&gt;PostgreSQL — стандарт для больших нагрузок, параллельных записей и репликации. PostgreSQL — это полноценный сервер, предназначенный для сложных запросов и многопользовательских сред. У нас нагрузка — регистрация проектов на форум. Применять полноценную СУБД было бы довольно расточительно, поэтому мы использовали один известный, встраиваемый движок&amp;hellip; Вы конечно же ничего о нём не знаете, потому что называется он — SQLite&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt; 😉&lt;/p&gt;
&lt;p&gt;SQLite — вероятно, один из самых тщательно протестированных программных проектов в мире. Это не маркетинговое утверждение, а измеренная величина.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Соотношение тестового кода к коду библиотеки — 590 к 1 (для версии 3.42.0)&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a href=&#34;#fn:6&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;100% покрытие ветвлений (branch coverage) и 100% покрытие MC/DC (Modified Condition/Decision Coverage) для ядра. MC/DC — это стандарт, применяемый в авионике по DO-178B&lt;sup id=&#34;fnref1:6&#34;&gt;&lt;a href=&#34;#fn:6&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;Четыре независимых набора тестов, каждый разрабатывается отдельно: TCL Tests, проприетарный TH3, SQL Logic Test (сравнение с PostgreSQL, MySQL, SQL Server и Oracle) и фаззер dbsqlfuzz&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a href=&#34;#fn:7&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;Перед каждым релизом прогоняется soak-тест TH3 — порядка 248,5 миллиона тестов&lt;sup id=&#34;fnref1:7&#34;&gt;&lt;a href=&#34;#fn:7&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;7,2 миллиона запросов в SQL Logic Test на 1,12 ГБ тестовых данных&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a href=&#34;#fn:8&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Когда мы кладём данные регистрации проектов в SQLite, мы кладём их в базу, проверенную строже, чем большинство коммерческих СУБД. Разумеется, SQLite не универсальна: одновременная запись от множества процессов, горизонтальное масштабирование, сложные административные сценарии — не её территория. Для нашей задачи — десятки заявок в час, один процесс, predictable-нагрузка — это не ограничения, а несуществующие требования. Остаётся фронтенд — и там та же логика выбора действует с той же последовательностью.&lt;/p&gt;
&lt;h2 id=&#34;фронтенд-elm-для-логики-и-w3-css-для-представления&#34;&gt;Фронтенд: Elm для логики и W3 CSS для представления
&lt;/h2&gt;&lt;p&gt;Если бэкенд отвечает за надёжность обработки данных, то фронтенд — за предсказуемую модель состояния. Принцип здесь тот же, что и на бэкенде: нужен язык с теми же свойствами — чистый, функциональный, со строгой типизацией, — но живущий в браузере. Им стал Elm: разработанный для фронтенда язык, построенный на тех же основах, что и Haskell, — неизменяемость, чистые функции, явная обработка ошибок. Для формы регистрации это означало компилятор на границе, где ошибки всплывают чаще всего: во взаимодействии пользователя с интерфейсом. Здесь у нас был выбор не только между «Elm или что-то на JavaScript», но и между двумя принципиально разными подходами: Elm, компилируемый в JavaScript или Haskell, компилируемый в WebAssembly. Pure-Javascript мы отвергли принципиально, Javascript — это огромный тропический лес, в котором полным-полно известных и неизвестных видов «растений» и «животных». Тем не менее, Javascript-код у нас есть и мы им пользуемся, почему — смотрите дальше.&lt;/p&gt;
&lt;p&gt;Elm задаёт чёткие правила: состояние, обновление, представление — всё разделено и явно описано. Это The Elm Architecture (TEA) — паттерн управления состоянием, заимствованный из функционального языка Elm, который обеспечивает строгий однонаправленный поток данных и предсказуемое обновление состояния. Приложения на Elm обязаны следовать TEA — простому паттерну проектирования, который подразумевает разделение кода на три части: Model, View и Update. Model — это данные, которые используются в приложении. View — это функция, преобразующая Model в пользовательский интерфейс. Update — это функция, ответственная за обновление состояния: она принимает Model и сообщение, и возвращает обновлённую Model. Логика каждой программы в Elm разбивается на три чётко разделённые части: модель, обновление и отображение.&lt;/p&gt;
&lt;p&gt;Главное преимущество Elm перед React — отсутствие runtime-исключений на практике. Elm использует вывод типов, чтобы находить проблемы на этапе компиляции&lt;sup id=&#34;fnref:9&#34;&gt;&lt;a href=&#34;#fn:9&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt;. Официальная формулировка: «No runtime exceptions in practice»&lt;sup id=&#34;fnref1:9&#34;&gt;&lt;a href=&#34;#fn:9&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt;. В Elm не бывает &lt;code&gt;undefined is not a function&lt;/code&gt; или &lt;code&gt;Cannot read property of null&lt;/code&gt;. В React даже с TypeScript ошибки времени выполнения остаются возможными — и это не редкость, а обычное дело. В Elm такой категории ошибок просто нет.&lt;/p&gt;
&lt;p&gt;Но у этой строгости есть обратная сторона: Elm не может напрямую вызывать JavaScript-код. Для этого в языке предусмотрен единственный официальный механизм — ports. Это не лазейка и не костыль, а осознанное решение: любой обмен данными с внешним миром проходит через явно объявленный канал с проверкой типов на обоих концах. Порт, по которому данные уходят из Elm в JavaScript, объявляется как &lt;code&gt;port sendMessage : String -&amp;gt; Cmd msg&lt;/code&gt;, а порт для приёма данных — как &lt;code&gt;port messageReceiver : (String -&amp;gt; msg) -&amp;gt; Sub msg&lt;/code&gt;. На стороне JavaScript эти порты выглядят как &lt;code&gt;app.ports.sendMessage.subscribe(...)&lt;/code&gt; и &lt;code&gt;app.ports.messageReceiver.send(...)&lt;/code&gt;. Типы проверяются компилятором Elm, а данные сериализуются в JSON и обратно. Именно через ports в реальных проектах подключают WebSockets, &lt;code&gt;localStorage&lt;/code&gt;, сторонние библиотеки и всё, что находится за пределами Elm. В нашем случае через них же проходит и интеграция с серверной частью, где данные формы уходят в Haskell-бэкенд. Также, на Javascript пришлось экстренно делать некоторые элементы UI, но мы клятвенно обещаем, что в последствии перетащим их на Elm.&lt;/p&gt;
&lt;p&gt;Теперь про WebAssembly и Haskell: GHC умеет компилировать Haskell в WebAssembly, и это направление продолжает развиваться&lt;sup id=&#34;fnref:10&#34;&gt;&lt;a href=&#34;#fn:10&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;10&lt;/a&gt;&lt;/sup&gt;. Существует production-ready фреймворк &lt;em&gt;Miso&lt;/em&gt; — библиотека для фронтенд-приложений на Haskell с компиляцией как в JavaScript, так и в WebAssembly&lt;sup id=&#34;fnref:11&#34;&gt;&lt;a href=&#34;#fn:11&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;11&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Почему же мы выбрали Elm, а не Haskell + WASM?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Зрелость инструментов.&lt;/em&gt; Elm создан специально для фронтенда, с первого дня. GHC WASM backend — сравнительно молодая функция, которая продолжает развиваться. Для production-системы регистрации проектов мы не можем позволить себе зависеть от экспериментальной сборки.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Взаимодействие с DOM.&lt;/em&gt; WebAssembly не может напрямую обращаться к DOM — нужен «клей» на JavaScript, и эти переходы могут съесть выигрыш. Для форм регистрации, где основная работа — ввод текста, чекбоксы, загрузка файлов, WASM не даёт преимуществ.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Простота поддержки.&lt;/em&gt; Elm компилируется в чистый JavaScript, который работает в любом браузере без дополнительных требований&lt;sup id=&#34;fnref2:9&#34;&gt;&lt;a href=&#34;#fn:9&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt;. WASM требует настройки сборки и поддержки на уровне браузера.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Elm даёт нам то же главное преимущество, что и Haskell на бэкенде, — компилятор как страховку. Только здесь это касается состояния интерфейса, а не бизнес-логики. И Elm построен на тех же концепциях функционального программирования, что и Haskell: стек остаётся в рамках одной экосистемы, а не собирается из разнородных частей.&lt;/p&gt;
&lt;h2 id=&#34;w3-css-минимализм-не-как-компромисс-а-как-принцип&#34;&gt;W3 CSS: минимализм не как компромисс, а как принцип
&lt;/h2&gt;&lt;p&gt;Остаётся представление. Логику интерфейса ведёт Elm, данные — Haskell, и следующий выбор подчиняется тому же принципу: минимизировать императивный слой. CSS по своей природе декларативен — он описывает, как выглядит элемент, а не как этого добиться. Но CSS-фреймворк с JavaScript-частью — как Bootstrap до версии 5, требовавший jQuery, — добавляет в слой представления внешнюю функциональность: её нужно поддерживать, сопровождать и интегрировать с собственной логикой интерфейса. Поэтому для стилизации выбран W3.CSS — «фреймворк» из семейства pure-CSS, без JavaScript.&lt;/p&gt;
&lt;p&gt;Философия W3 CSS — «предоставить инструменты, а не диктовать архитектуру». W3 CSS даёт классы для типовых задач: сетки, карточки, формы, таблицы, навигация, типографика. Вы используете только то, что нужно и не «грузите» в проект ничего лишнего.&lt;/p&gt;
&lt;p&gt;Сетка и адаптивность: W3 CSS поддерживает 12-колоночную адаптивную сетку с mobile-first подходом. Колонки перестраиваются автоматически при изменении размера экрана: на большом экране — три колонки, на маленьком — одна&lt;sup id=&#34;fnref:12&#34;&gt;&lt;a href=&#34;#fn:12&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;12&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Дизайн-язык: W3 CSS вдохновлён Google Material Design: плоские элементы, чёткие границы, минималистичные тени, продуманная типографика. Шрифты — читаемые, с хорошим межбуквенным интервалом. Всё это делает интерфейс предсказуемым и одинаково понятным на разных экранах — от смартфона до десктопа.&lt;/p&gt;
&lt;p&gt;Что это даёт нам? В системе регистрации проектов W3 CSS отвечает за всё представление: формы, карточки проектов, модальные окна, сетку галереи, уведомления. Elm управляет состоянием и логикой, W3 CSS — отображением. Такое разделение прозрачно: логика не смешивается со стилями, стили не зависят от JavaScript, а вёрстка остаётся читаемой и переиспользуемой.&lt;/p&gt;
&lt;p&gt;На этом описание стека завершено: контракт — Servant, транспорт — Warp, хранение — SQLite, логика интерфейса — Elm, представление — W3.CSS. Но у этой истории есть второй слой. Все перечисленные инструменты входят в учебную программу ИТИ, и это не совпадение: один и тот же принцип выбора действует и в рабочих проектах, и в курсе.&lt;/p&gt;
&lt;h2 id=&#34;как-учебный-предмет-становится-производственным-инструментом&#34;&gt;Как учебный предмет становится производственным инструментом
&lt;/h2&gt;&lt;p&gt;Принцип этот звучит так: инструмент, попадающий в курс, по возможности не должен существовать только в курсе. Он либо уже используется в рабочих задачах, либо входит в них к моменту, когда студенты начинают его осваивать. Разрыв между «учебным» и «производственным» в таком случае часто не успевает возникнуть — чаще всего это одна и та же среда.&lt;/p&gt;
&lt;p&gt;Мы стараемся преподавать то, на чём работаем, — а если рабочий инструмент меняется, за ним следует и курс. Не «академический язык ради понимания типов», а Haskell, на котором собран бэкенд форума. Не «знакомство с функциональным фронтендом», а Elm, на котором собирается форма регистрации. Не «обзор систем виртуализации», а OpenNebula, которая обслуживает и учебный процесс, и наши собственные проекты, и IT-инфраструктуру. Студент видит не абстрактный пример из методички, а код, который завтра пойдёт в продакшен, — и это меняет отношение к предмету сильнее многих слов о важности образования.&lt;/p&gt;
&lt;p&gt;Мы ошибаемся. Наступаем на грабли. Что-то переделываем, что-то выбрасываем, что-то осваиваем с нуля — и делаем это не за закрытой дверью, а вместе со студентами. Программа дисциплины пересматривается каждый год не потому, что так положено, а потому, что за год меняются задачи, инструменты и сами подходы. Кто-то из студентов видит, как рождается решение, которое потом уходит в реальный сервис, — и это не «стажировка после диплома», а обычный учебный процесс. Мы узнаём новое вместе с ними: не «сначала выучим, потом применим», а «разбираемся, потому что это нужно прямо сейчас». Теория перестаёт быть теорией в тот момент, когда её результат виден на экране и им пользуются люди. Именно это, а не количество часов в учебном плане, делает предмет производственным инструментом.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Peter Norvig, «Design Patterns in Dynamic Languages» (1996). — &lt;a class=&#34;link&#34; href=&#34;https://norvig.com/design-patterns/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://norvig.com/design-patterns/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;HttpArena — независимые замеры Haskell-серверов, Alpha Round. — &lt;a class=&#34;link&#34; href=&#34;https://www.http-arena.com/frameworks/lang/haskell/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://www.http-arena.com/frameworks/lang/haskell/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;Debian: mighttpd2 — package description. — &lt;a class=&#34;link&#34; href=&#34;https://packages.debian.org/forky/mighttpd2&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://packages.debian.org/forky/mighttpd2&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;Generating Javascript functions to query an API — документация Servant, &lt;a class=&#34;link&#34; href=&#34;https://docs.servant.dev/en/latest/tutorial/Javascript.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://docs.servant.dev/en/latest/tutorial/Javascript.html&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;SQLite — встраиваемый движок базы данных. — &lt;a class=&#34;link&#34; href=&#34;https://www.sqlite.org/whentouse.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://www.sqlite.org/whentouse.html&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:5&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;SQLite: соотношение тестового кода к коду библиотеки 590:1 (для версии 3.42.0), 100% покрытие ветвлений и 100% MC/DC (набор TH3). — &lt;a class=&#34;link&#34; href=&#34;https://www.sqlite.org/testing.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://www.sqlite.org/testing.html&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:6&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref1:6&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;SQLite: четыре независимых набора тестов (TCL, TH3, SQL Logic Test, dbsqlfuzz); soak-тест TH3 перед релизом — около 248,5 млн тестов; SLT — сравнение с PostgreSQL, MySQL, SQL Server и Oracle. — &lt;a class=&#34;link&#34; href=&#34;https://www.sqlite.org/testing.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://www.sqlite.org/testing.html&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:7&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref1:7&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:8&#34;&gt;
&lt;p&gt;SQLite: SQL Logic Test — 7,2 млн запросов на 1,12 ГБ данных. — &lt;a class=&#34;link&#34; href=&#34;https://www.sqlite.org/testing.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://www.sqlite.org/testing.html&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:8&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:9&#34;&gt;
&lt;p&gt;Elm — функциональный язык для фронтенда; вывод типов, «No runtime exceptions in practice». — &lt;a class=&#34;link&#34; href=&#34;https://elm-lang.org/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://elm-lang.org/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:9&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref1:9&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref2:9&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:10&#34;&gt;
&lt;p&gt;GHC WebAssembly backend. — &lt;a class=&#34;link&#34; href=&#34;https://ghc.gitlab.haskell.org/ghc/doc/users_guide/wasm.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://ghc.gitlab.haskell.org/ghc/doc/users_guide/wasm.html&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:10&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:11&#34;&gt;
&lt;p&gt;Miso — фреймворк для Haskell с компиляцией в JavaScript и WebAssembly. — &lt;a class=&#34;link&#34; href=&#34;https://hackage.haskell.org/package/miso&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://hackage.haskell.org/package/miso&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:11&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:12&#34;&gt;
&lt;p&gt;W3.CSS — документация: адаптивная сетка (W3.CSS 12-Columns), типографика в духе Material Design (W3.CSS Material). — &lt;a class=&#34;link&#34; href=&#34;https://www.w3schools.com/w3css/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://www.w3schools.com/w3css/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:12&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
        </item>
        
    </channel>
</rss>
