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

от автора

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

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

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

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/


ссылка на оригинал статьи https://habr.com/ru/articles/847958/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *