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

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

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

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

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
{
    bool Equals(T other);
}

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

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

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

    public int CompareTo(object obj) {}
    public bool Equals(int other) {}
}

Напоминание

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

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

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

Принцип подстановки Барбары Лисков (буква 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/

© Habrahabr.ru