惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
K
Kaspersky official blog
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
Recorded Future
Recorded Future
Engineering at Meta
Engineering at Meta
U
Unit 42
D
Docker
I
InfoQ
D
DataBreaches.Net
Google DeepMind News
Google DeepMind News
N
Netflix TechBlog - Medium
C
Check Point Blog
The Cloudflare Blog
美团技术团队
V
Vulnerabilities – Threatpost
博客园_首页
T
Threat Research - Cisco Blogs
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
A
Arctic Wolf
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Troy Hunt's Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
人人都是产品经理
人人都是产品经理
C
Cyber Attacks, Cyber Crime and Cyber Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Schneier on Security
Apple Machine Learning Research
Apple Machine Learning Research
MyScale Blog
MyScale Blog
P
Privacy International News Feed
云风的 BLOG
云风的 BLOG
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
The Blog of Author Tim Ferriss
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
The Register - Security
The Register - Security
博客园 - 三生石上(FineUI控件)
Forbes - Security
Forbes - Security
NISL@THU
NISL@THU
Y
Y Combinator Blog
T
Threatpost
Microsoft Azure Blog
Microsoft Azure Blog
L
Lohrmann on Cybersecurity

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
8. Проектирование с помощью типов: Заключение
markshevchen · 2026-05-12 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение8 мин

Охват и читатели3

Туториал

Перевод

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

  • Разбиение больших структур на маленькие «атомарные» компоненты.

  • Использование одновариантных объединений для добавления семантического значения и валидации к типам предметной области, наподобие EmailAddress и ZipCode.

  • Понимание, что система типов может представлять только корректные данные («делаем недопустимые состояния не представимыми»).

  • Использование типов как инструмента анализа для выявления скрытых требований.

  • Замена флагов и перечислений простыми конечными автоматами.

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

В последнем посте цикла, давайте рассмотрим их вместе.

Код «до»

Оригинальный пример, с которого мы начали в первом посте цикла:

type Contact =
    {
    FirstName: string;
    MiddleInitial: string;
    LastName: string;

    EmailAddress: string;
    //true, если электронный адрес подтверждён
    IsEmailVerified: bool;

    Address1: string;
    Address2: string;
    City: string;
    State: string;
    Zip: string;
    //true, если проверен с помощью сервиса проверки адресов
    IsAddressValid: bool;
    }

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

Код «после»

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

// ========================================
// WrappedString
// ========================================

