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

推荐订阅源

美团技术团队
T
The Blog of Author Tim Ferriss
C
Check Point Blog
博客园_首页
J
Java Code Geeks
云风的 BLOG
云风的 BLOG
L
LangChain Blog
小众软件
小众软件
Stack Overflow Blog
Stack Overflow Blog
爱范儿
爱范儿
Vercel News
Vercel News
博客园 - Franky
V
V2EX
IT之家
IT之家
U
Unit 42
N
Netflix TechBlog - Medium
腾讯CDC
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
博客园 - 叶小钗
H
Help Net Security
V
Visual Studio Blog
GbyAI
GbyAI

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания
3. Проектирование с помощью типов: Делаем недопустимые со...
markshevchen · 2026-04-23 · via Все публикации подряд на Хабре

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

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

Охват и читатели1.6K

Туториал

Перевод

В этом посте мы познакомимся с ключевым преимуществом F#, который использует систему типов, чтобы «сделать недопустимые состояния непредставимыми» (фраза позаимствована у Ярона Мински).

В прошлом посте мы упростили тип Contact благодаря рефакторингу. Теперь он выглядит так:

type Contact =
    {
    Name: Name;
    EmailContactInfo: EmailContactInfo;
    PostalContactInfo: PostalContactInfo;
    }

Теперь представим, что у нас есть простое бизнес-правило: «Контакт должен иметь электронный адрес или почтовый адрес». Соответствует ли наш тип этому правилу?

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

Решение кажется очевидным — сделать адреса опциональными:

type Contact =
    {
    Name: PersonalName;
    EmailContactInfo: EmailContactInfo option;
    PostalContactInfo: PostalContactInfo option;
    }

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

Так какое решение правильное?

Делаем недопустимые состояния непредставимыми

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

  • У контакта есть только электронный адрес

  • У контакта есть только почтовый адрес

  • У контакта есть и электронный, и почтовый адреса

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

type ContactInfo =
    | EmailOnly of EmailContactInfo
    | PostOnly of PostalContactInfo
    | EmailAndPost of EmailContactInfo * PostalContactInfo

type Contact =
    {
    Name: Name;
    ContactInfo: ContactInfo;
    }

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

Для варианта «электронный и почтовый адреса» я пока использую обычный тип-кортеж. Его вполне хватает.

Конструируем ContactInfo

Теперь разберёмся, как использовать наш тип на практике. Начнём с создания контакта:

let contactFromEmail name emailStr =
    let emailOpt = EmailAddress.create emailStr
    // обрабатываем варианты правильного и неправильного электронного адреса
    match emailOpt with
    | Some email ->
        let emailContactInfo =
            {EmailAddress=email; IsEmailVerified=false}
        let contactInfo = EmailOnly emailContactInfo
        Some {Name=name; ContactInfo=contactInfo}
    | None -> None

let name = {FirstName = "A"; MiddleInitial=None; LastName="Smith"}
let contactOpt = contactFromEmail name "abc@example.com"

Здесь мы написали простую вспомогательную функцию conctactFromEmail, чтобы создать новый контакт из имени и электронного адреса. Однако, электронный адрес может быть неправильным, так функция умеет обрабатывать ошибку. Поэтому возвращаемый тип — это Contact option, а не просто Contact.

Обновляем ContactInfo

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

  • Если контакт содержал только электронный адрес, добавляем почтовый адрес. Возвращаем вариант EmailAndPost.

  • Если контакт содержал только почтовый адрес, заменяем этот адрес на новый. Возвращаем вариант PostOnly.

  • Если контакт содержал и электронный, и почтовый адреса, заменяем почтовый адрес на новый. Возвращаем вариант EmailAndPost.

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

let updatePostalAddress contact newPostalAddress =
    let {Name=name; ContactInfo=contactInfo} = contact
    let newContactInfo =
        match contactInfo with
        | EmailOnly email ->
            EmailAndPost (email,newPostalAddress)
        | PostOnly _ -> // игнорируем текущий адрес
            PostOnly newPostalAddress
        | EmailAndPost (email,_) -> // игнорируем текущий адрес
            EmailAndPost (email,newPostalAddress)
    // создаём новый контакт
    {Name=name; ContactInfo=newContactInfo}

А вот пример использования:

let contact = contactOpt.Value   // см. предупреждение об option.Value ниже
let newPostalAddress =
    let state = StateCode.create "CA"
    let zip = ZipCode.create "97210"
    {
        Address =
            {
            Address1= "123 Main";
            Address2="";
            City="Beverly Hills";
            State=state.Value; // см. предупреждение об option.Value ниже
            Zip=zip.Value;     // см. предупреждение об option.Value ниже
            };
        IsAddressValid=false
    }
let newContact = updatePostalAddress contact newPostalAddress

ПРЕДУПРЕЖДЕНИЕ: я использую option.Value, чтобы извлечь содержимое опционального типа. Так можно делать в учебном коде, но категорически нельзя в продуктовом! Работая с опциональным типом всегда применяйте сопоставление с образцом.

Зачем вообще создавать такие сложные типы?

Возможно, сейчас вы думаете, что я всё неоправданно усложнил. Отвечу вам так:

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

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

type ContactInfo =
    | EmailOnly of EmailContactInfo
    | PostOnly of PostalContactInfo
    | EmailAndPost of EmailContactInfo * PostalContactInfo

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

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