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

Сначала вспомним главное определение:

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

1. Множество целых чисел: {1, 2, 3, 4}

2. Множество гласных букв русского алфавита: {А, Е, И, О, У, Ы, Э, Ю, Я}

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

Давайте разберемся чуть подробнее.
Примеры буду приводить на языке C#, однако их можно воспринимать как псевдо-код

Целочисленные типы как множества чисел

Рассмотрим целочисленные типы.
Мы можем представлять их как множества чисел с определёнными диапазонами:

Напоминание

x ∈ ℤ следует понимать как "x принадлежит множеству целых чисел"

  • sbyte: {x ∈ ℤ | -128 ≤ x ≤ 127}

  • short (Int16): {x ∈ ℤ | -32,768 ≤ x ≤ 32,767}

  • int (Int32): {x ∈ ℤ | -2,147,483,648 ≤ x ≤ 2,147,483,647}

  • long (Int64): {x ∈ ℤ | -9,223,372,036,854,775,808 ≤ x ≤ 9,223,372,036,854,775,807}

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

Отношения подмножеств и преобразование типов

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

// Безопасное приведение от short к int: каждый элемент множества "short" 
// также является допустимым элементом для множества "int".

short s = 1000;
int i = s; 

Однако обратное не всегда верно:

// Небезопасное приведение: требует явного приведения типов и может привести 
// к потере данных, поскольку не каждый элемент множества "int" 
// является элементом множества "short".

int i = 40000;
short s = (short)i; 
Целочисленные типы как множества чисел наглядно
Целочисленные типы как множества чисел наглядно

Тип bool как множество

Тип bool в C# можно рассматривать как множество с ровно двумя элементами:

  • bool: {true, false}

Типы перечислений как конечные множества

Перечисления в C# — отличные примеры конечных множеств. Например:

enum DaysOfWeek
{
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

Перечисление DaysOfWeek можно рассматривать как множество: {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}.

Сложные типы как множества

Давайте посмотрим, как можно воспринимать и более сложные типы как множества.

Пример с транспортом

Перед нам иерархия типов транспорта:

class Vehicle
{
    public string Make { get; set; }
    public string Model { get; set; }
}

class Car : Vehicle
{
    public int NumberOfDoors { get; set; }
}

class AudiCar : Car
{
    public string AudiSpecificFeature { get; set; }
}

Посмотрим на эти типы как на множества:

  • Vehicle: Множество всех возможных транспортных средств с маркой и моделью.

  • Car: Подмножество Vehicle включающее все транспортные средства с дверьми.

  • AudiCar: Подмножество Car включающее все автомобили с особенностями, характерными для Audi.

Напоминание

A ⊂ B следует понимать как "множество A является подмножеством B".

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

  • Vehicle = {x | x имеет марку и модель}

  • Car = {x ∈ Vehicle | x имеет двери}

  • AudiCar = {x ∈ Car | x имеет особенности, характерные для Audi}

Можно заключить, что AudiCar ⊂ Car ⊂ Vehicle.

Пример с электронными устройствами

Рассмотрим иерархию электронных устройств:

class ElectronicDevice 
{ 
  public string PowerOn() {} 
} 

class Smartphone : ElectronicDevice 
{ 
  public string MakeCall() {} 
  public string SendText() {}
} 

class DualCameraSmartphone : Smartphone 
{ 
  public string TakePhoto() { } 
}

Мы также можем рассматривать эти типы как множества:

  • ElectronicDevice: Множество всех возможных устройств, которые могут включаться.

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

  • DualCameraSmartphone: Подмножество Smartphone, включающее все смартфоны с возможностью делать качественные фотографии с использованием двойной камеры.

В терминах множеств:

  • ElectronicDevice = {x | x может быть включен}

  • Smartphone = {x ∈ ElectronicDevice| x может звонить и отправлять сообщения}

  • DualCameraSmartphone = {x ∈ Smartphone | x может делать фото}

Соответственно, DualCameraSmartphone ⊂ Smartphone ⊂ ElectronicDevice.

Пример с интерфейсами

Интерфейсы также могут быть рассмотрены с точки зрения множеств.
Например:

interface IComparable
{
    int CompareTo(object obj);
}

interface IEquatable<T>
{
    bool Equals(T other);
}

Мы можем представить интерфейс IComparable как множество всех объектов, которые имеют метод CompareTo(object obj), а интерфейс IEquatable<T> — как множество всех объектов, которые имеют метод Equals(T other).

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

class CompareableInt : IComparable, IEquatable<int>
{
    public int Value { get; set; }

    public int CompareTo(object obj) {}
    public bool Equals(int other) {}
}
Напоминание

A ∩ B следует понимать как "пересечение множеств A и B".

В терминах множеств, CompareableInt принадлежит к IComparable ∩ IEquatable<int>.

Принцип подстановки Барбары Лисков

Принцип подстановки Барбары Лисков (буква L из SOLID) является фундаментальным принципом объектно-ориентированного программирования, который тесно связан с нашим представлением о типах как о множествах.

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

С точки зрения множеств, это означает, что каждый элемент множества A должен вести себя так, как ожидается от элементов более широкого множества B, если A ⊂ B.

Рассмотрим знаменитый пример нарушения этого принципа:

class Rectangle 
{ 
  public virtual int Width { get; set; } 
  public virtual int Height { get; set; } 
  public int Area() => Width * Height; }
}

class Square : Rectangle 
{ 
  public override int Width 
    { set { base.Width = base.Height = value; } } 
  public override int Height 
    { set { base.Width = base.Height = value; } } 
}

Изюминка:
Вот здесь наш Debug.Assert() будет вести себя по разному в зависимости от того, объект какого типа на самом деле был передан в метод - Rectangle или Square.

void IncreaseWidth(Rectangle rectangle) 
{ 
  int originalHeight = rectangle.Height; 
  rectangle.Width += 1; 
  Debug.Assert(rectangle.Height == originalHeight); // Для квадрата это будет неверно.
}

Чтобы соблюдать LSP, нам нужно гарантировать, что все операции, которые верны для элементов множества Rectangle, также верны для элементов его подмножества Square.
В данном примере кода эта гарантия не соблюдается, поэтому принцип нарушен.

Чего сказать-то хотел?

Сказать хотел, что если вы, как и я, погрязли в перекладывании DTO, настройке инфраструктуры, трекинге в Jira/Asana/Whatever, бесконечных созвонах/переписках и забыли, что программирование это, вообще-то, красиво, - попробуйте посмотреть на обыденные вещи(типы, наследование, интерфейсы и т.д.) с другой, непривычной точки зрения.

Послесловие

На самом деле, есть целая область, которая в том числе покрывает и то, о чем мы сегодня говорили.

Она называется - "Теория Типов".

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

Начать можно, например, отсюда https://habr.com/ru/articles/758542/