/// Общий код для обёрток над строками
module WrappedString =

    /// Интерфейс, который поддерживают все обёртки над строками
    type IWrappedString =
        abstract Value : string

    /// Создаёт опциональное завёрнутое значение
    /// 1) Приводит входные данные к каноническому виду
    /// 2) Если валидация прошла, возвращает Some результата конструктора
    /// 3) Если валидация не прошла, возвращает None
    /// Значения null не считаются валидными.
    let create canonicalize isValid ctor (s:string) =
        if s = null
        then None
        else
            let s' = canonicalize s
            if isValid s'
            then Some (ctor s')
            else None

    /// Применяет функцию к завёрнутому значению
    let apply f (s:IWrappedString) =
        s.Value |> f

    /// Возвращает завёрнутое значение
    let value s = apply id s

    /// Равенство
    let equals left right =
        (value left) = (value right)

    /// Сравнение
    let compareTo left right =
        (value left).CompareTo (value right)

    /// Приводит строку к каноническому виду перед вызовом конструктора
    /// * конвертирует все пробельные символы в пробелы
    /// * обрезает слева и справа
    let singleLineTrimmed s =
        System.Text.RegularExpressions.Regex.Replace(s,"\s"," ").Trim()

    /// Функция валиадации на основе длины строки
    let lengthValidator len (s:string) =
        s.Length <= len

    /// Строка длины 100
    type String100 = String100 of string with
        interface IWrappedString with
            member this.Value = let (String100 s) = this in s

    /// Конструктор строк длины 100
    let string100 = create singleLineTrimmed (lengthValidator 100) String100

    /// Конвертирует обёртку над строками в строку длины 100
    let convertTo100 s = apply string100 s

    /// Строка длины 50
    type String50 = String50 of string with
        interface IWrappedString with
            member this.Value = let (String50 s) = this in s

    /// Конструктор строк длины 50
    let string50 = create singleLineTrimmed (lengthValidator 50)  String50

    /// Конвертирует обёртку над строками в строку длины 50
    let convertTo50 s = apply string50 s

    /// Вспомогательные функции для словарей
    let mapAdd k v map =
        Map.add (value k) v map

    let mapContainsKey k map =
        Map.containsKey (value k) map

    let mapTryFind k map =
        Map.tryFind (value k) map

// ========================================
// Электронные адреса (код, не специфичный для приложения)
// ========================================

module EmailAddress =

    type T = EmailAddress of string with
        interface WrappedString.IWrappedString with
            member this.Value = let (EmailAddress s) = this in s

    let create =
        let canonicalize = WrappedString.singleLineTrimmed
        let isValid s =
            (WrappedString.lengthValidator 100 s) &&
            System.Text.RegularExpressions.Regex.IsMatch(s,@"^\S+@\S+\.\S+$")
        WrappedString.create canonicalize isValid EmailAddress

    /// Конвертиует любую обёртку над строками в EmailAddress
    let convert s = WrappedString.apply create s

// ========================================
// Почтовый индекс (код, не специфичный для приложения)
// ========================================

module ZipCode =

    type T = ZipCode of string with
        interface WrappedString.IWrappedString with
            member this.Value = let (ZipCode s) = this in s

    let create =
        let canonicalize = WrappedString.singleLineTrimmed
        let isValid s =
            System.Text.RegularExpressions.Regex.IsMatch(s,@"^\d{5}$")
        WrappedString.create canonicalize isValid ZipCode

    /// Конвертиует любую обёртку над строками в ZipCode
    let convert s = WrappedString.apply create s

// ========================================
// Код штата (код, не специфичный для приложения)
// ========================================

module StateCode =

    type T = StateCode  of string with
        interface WrappedString.IWrappedString with
            member this.Value = let (StateCode  s) = this in s

    let create =
        let canonicalize = WrappedString.singleLineTrimmed
        let stateCodes = ["AZ";"CA";"NY"] // и т. д.
        let isValid s =
            stateCodes |> List.exists ((=) s)

        WrappedString.create canonicalize isValid StateCode

    /// Конвертиует любую обёртку над строками в StateCode
    let convert s = WrappedString.apply create s

// ========================================
// Почтовый адрес (код, не специфичный для приложения)
// ========================================

module PostalAddress =

    type USPostalAddress =
        {
        Address1: WrappedString.String50;
        Address2: WrappedString.String50;
        City: WrappedString.String50;
        State: StateCode.T;
        Zip: ZipCode.T;
        }

    type UKPostalAddress =
        {
        Address1: WrappedString.String50;
        Address2: WrappedString.String50;
        Town: WrappedString.String50;
        PostCode: WrappedString.String50;   // доделать
        }

    type GenericPostalAddress =
        {
        Address1: WrappedString.String50;
        Address2: WrappedString.String50;
        Address3: WrappedString.String50;
        Address4: WrappedString.String50;
        Address5: WrappedString.String50;
        }

    type T =
        | USPostalAddress of USPostalAddress
        | UKPostalAddress of UKPostalAddress
        | GenericPostalAddress of GenericPostalAddress

// ========================================
// Личное имя (код, не специфичный для приложения)
// ========================================

module PersonalName =
    open WrappedString

    type T =
        {
        FirstName: String50;
        MiddleName: String50 option;
        LastName: String100;
        }

    /// Создаёт новое значение
    let create first middle last =
        match (string50 first),(string100 last) with
        | Some f, Some l ->
            Some {
                FirstName = f;
                MiddleName = (string50 middle)
                LastName = l;
                }
        | _ ->
            None

    /// Склеивает вместе имя и фамилию
    /// и возвращает простую строку
    let fullNameRaw personalName =
        let f = personalName.FirstName |> value
        let l = personalName.LastName |> value
        let names =
            match personalName.MiddleName with
            | None -> [| f; l |]
            | Some middle -> [| f; (value middle); l |]
        System.String.Join(" ", names)

    /// Склеивает вместе имя и фамилию
    /// и возвращает None, если слишком длинная строка
let fullNameOption personalName =
        personalName |> fullNameRaw |> string100

    /// Склеивает вместе имя и фамилию
    /// и обрезает слишком длинную строку
    let fullNameTruncated personalName =
        // вспомогательная функция
        let left n (s:string) =
            if (s.Length > n)
            then s.Substring(0,n)
            else s

        personalName
        |> fullNameRaw  // склеиваем
        |> left 100     // обрезаем
        |> string100    // заворачиваем
        |> Option.get   // всё вместе даёт результат без ошибок

А теперь — типы, специфичные для приложения.


// ========================================
// EmailContactInfo -- конечный автомат
// ========================================

module EmailContactInfo =
    open System

    // UnverifiedData = просто EmailAddress
    type UnverifiedData = EmailAddress.T

    // VerifiedData = EmailAddress плюс дата/время проверки
    type VerifiedData = EmailAddress.T * DateTime

    // множество состояний
    type T =
        | UnverifiedState of UnverifiedData
        | VerifiedState of VerifiedData

    let create email =
        // непроверенный при создании
        UnverifiedState email

    // обработать событие "проверен"
    let verified emailContactInfo dateVerified =
        match emailContactInfo with
        | UnverifiedState email ->
            // конструируем новый объект в проверенном состоянии
            VerifiedState (email, dateVerified)
        | VerifiedState _ ->
            // игнорируем
            emailContactInfo

    let sendVerificationEmail emailContactInfo =
        match emailContactInfo with
        | UnverifiedState email ->
            // отправляем письмо
            printfn "отправка письма"
        | VerifiedState _ ->
            // ничего не делаем
            ()

    let sendPasswordReset emailContactInfo =
        match emailContactInfo with
        | UnverifiedState email ->
            // игнорируем
            ()
        | VerifiedState _ ->
            printfn "отправка запроса за сброс пароля"

// ========================================
// PostalContactInfo -- конечный автомат
// ========================================

module PostalContactInfo =
    open System

    // InvalidData = просто PostalAddress
    type InvalidData = PostalAddress.T

    // ValidData = PostalAddress плюс дата/время валидации
    type ValidData = PostalAddress.T * DateTime

    // множество состояний
    type T =
        | InvalidState of InvalidData
        | ValidState of ValidData

    let create address =
        // непроверенный при создании
        InvalidState address

    // обрабатываем событие "проверен"
    let validated postalContactInfo dateValidated =
        match postalContactInfo with
        | InvalidState address ->
            // конструируем новый объект в проверенном состоянии
            ValidState (address, dateValidated)
        | ValidState _ ->
            // игнорируем
            postalContactInfo

    let contactValidationService postalContactInfo =
        let dateIsTooLongAgo (d:DateTime) =
            d < DateTime.Today.AddYears(-1)

        match postalContactInfo with
        | InvalidState address ->
            printfn "соединяемся с сервисом проверки адресов"
        | ValidState (address,date) when date |> dateIsTooLongAgo  ->
            printfn "последняя проверка была слишком давно"
            printfn "снова соединяемся с сервисом проверки адресов"
        | ValidState  _ ->
            printfn "недавно проверен, ничего не делаем"

// ========================================
// ContactMethod и Contact
// ========================================

type ContactMethod =
    | Email of EmailContactInfo.T
    | PostalAddress of PostalContactInfo.T

type Contact =
    {
    Name: PersonalName.T;
    PrimaryContactMethod: ContactMethod;
    SecondaryContactMethods: ContactMethod list;
    }

Заключение

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

Я думаю, ответ — да. И вот почему:

Новый код явно выражает намерения разработчика

Структура данных была «тупой», а бизнес-правила — неявными. Все шансы получить множество коварных ошибок, которые даже не проявятся в модульных тестах. (Вы уверены, что приложение сбрасывает флаг IsEmailVerified везде, где меняется электронный адрес?)

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

Новый код не разрешает откладывать обработку ошибок

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

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

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

Огромное преимущество нового кода в том, что в нём гораздо меньше ошибок. Даже без написания модульных тестов я уверен, что имя при записи в базу не будет усечено до varchar(50), и что я — даже случайно — не отправлю два раза письмо для проверки адреса.

Что касается самого кода, в нём нет многого из того, что вам, как разработчику, приходится помнить. Ни проверок на null, ни приведения типов, ни беспокойства о том, что должно быть в ветке default оператора switch. И если вам нравиться использовать цикломатическую сложность, как метрику качества кода, обратите внимание, что в новом коде всего три оператора if на 350 с лишним строк.

Слова предостережения…

Будьте осторожны! Стиль проектирования, основанный на типах, окажет на вас коварное воздействие. Всякий раз, когда вы встретите недостаточно типизированный код, у вас будет возникать паранойя. (Какой точно длины должен быть электронный адрес?) Вы не сможете написать простейший скрипт на Python, не испытывая беспокойства. После этого вы станете полноправным членом культа. Добро пожаловать!

Если вам понравился этот цикл, возможно, посмотрите мой доклад