Комментарии (19)


  1. onyxmaster
    03.10.2024 12:29
    +3

    Так и до абстрактной алгебры недалеко!


  1. RodionGork
    03.10.2024 12:29
    +1

    можно(и нужно) рассматривать как математические множества.Мысль хоть и очевидная

    почему "нужно" и, простите, с какой стороны это "очевидно"?

    наверное лучше вбрасывая такие громкие заявления как-то их мотивировать :)

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


    1. cpud47
      03.10.2024 12:29

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

      кажется сейчас от хаскеля даже в Биокаде отказались...

      А чем это так показательно?


    1. akilayd Автор
      03.10.2024 12:29

      Вы правы, звучит действительно как провокация, но таковой не является)

      Мне следовало написать можно(и нужно) в том числе рассматривать как математические множества


  1. 2128507
    03.10.2024 12:29

    чушь какая-то, извините.


  1. ImagineTables
    03.10.2024 12:29

    А память это для морлоков.


  1. Nail_S
    03.10.2024 12:29

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


    1. RodionGork
      03.10.2024 12:29

      наверное логарифмический масштаб :)


    1. akilayd Автор
      03.10.2024 12:29

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


    1. lobotomic
      03.10.2024 12:29
      +2

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


      1. akilayd Автор
        03.10.2024 12:29

        Да, ровно это и предполагалось)


      1. Nail_S
        03.10.2024 12:29

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


  1. zzzzzzerg
    03.10.2024 12:29
    +2

    Есть классическая уже книга - Типы в языках программирования - у нас делали перевод - Типы в языках программирования (tversu.ru)


  1. MasterLamaster
    03.10.2024 12:29

    Как говорил наш препод: "программист без знаний математики - это не программист, а переводчик".


  1. domix32
    03.10.2024 12:29
    +1

    Она называется - "Теория Типов".

    Кажется вы до неё так толком и не добрались. Кстати с похожим успехом можно трогать более абстрактную теорию категорий. Заодно и мем про моноид в категории эндофункторов понять.


  1. Nemoumbra
    03.10.2024 12:29

    Улыбнуло "определение" множества через якобы более простой термин "коллекция". Это ж без определения идёт обычно.)


  1. youngmysteriouslight
    03.10.2024 12:29
    +3

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

    Множество — это коллекция элементов, обладающих общим свойством, которые рассматриваются как единое целое

    В общепринятой математике множество — это (неформально говоря) объекты, рассматриваемые как один объект. На объектах определено отношение (двуместный предикат) принадлежности, то есть один объект или принадлежит второму, или нет. Если второй объект не множество — всегда нет.

    Никакое требование об общем свойстве не накладывается: множество {1, "hello", <кот Василий>} существует. Есть множества, все элементы которого имеют только то общее свойство, что они принадлежат этому множеству.

    Множества подчиняются аксиоме объёмности: если два множества состоят из одних и тех же элементов, то это одно и то же множество.

    Типы в программировании можно(и нужно) рассматривать как математические множества.

    Лучше бы так не делать, потому что типы не обладают вышеперечисленными свойствами множеств. Начнём с того, что нет предиката принадлежности объекта типу. Когда мы пишем x : T, мы не говорим «вот есть объект T, вот есть объект x; кстати, T — это тип; ах да, x принадлежит T». Мы вместо этого говорим или «вот есть выражение x и оно доказуемо по построению имеет T», или «вот переменная x, считать её по определению имеющей тип T».

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

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

    Простым примером системы типов является система цветных функций.

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

    С одной стороны, развитые системы типов (вроде Agda, Coq, HoTT) считают, что множество — это частный вид типов. Но там и множество понимает не общепринято. Поскольку я плохо разбираюсь в интуиционистском варианте теории множеств, призываю малышку @Natasha_Klaus на помощь пояснить за Coq. В HoTT множеством называется тип, у которого любые два экземпляра равны не более чем одним способом.

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

    • тип как топологическое пространство (точнее, решётка, см. работы Даны Скотт) — позволяет адекватно говорить о функции f : A → B как о непрерывном отображении из A в B, потому что не всякая функция из множества A во множество B может быть программой

    • тип как идеал — то же, но с поддержкой полиморфизма (дженериков)

    • тип как интервал — то же, но с нормальной поддержкой полиморфизма и типов-дополнений (т.е. «всё, что не является элементом типа A»)

    • тип как отношение — для бесплатных теорем, бомба


  1. meonsou
    03.10.2024 12:29

    Примеры буду приводить на языке C#, однако их можно воспринимать как псевдо-код

    Ну это вы зря на самом деле. Конкретно на C# ваши выводы вообще не имеют смысла, как и в любом языке с номинальной типизацией (коих большинство из мейнстримных).

    class Vehicle
    {
        public string Make { get; set; }
        public string Model { get; set; }
    }
    

    Vehicle = {x | x имеет марку и модель}

    Очевидно что Vehicle это не "все возможные объекты с маркой и моделью", это конкретно объекты, определённые как Vehicle, не больше и не меньше.

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


  1. Mokytex
    03.10.2024 12:29

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