Полиморфизм, сколько в этом слове красивого и даже таинственного. Происходит оно от греческого πολύμορφος что означает — многообразный.  В программировании это понятие встречается часто и является обыденным для понимания большинством разработчиков. Но так ли обстоят дела на самом деле? 

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

Полиморфизм — или способность объекта выполнять специализированные действия на основе его типа.

Полиморфизм — это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.

Полиморфизм — это способность объекта использовать методы производного класса, который не существует на момент создания базового.

Полиморфизм — это свойство, которое позволяет одно и тоже имя использовать для решения нескольких технически разных задач.

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

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

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

Базовые понятия

Для лучшего восприятия, начнем с определений и упрощений. Как бы не выглядели языки программирования, обычно они используется схожий подход - есть данные и есть функции которые к данным применяются. Взять к примеру ООП. Некоторые предполагают что ООП отличается от других парадигм, ведь там есть объекты а у объектов есть методы. Методы - это особые функции привязанные к объекту. На деле же ничего особого в них нет и это не более чем удобный синтаксический сахар над обычными функциями.

Например, в таких языках как Java, C#, Kotlin и т.д., this это скрытый нулевой аргумент функции который содержит указатель на объект. Часто, называемый - method receiver.

// Неявный ресивер this
public string GetName() {
  // Мы не видим суслика а он есть
   return this._name;
}
// Явный ресивер _this
public static string GetName(User _this) {
   return _this._name;
}

...
  
User user = new User();
var u1 = user.GetName();
var u2 = User.GetName(user);

Первый пример - просто синтаксический сахар, на деле по механике аналогичен второму.
Некоторые языки позволяют явно вызывать методы как функции к примеру Rust:

let a = "Hello".replace('l', "");
let b = str::replace("Hello", 'l', "");

let c = 1.add(2);
let d = i32::add(1, 2);

let user_name = "Mike".to_string();
let user = User { name: user_name };
let u1 = user.get_name();
let u2 = User::get_name(&user);

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

Function<User, String> getNameMethod = User::getNameA;
String u1 = getNameMethod.apply(user);

С этим надеюсь разобрались. Методы - это обычный синтаксический сахар над функциями.

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

Начнем с конструкторов - функций инициализирующих объект. Какие-то языки имеют особые правила и синтаксис а в каких то, наподобие Go и Rust, конструкторами называют обычные пользовательские функции для создания структуры. В любом случае сути это не меняет.

Чтобы понять почему собаки - это киты операторы - это функции, полезно затронуть тему  способов записи выражений. Они бывают:

// префиксными
// имя расположено перед аргументами
++n
inc n
inc(n)
add n m
add(n, m)
  
// инфиксными
// имя расположено между аргументами
n + m		 
n add m 

// постфиксными
// имя расположено после аргументов
n++
n inc

Все эти способы записи просто вариации синтаксиса над вызовом функции. Не смущайтесь увидев знаки + - > и т.д. - от имен обычных функций они ничем не отличаются.

Взглянем на Kotlin, где можно самому определять как инфиксные функции так и операторы:

// Пример инфиксной функции
infix fun Int.myAdd(m: Int): Int {
   return this + m;
}
// Пример определения функции как оператора
data class Point(val x: Int, val y: Int) {
   // Определяем собственный оператор + для типа Point
   operator fun plus(other: Point): Point {
       return Point(x + other.x, y + other.y)
   }
}
fun main() {
   // Вызов как функции
   var res1 = (Int::myAdd)(1, 2)
   // Вызов как метода
   val res2 = 1.myAdd(2)
   // Вызов как инфиксной функции
   var res3 = 1 myAdd 2
  
   val pointA = Point(1, 2)
   val pointB = Point(3, 3)
   // Мы определили функцию в качестве оператора и теперь можем складывать точки
   var pointC = pointA + pointB;
}

Надеюсь теперь стало понятно что оператор - это такая же функция с особым синтаксисом.

Теперь, когда  все несколько упростилось, перейдем к главному. Попробую сформулировать простое и очевидное определение для такого понятия как полиморфизм:

Полиморфизм — это свойство программных сущностей работать сходным образом с данными разных типов.

Чуть ближе к основной теме:

Мономорфная функция — это функция способная применяться к конкретному типу данных.

Полиморфная функция — это функция способная применяется к различным типам данных.

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

Если уточнить определение полиморфной функции то получим два семейства полиморфизма:

Специальная полиморфная функция — это функция которая способна применяется к различным типам данных специальным для каждого отдельного типа образом.

Это определение описывает первую группу которая называется специальным или AD HOC полиморфизмом. Специальным он называется потому что для каждого поддерживаемого типа требуется специальная реализация. 

Универсальная полиморфная функция — это функция которая способна применяется к различным типам данных одинаковым для всех них образом.

Это определение описывает универсальный полиморфизм. Суть в том чтобы написать одну функцию и работать с любыми допустимыми для нее типами.

Более простыми словами: предположим, есть библиотека с функцией Fu, определенной для типов A и B. Есть задача также работать с новым типом C.

Если для этого необходимо будет внести изменения в библиотеку то, с большей вероятностью, перед нами специальная полиморфная функция. В противном случае - универсальная.

Давайте же перейдем к делу и рассмотрим варианты полиморфизма, как специального так и универсального.

Перегрузка функций

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

Пример перегрузки функций на C#:

public virtual void Write(ulong value) { Write(value.ToString(FormatProvider)); }
public virtual void Write(float value) { Write(value.ToString(FormatProvider)); }
public virtual void Write(double value) { Write(value.ToString(FormatProvider)); }
public virtual void Write(decimal value) { Write(value.ToString(FormatProvider)); }
public virtual void Write(string? value) {
   if (value != null) { Write(value.ToCharArray()); }
}
public virtual void Write(object? value) {
   if (value != null) {
       if (value is IFormattable f){ Write(f.ToString(null, FormatProvider)); }
       else { Write(value.ToString()) };
   }
}

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

// Языки без параметров по умолчанию часто используют перегрузку так
public static void Send(string body) {
   Send(body, "default");
}
public static void Send(string body, string to) {
   Send(body, to, 100);
}
public static void Send(string body, int timeOut) {
   Send(body, "default", timeOut);
}
public static void Send(string body, string to, int timeOut) {...}


// В то время языки с поддержкой параметров по умолчанию в ней не нуждаются
public static void Send(string body, string to = "default", int timeOut = 100) {...}

Другое дело что для особых видов функций, таких как конструкторы, перегрузка иногда жизненно необходима т.к., чаще всего, нельзя давать конструкторам класса имена.

Мысли вслух

Справедливости ради, перегрузка конструктора это не всегда хорошая идея. Часто правильней закрыть прямой доступ к конструктору  и создавать экземпляр типа с помощью специальных функций. Яркий пример в Java DateTime API.
В классе LocalDateимеется множество методов создания экземпляра типа:now(), of(), from(), parse()etc. Это намного удобнее чем выбирать 1 из 20 перегрузок конструктора. Как в плане поддержки так и использования.

Где перегрузка действительно показывает свою пользу и потенциал - это в работе с операторами. Взгляните на Java, где от перегрузки операторов намеренно отказались:

Vector3 complex = a.add(b).subtract(new Vector3(1, 1, 1)).multiply(0.5);

и как тоже самое будет выглядеть на C#

Vector3 complex = (a + b - new Vector3(1, 1, 1)) * 0.5;

ну и любимое

BigDecimal result = (amount1.add(amount2)).multiply(BigDecimal.ONE.add(rate)).divide(new BigDecimal("2"), 10, RoundingMode.HALF_UP).subtract(correction);

против

decimal result = ((amount1 + amount2) * (1 + rate) / 2) - correction;

Пример на Kotlin. Операторы аналогичные им обычные функции:

dateB > dateA                     // dateB.isAfter(dateA)
dateA == dateB                    // dateA.isEqual(dateB)
dateA < dateB                     // dateA.isBefore(dateB)
dateC in dateA..dateB             // dateA.rangeTo(dateB).contains(dateC)


listOf(1, 2, 3) + listOf(4, 5, 6) // listOf(1, 2, 3).plus(listOf(4, 5, 6))
listOf(1, 2, 3) + 4               // listOf(1, 2, 3).plus(4)
1 in listOf(1, 2, 3)              // listOf(1, 2, 3).contains(1)


map["One"]                        // map.get("One")
"One" in map                      // map.containsKey("One")

Пример наглядно демонстрирует как правильное использование операторов делают код минималистичнее и проще для восприятия.

Приведение типов

Начнем с простого и безобидного:

public static long Calculate(long n) {
   return n + 100 * 2;
}

Функция вроде бы принимает тип long но на деле многие языки могут производить неявные преобразования:

byte b = 100;
short s = 100;
int i = 100;
long r1 = Calculate(b);
long r2 = Calculate(s);
long r3 = Calculate(i);

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

Немного боли

Да да, привет Rust. Привет as usize.

Некоторые языки позволяют пойти дальше и описывать неявные приведения одних типов к другим. К примеру, есть функция:

public static Connection Connect(Address address) {...}

Для того чтобы получить соединение мы сделаем следующее:

var connection = Connect(new Address("192.168.1.1", 8453));

Адрес в формате строки “192.168.1.1:8453” придется сначала парсить и затем создавать объект Address. Можно конечно написать перегрузку но делать это придется для каждой функции принимающей Address. Альтернативный вариант - использовать неявное приведение типа:

public struct Address {
  
   public string Host { get; init; }
   public int Port { get; init; }
  
   public Address(string host, int port) {
       Host = host;
       Port = port;
   }
   public static implicit operator Address(string address) {
       Validator.Validate(address);
       var pair = address.Split(":");
       return new Address(pair[0], Convert.ToInt32(pair[1]));
   }
}

Ключевое слово implicit определяет оператор неявного преобразования. Тип string, переданный в функцию где требуется Address, будет неявно преобразован к нему с помощью данной функции.

var connection1 = Connect(new Address("192.168.1.1", 8453));
var connection2 = Connect("192.168.1.1:8453");

Любая функция требующая Address будет полиморфна по этому параметру так как сможет принимать любой тип для которого Address реализовал неявное преобразование.

Algebraic data types

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

Тип данных представляет из себя множество допустимых значений. Например логический тип это два возможных варианта или true или false представляющих множество
{ true | false }. Если спрятать такой тип за nullable ссылкой то можно получить уже 3 возможных значения { true | false | null }. Целочисленный 32 битный тип это уже 4 294 967 296 допустимых значений { 2,147,483,648 |…| -1 | 0 | 1 | .. |  2,147,483,647 }. Языки программирования не часто дают возможность создавать собственные типы путем перечисления множества допустимых вариантов. Чаще всего можно встретить перечисления констант:

public enum DayOfWeek {
 Sunday,
 Monday,
 Tuesday,
 Wednesday,
 Thursday,
 Friday,
 Saturday,
}

Поддержка возможности создавать новый тип и определять множество допустимых для него значений на основе других типов это и есть поддержка алгебраических типов данных. Примеры таких языков: Haskell, Rust, F# и TypeScript. Что характерно, возможность принимать тип определенный множеством других типов делают функцию полиморфной. Начнем с простого, создадим в Rust тип определяющий дни недели как в примере выше:

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

Отличий немного. Рассмотрим что-нибудь поинтереснее. Например представление типа Option в стандартной библиотеке Rust:

pub enum Option<T> {
    None,
    Some(T),
}

Все предельно просто. У типа Option два допустимых варианта: либо значения просто нет, либо оно есть и хранится внутри Some.

fn show(n: &Option<i32>) {
    match n {
        Some(value) => println!("Value is {value}"),
        None => println!("Value is not present"),
    }
}

Данная функция предельно проста - она распаковывает n и печатает значение в случаи наличия или пишет о том что значение не представлено.

Может показаться что Optional в Java - это тоже самое, но на деле это скорей имитация такого поведения и это станет понятно в следующем примере:

Определение типов
struct Article {
    id: u32,
    text: String,
    author_id: u32,
}
enum SearchCriteria {
    ById(u32),
    ByTextPart(String),
}


enum SearchType {
    FindOne,
    FindMany { page: u32, size: u32 },
}


enum SearchResult {
    FoundOne(Article),
    FoundMany(Vec<Article>),
    NotFound,
    Error(String),
}


enum DeletingResult {
    Success,
    NotFound,
    Error(String),
}
enum ArticleRequest {
    Create {
        text: String,
    },
    Update {
        article_id: u32,
        new_text: String,
    },
    Delete {
        article_id: u32,
    },
    Find {
        search_type: SearchType,
        criteria: SearchCriteria,
    },
    SyncCatalog,
}

enum ArticleResponse {
    CreatedInfo(Article),
    UnableToCreate(String),
    UnableToUpdate(String),
    Deleted(DeletingResult),
    Searched(SearchResult),
    Synced,
    UnexpectedError(String),
}

У нас есть тип ArticleRequest определяющий все варианты выполнения действий со статьями и есть тип ArticleResponse для всех возможных вариантов результатов обработки запроса. Настало время для полиморфной функции:

fn perform(operation: ArticleRequest) -> ArticleResponse {
    match operation {
        ArticleRequest::Create { text } => create(text),
        ArticleRequest::Update {
            article_id,
            new_text,
        } => update(article_id, new_text),
        ArticleRequest::Delete { article_id } => delete(article_id),
        ArticleRequest::SyncCatalog => sync(),
        ArticleRequest::Find {
            search_type,
            criteria,
        } => find(search_type, criteria),
    }
}

Функция perform способна принять любой из вариантов типа ArticleRequest и с помощью сопоставления с образцом выбирать подходящую для обработки логику.  

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

public string HandleResponse(IResponse response) {
   if (response is Successful successful) {
       return successful.Body;
   }
   if (response is Error error) {
       throw new Exception(error.Message);
   }
   throw new Exception();
}

Полиморфизм подтипов

Данный, характерный для ООП языков, вид полиморфизма можно еще назвать полиморфизмом через наследование типов.

Идея наследования типов проста:

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

Приведем пример:

Смеха роста офисного планктона
Смеха роста офисного планктона

На схеме, Object предок всех типов, User наследует методы Object и добавляет свои. Так происходит на каждой ступени наследования. Многие думают что это и есть полиморфизм в представлении ООП языков. Нет, это все еще наследование.

Для реализации полиморфизма рассмотрим еще пару вещей. Первое, тип, в программировании, обычно определяет такие свойства данных как размер и способ работы. В нашем случае упростим до - какие методы доступны для объекта. Второе, в системах с наследованием типов, доступны операции приведения к родителю (upcasting) и приведение к дочернему типу (downcasting).

На изображении можно наблюдать объект с логином "ivanov" - это администратор системы. Можно сделать upcasting типа Admin до любого из его родителей. Чем выше будет тип родителя тем абстрактнее будет api нашего объекта но тем меньшим количеством методов он сможет оперировать. Например если привести объект который имел тип Admin к его родителю - User то он потеряет как возможность назначать разрешения, доступную типу Admin так и возможность работать с документами доступную типу Employee.

Другой тип приведения downcasting наоборот предполагает усиление возможностей типа за счет его уточнения. Например объект "petrov", будучи изначально Employee, приведенный к родителю User, можно привести обратно к Employee или к любому из промежуточных типов. Стоит обратить внимание что downcasting это небезопасная операция - если попытаться привести "petrov" к типу Admin, которому тот не соответствует, то произойдет ошибка.

Давайте вернемся к полиморфизму. В примере две невероятно полезные мономорфные функции, работающие с конкретными типами:

public static void PrintLogin(Visitor visitor) {
   Console.WriteLine(visitor.GetLogin());
}
public static void PrintLogin(Admin admin) {
   Console.WriteLine(admin.GetLogin());
}

Выделим два важных факта: любой потомок умеет то что умеет родитель, любой объект можно как привести к родителю так и уточнить до изначального типа. Компилятор и среда выполнения может автоматически произвести upcasting  любого дочернего объекта к необходимому родительскому типу и удовлетворить требование по типу функции. В примере выше, getLogin - это метод типа User, а это значит что если передать любого потомка User, то аргумент будет приведен к User автоматически. Попробуем:

public static void PrintLogin(User user) {
   Console.WriteLine(user.GetLogin());
}

Данная функция уже полиморфна и работает универсально с любыми типами-наследниками: Employee, Admin и Visitor.

PrintLogin(new User());
PrintLogin(new Employee());
PrintLogin(new Admin());
PrintLogin(new Visitor());

Таким незамысловатым образом устроен полиморфизм подтипов суть которого установить абстрактную границу возможностей функции и определить входные параметры по этой границе.

Главное помнить что приведение типов имеет свою цену - чем сильнее абстрагируется тип - тем гибче становится  но тем больше функциональность теряет и наоборот - чем сильнее уточняется - тип тем большую функциональность получаем в обмен на гибкость. Помните, любой upcasting разумно проводить до минимально необходимого типа но не выше.

Утиная типизация

Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка.

Смысл данного вида типизации в следующем: если тип содержит подходящие функции, этого достаточно чтобы им воспользоваться . Если функция Fu требует тип B но в типе A имеются функции идентичные по наименованию, параметрам и возвращаемому значению, то в функцию Fu можно передать тип A.  В противовес этому, в классической номинативной типизации если Fu принимает B то значит должен быть передан именно B и никак иначе.  

Возможно удивлю, но в C# есть элементы утиной типизации. К примеру, для того чтобы класс можно было  использовать в foreach, необходимо для него реализовать интерфейс IEnumerable. Это знают многие. Но не все знают что это делать необязательно. Напишем класс который перебирает ходы на шахматной доске:

class ChessBoard {
  
   private readonly char[] _files = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
   private readonly int[] _ranks = [1, 2, 3, 4, 5, 6, 7, 8];
  
   public IEnumerator<string> GetEnumerator() {
       foreach (var rank in _ranks) {
           foreach (var file in _files) {
               yield return $"{file}{rank}";
           }
       }
   }
}

Обратите внимание что класс ChessBoard не реализует IEnumerable. В C# достаточно чтобы метод GetEnumerator просто присутствовал в классе. Вот вам и утиная типизация.

Любопытно также посмотреть как это реализовано в Go:

type Logger interface {
	Log(message string)
}

type ConsoleLogger struct{}

func (c ConsoleLogger) Log(message string) {
	fmt.Println("[Console]", message)
}

type FileLogger struct {
	File *os.File
}

func (f FileLogger) Log(message string) {
	timestamp := time.Now().Format("2006-01-02 15:04:05")
	fmt.Fprintf(f.File, "[%s] %s\n", timestamp, message)
}

func Process(logger Logger) {
	logger.Log("Программа запущена")
	logger.Log("Процесс выполняется")
	logger.Log("Готово!")
}

Все ну очень похоже не полиморфизм подтипов кроме одного но - отсутствует явная привязка реализации к конкретному типу. Здесь работает принцип утиной типизации, в функцию Process можно передать все что угодно у чего есть функция Log(message string).

=\

Вроде бы Go хомяк а ведет себя как утка.

Параметрический полиморфизм

Ну вот мы и подошли к самому интересному и мощному виду полиморфизма. В народе часто называемому дженериками. Знаю что некоторые разработчики плавают в понимании этой абстракции. Попытаемся исправить. Возьмем бесполезную но показательную функцию на языке Java:

public static <T> List<T> toList(T value) {
   return List.of(value);
}

Вернемся к названию данного вида полиморфизма - параметрический.
Это ключ к пониманию главной концепции. Для обычной функции характерно наличие входящих параметров, позволяющих передавать в нее различные значения. Параметрический полиморфизм дает возможность передавать в функцию типы. Отсюда и происходит название.
Часто одна из причин непонимания неудачный синтаксис, как в Java в примере выше. Лучше все таки вернемся к C# :

public static IList<T> ToList<T>(T value) {
   return new List<T>() { value };
}

Взгляните на эту часть ToList<T>(T value). В конце привычный блок параметров 
(T value) обрамленный круглыми скобками, определяющий что функция требует передать значение типа T. А перед ними блок параметров типов <T> обрамленный угловыми скобками определяющий что функция требует передать некий тип. 

имя функции<блок параметров типов>(блок параметров значений)

Если посмотреть на вызов этой функции то все встанет на свои места:

IList<int> list = ToList<int>(1);
:\

В отличии от синтаксиса Java, который местами призван свести разработчика с ума:

public static <T> List<T> toList(T value) {
   return List.of(value);
}

...
  
List<Integer> list = SomeClass.<Integer>toList(1);

Аргумент 1 передан в блок параметров значений и аргумент-тип int в блок параметров типов.  Параметр типа T это заполняемый шаблон, передаваемый в функцию извне, условно преобразуя ее к подобному виду :

public static IList<int> ToList<int>(int value) {
   return new List<int>() { value };
}

Один из типов был вынесен как шаблон что позволило передавать различные его вариации извне.

Параметризация - это абстрагирование от конкретных типов используемых функцией позволяющая уточнять эти типы на этапе вызова.

Чтобы лучше понять идею возьмем две функции которые принимают только параметры типов:

public static IList<T> EmptyList<T>() {
   return new List<T>();
}

public static void PrintType<T>() {
   Console.WriteLine(typeof(T));
}

А вот их вызовы:

IList<int> intList = EmptyList<int>();
IList<string> strList = EmptyList<string>();

// System.Int32
PrintType<int>();
// System.String
PrintType<string>();

Вы можете условно представить что в момент вызова произошло следующее:

public static IList<int> EmptyList<int>() {
   return new List<int>();
}
public static IList<string> EmptyList<string>() {
   return new List<string>();
}
public static void PrintType<int>() {
   Console.WriteLine(typeof(int));
}
public static void PrintType<string>() {
   Console.WriteLine(typeof(string));
}

Представление и возможности параметров типов сильно зависят от конкретной реализации в языке программирования. Например в Java нельзя реализовать метод PrintType подобным образом. Но это тема для другой статьи.

Вы можете возразить что редко вообще передаете параметры типов явно и будете правы. Компиляторы, чаще всего, могут сами их вывести. Для того чтобы понять как они это делают, определим три ключевые места где параметры типов используются в объявлении функции и местах ее вызова:

Параметр типа и аргумент типа

 <T> ← <int>

Параметр значения использующий параметр типа и аргумент передаваемый в него

 (T value) ←(1)

Возвращаемое значение

IList<T> ← IList<int>

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

Примеры:

// Передан int логично что T == int и IList<T> == IList<int>
var list1 = ToList(1);

// Нельзя вывести тип так как нет входных параметров, нужно указывать явно
var emptyList = EmptyList<int>();

// К сожалению компилятор C# не умеет выводить параметр типа из возвращаемого значения
// IList<int> emptyList2 = EmptyList();

В примере выше, компилятор C# не смог вывести параметр типа по возвращаемому значению. Такая же история есть в других языках, например Java. В более современных языках типа TypeScript, Koltin, Rust таких проблем нет:

// TS
const emptyList1 = emptyList<number>();
const emptyList2: number[] = emptyList();
// Kotlin
val emptyList1 = emptyList<Int>()
val emptyList2: List<Int> = emptyList()
// Rust
let emptyList1 = emptyList::<i32>();
let emptyList2: Vec<i32> = emptyList();

Перейдем к использованию. Функция получила T в качестве параметра типа, но как его использовать? В прошлых примерах T передавался  в другие контейнеры либо функции. Что полезного можно сделать с таким типом?  T - это буквально все что угодно. В  C# "все что угодно" определяется типом Object и содержит небогатый арсенал методов.

public static void Fu<T>(T valueA) {
   Console.WriteLine(valueA?.Equals(null));
   Console.WriteLine(valueA?.GetHashCode());
   Console.WriteLine(valueA?.ToString());
   Console.WriteLine(valueA?.GetType());
}

К счастью идея параметрического полиморфизма идет в комплекте с механизмом ограничений через контракты. Можно установить ограничение для параметра типа снизив его абстрактность но увеличив возможности. Реализация контрактов зависит от языка программирования. В C# для реализации контрактов используется система наследования типов.

public interface IComparable<in T> {
 int CompareTo(T? other);
}
...
// Используем интерфейс IComparable в качестве ограничения
public static void PrintCompared<T>(T valueA, T valueB) where T : IComparable<T> {
   switch (valueA.CompareTo(valueB)) {
       case 0:
           Console.WriteLine($"{valueA} equals {valueB}");
           break;
       case > 0:
           Console.WriteLine($"{valueA} more than {valueB}");
           break;
       default:
           Console.WriteLine($"{valueA} less than {valueB}");
           break;
   }
}

Ограничение представленное как where T : IComparable<T> позволяет передавать в качестве параметра только типы реализующие интерфейс IComparable.Теперь T обязан реализовывать метод CompareTo, и мы можем использовать все возможные методы IComparable внутри PrintCompared.

// 1 less than 2
PrintCompared(1, 2);
// 1 less than 2
PrintCompared(1.0, 2.0);
// 1 less than 2
PrintCompared("1", "2");

Функция PrintCompared полиморфна, позволяет работать с любым типом который можно сравнить.

Можно использовать несколько ограничений одновременно. Например ограничить тип только числами:

public static void PrintCompared<T>(T valueA, T valueB) where T : IComparable<T>, INumber<T> {
   switch (valueA.CompareTo(valueB)) {
       case 0:
           Console.WriteLine($"{valueA} equals {valueB}");
           break;
       case > 0:
           Console.WriteLine($"{valueA} more than {valueB}");
           break;
       default:
           Console.WriteLine($"{valueA} less than {valueB}");
           break;
   }
}

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

Помимо использования в функциях, параметры типов могут быть составной частью других типов. Бегло рассмотрим использование параметров типов в самих типах:

interface Appendable<T> where T : INumber<T> {
   void Append(T value);
   T Total();
}

class IntAppender : Appendable<int> {
   private int _acc;
   public void Append(int value) {
       _acc += value;
   }
   public int Total() {
       return _acc;
   }
}

class ListAppender<T> : Appendable<T> where T : INumber<T> {
   private readonly List<T> _list = [];
   public void Append(T value) {
       _list.Add(value);
   }
   public T Total() {
       return _list.Aggregate((a, b) => a + b);
   }
}

В реализации IntAppender представлено место использования параметров типов. Здесь уточняется тип для Appendable. В коде IntAppender уже используется конкретный тип int.

В реализации ListAppender видно как объявление нового параметра типа для класса ListAppender так и использование его для уточнения Appendable. Благодаря чему сохраняется возможность работать с абстракцией.

Ну и наконец, использование обоих классов. Здесь ничего особенного, ListAppender может быть уточнен любым числовым типов в то время как в IntAppender зашит тип int.

Appendable<int> intAppender = new IntAppender();
Appendable<double> listAppender = new ListAppender<double>();

Напоследок хотелось бы добавить про интересные возможности параметрического полиморфизма. В разных языках программирования различный подход к его реализации. Где-то, как в Java, типы являются просто ограничителями для компилятора а где то они являются полноценными параметрами: C#, Rust, местами Kotlin. Для примера, возьмем  такой код на Rust:

let set = vec![1, 2, 3]
    .iter()
    .map(|n| n + 1)
    .collect::<HashSet<i32>>();

Если мы возьмем похожий код на Java то это будет выглядеть так:

var set = List.of(1, 2, 3)
       .stream()
       .map(n -> n + 1)
       .collect(Collectors.toSet());

Код почти идентичный но все же бросается в глаза то что в Java нужно явно вызывать метод Collectors.toSet() чтобы указать в какую структуру данных преобразовать итератор, в Rust же достаточно прописать тип явно и он все сделает сам. Это возможно благодаря продвинутой системе типов.

    fn collect<B: FromIterator<Self::Item>>(self) -> B where Self: Sized {
        FromIterator::from_iter(self)
    }

Целевой тип B должен реализовывать контракт по которому он может быть преобразован из итератора. Контракт и его реализация для HashSet. Все просто:

pub trait FromIterator<A>: Sized {
    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self;
}
...

fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
    let mut set = Self::with_hasher_in(Default::default(), Default::default());
    set.extend(iter);
    set
}

Интересное можно найти и в C#:

class IntContainer {
   public static int Value { get; }
}
// Все знают что с типом могут быть ассоциированы статические переменные. 
// Но что будет если эти переменные параметризировать?
class Container<T> {
   public static T Value { get; set; }
}

Если бы это был код на Java то компилятор бы просто сдался:

non-static type variable T cannot be referenced from a static context

Компилятор не понимает как представить класс Container во время выполнения если он может быть параметризован любым T. Вместо T могут быть подставлены разные типы а под капотом один и тот же класс. Но C# не так прост, все дело в том что в отличии от Java, где используется боксинг для реализации параметрического полиморфизма, C# использует специализацию т.е. метод при котором для каждого параметризованного типа создается отдельный класс. Иными словами, Container<int> - в рантайме это ContainerInt а Container<float> - это ContainerFloat.

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

Container<int>.Value = 10;
Container<float>.Value = 20.0f;
Container<string>.Value = "30";
Container<A>.Value = new A { Value = 40 };
Container<B>.Value = new B { Value = 50 };
Container<C>.Value = new C { Value = true };

Console.WriteLine(Container<int>.Value);     // 10
Console.WriteLine(Container<float>.Value);   // 20
Console.WriteLine(Container<string>.Value);  // 30
Console.WriteLine(Container<A>.Value);       // A { Value = 40 }
Console.WriteLine(Container<B>.Value);       // B { Value = 50 }
Console.WriteLine(Container<C>.Value);       // C { Value = True }

Для того чтобы изучить как можно использовать всю мощь параметров типов в C# очень советую ознакомиться с ECS библиотекой StaticEcs. Возможно даже кому-то она пригодится для написания игры.

Вишенка на торте

Мы рассмотрели разные примеры полиморфизма, и надеюсь, все поняли, что это не какая-то прерогатива ООП, а характерная своим разнообразием возможность, присущая различным языкам и парадигмам. Давайте окончательно в этом убедимся на примере языка Си, в котором, как некоторые думают, полиморфизма нет. Посмотрим на пример простого TCP сервера:

Код примера
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>


#define PORT 8080
#define BUFFER_SIZE 1024


int main(void) {

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    if (server_fd < 0) {
        perror("Socket creating failure");
        exit(EXIT_FAILURE);
    }

    int opt = 1;

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("Socket options setting failure");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("Binding error");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 5) < 0) {
        perror("Listening error");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    char buffer[BUFFER_SIZE];

    const char *response =
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html; charset=UTF-8\r\n"
        "Content-Length: 48\r\n"
        "Connection: close\r\n"
        "\r\n"
        "<html><body><h1>Hello, World!</h1></body></html>";

    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_fd;

    for (;;) {
        client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
        if (client_fd < 0) {
            perror("Accepting error");
            continue;
        }

        printf("Connection %s:%d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);

        memset(buffer, 0, BUFFER_SIZE);
       
        ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);

        if (bytes_read < 0) {
            perror("Read error");
            close(client_fd);
            continue;
        }
      
        buffer[bytes_read] = '\0';
        printf("Request receiving:\n%s\n", buffer);
      
        ssize_t bytes_writen = write(client_fd, response, strlen(response));
        if (bytes_writen < 0) {
            perror("Read error");
            close(client_fd);
            continue;
        }

        close(client_fd);
    }

    close(server_fd);

    return 0;
}

Из примера выше представляют интерес две функции:

// связывает локальный адрес с сокетом
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))
...
// ожидает соединение и открывает новый сокет для работы с ним
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);  

Взглянем на код:

(struct sockaddr *)&server_addr
(struct sockaddr *)&client_addr

Здесь можно увидеть, как указатель на тип sockaddr_in приводится к типу sockaddr.  Неужели полиморфизм? Да, он самый. Есть обобщенная структура, содержащая минимальную информацию о типе адреса.

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);  /* Common data: address family and length.  */
    char sa_data[14];   /* Address data.  */
  };

А есть структуры реализующие конкретные форматы адреса. Использованные выше sockaddr_in или sockaddr_in6 для Ipv6:

/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;     /* Port number.  */
    struct in_addr sin_addr;    /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr)
         - __SOCKADDR_COMMON_SIZE
         - sizeof (in_port_t)
         - sizeof (struct in_addr)];
  };
struct sockaddr_in6
  {
    __SOCKADDR_COMMON (sin6_);
    in_port_t sin6_port;  /* Transport layer port # */
    uint32_t sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr;  /* IPv6 address */
    uint32_t sin6_scope_id; /* IPv6 scope-id */
  };

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

Таким образом функции bind и accept могут быть полиморфными и принимать различные типы адресов. И это все на C без каких либо проблем.

Заключение

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

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

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


  1. PeeWeee
    17.05.2025 07:41

    КДПВ настолько классная, что пришлось прочесть статью :)


  1. nin-jin
    17.05.2025 07:41

    Прекрасная статья. Всё чётко и без воды.

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


    1. Dhwtj
      17.05.2025 07:41

      Если вам всё понятно, то можно выжимку из статьи?


      1. nin-jin
        17.05.2025 07:41

        У вас токены на LLM закончились что ли?


        1. Dhwtj
          17.05.2025 07:41

          Может, вы просто троечник, которому мерещится что всё понял, а реально ничего.

          Лично мне из статьи неясно

          • Нафига

          • Все ли варианты полиморфизма указаны

          Например, в заголовках идёт перечисление типов полиморфизма: перегрузка функций (также известный как ad-hoc полиморфизм), приведение типов (полиморфизм подтипов) и вдруг следущий пункт "Disjoint union и Algebraic data types"

          Ответьте, желательно не выходя за пределы статьи, это отдельный вид полиморфизма? Какое отношение к полиморфизму и зачем после первых двух вариантов внезапно это понятие введено?

          Полиморфизм, основанный на структуре данных? Обычно такой не упоминают. Является ли этот вариант полиморфизмом в полной мере? Особенно, с учётом что не все языки дают гарантии что switch и перебор образцов гарантируют полноту перечисления вариантов. Может, это попытка полиморфизма, но без гарантий.

          И так далее...


          1. nin-jin
            17.05.2025 07:41

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

            Ставлю вам двойку по внеклассному чтению. К следующему уроку комментарию, прочитайте, пожалуйста, все 3 статьи, чтобы вести дискуссию более предметно. Никому ваши галюцинации тут не интересны.


    1. Lewigh Автор
      17.05.2025 07:41

      Прекрасная статья. Всё чётко и без воды.

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

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


  1. Vitaly_js
    17.05.2025 07:41

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

     Строго говоря, это не так. У функциии в программировании есть самостоятельное значение. Тут бы ближе по смыслу был термин "операция".

    Некоторые предполагают что ООП отличается от других парадигм, ведь там есть объекты а у объектов есть методы. Методы - это особые функции привязанные к объекту. На деле же ничего особого в них нет и это не более чем удобный синтаксический сахар над обычными функциями.

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

    С этим надеюсь разобрались. Методы - это обычный синтаксический сахар над функциями.

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

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

    На самом деле меняет. Например возьмет JS там есть очереди функций, очереди микро задач, макро задач, и ни в одну из них не помещаются, например, арифметические операции.

    Так что бы вы понимали, есть такой язык как Ассемблер (собственно в него и компилируются языки высокого уровня) и для того, что бы сложить два числа на ЦПУ нужно их поместить в два регистра, а результат будет на месте первого операнда. Операция выполнится за такт. Поэтому и существуют и операторы, например, математические, логические, а есть функции.

    Все эти способы записи просто вариации синтаксиса над вызовом функции. Не смущайтесь увидев знаки + - > и т.д. - от имен обычных функций они ничем не отличаются.

    Как мы выяснили, на самом деле это не так. Далеко не каждый оператор является функцией. Особенно это относится к тем операторам, которые вы перечислили. Вот все таки стоило в самом начале дать ясное определение функции.

    Надеюсь теперь стало понятно что оператор - это такая же функция с особым синтаксисом.

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

    Дальше идет просто нечто. Несмотря на то, что все это имеет место быть, в рамках статьи, по моему, это подано не с той точки зрения.

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

    Я интереса ради загуглил эту тему и гугл выдал вполне ясный ответ:

    Типы полиморфизма: Существуют разные виды полиморфизма

    Подтиповой полиморфизм (Polymorphism by Subtypes): Обработка объектов различных классов, которые связаны отношениями наследования.

    Параметрический полиморфизм (Parametric Polymorphism): Использование обобщенных типов, которые позволяют работать с данными различных типов, не указывая их конкретно при определении функции.

    Абстрактный полиморфизм (Abstract Polymorphism): Различные реализации одного и того же интерфейса, позволяющие выполнять различные действия в зависимости от конкретного объекта.

    Ниже вы все это так же распишете.

    Тип данных представляет из себя множество допустимых значений. Например логический тип это два возможных варианта или true или false представляющих множество { true | false }. Если спрятать такой тип за nullable ссылкой то можно получить уже 3 возможных значения { true | false | null }. Целочисленный 32 битный тип это уже 4 294 967  296 допустимых значений { 2,147,483,648 |…| -1 | 0 | 1 | .. |  2,147,483,647 }.

    Вроде и написано нормально. Но вот представлять 32 битный целочисленный примитив в виде множества допустимых значений. Тут то как раз суть в том, что любое значение будет представлено 32 битным чилом, т.е. и 1 и 100000 - это всегда 32 битное число. В общем, звучит как то странно.

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

    Да в общем-то нет. Именно из-за того, что теперь полиморфизм - это в том числе и про ООП мы получаем что некоторые виды полиморфизма относятся только к ООП.


    1. Lewigh Автор
      17.05.2025 07:41

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

      Суть дела не меняет. В контексте обсуждения метод - это такая же функция а нет смысла выделять ему особое внимание.

      Так что бы вы понимали, есть такой язык как Ассемблер (собственно в него и компилируются языки высокого уровня) и для того, что бы сложить два числа на ЦПУ нужно их поместить в два регистра, а результат будет на месте первого операнда. Операция выполнится за такт. Поэтому и существуют и операторы, например, математические, логические, а есть функции.

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

      Это не имеет никакого значения так как здесь 0 магии. Операция/инструкция/функция - это просто имя и параметры которые могут быть представлены как угодно, хоть в стеке хоть в регистрах хоть часть самой команды, данная команда кем-то интерпретируется. Нет никакой магии вокруг операторов, это просто привычное нам название для одного из видов синтаксиса. Никакой прямой связи с представлением этого в машинный код нет. Есть компилятор или интерпретатор которые представляют тот или иной текст согласно правилам. Для примера есть функции интринсики которые компилятор или интерпретатор могут выполнять особым образом также как есть кача языков которые позволяют определять свои операторы. И оператор и обычная функция - это просто совокупность имени и параметров.

      Как мы выяснили, на самом деле это не так. Далеко не каждый оператор является функцией. Особенно это относится к тем операторам, которые вы перечислили.

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

      Да в общем-то нет. Именно из-за того, что теперь полиморфизм - это в том числе и про ООП .

      Это, чисто логически, странная точка зрения что более общее понятие про более частное а не наоборот.


      1. Vitaly_js
        17.05.2025 07:41

        Вас послушать, так вообще любой язык программирования - это синтаксический сахар над ассемблером. И если "чисто логически" для вас не аргумент, то откуда вообще взялись функции? Все в какую-то кашу превратилось.

        Нет никакой магии вокруг операторов, это просто привычное нам название для одного из видов синтаксиса. Никакой прямой связи с представлением этого в машинный код нет.

        Это как это? Вообще то все примитивные данные типа 32-64-80 бит числа выбраны именно из-за того, что это данные с которыми работает ЦПУ. Это же относится и к операциям.

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

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

        А по поводу магии, у вас ее действительно очень много, потому что вы в одну кучу смешали логическую и физическую составляющие.


        1. SolidSnack
          17.05.2025 07:41

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

          Не так разве? А ассемблер синтаксический сахар над байт кодом)

          С точки зрения программиста вся эта закулиса ему недоступна.

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

          Поэтому он может предполагать

          ?????????


          1. Vitaly_js
            17.05.2025 07:41

            Не так разве? А ассемблер синтаксический сахар над байт кодом)

            Нет. ЯП как и синтаксический сахар - это самостоятельные понятия.

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

            Вы путаете логическую и физическую архитектуры. Логическую архитектуру все так же необходимо понимать. Как раз для того, что бы все в одну кашу не скатывалось и функции, и данные, и операции. А физически все процессоры по разному могут выполнять команды ассемблера.

            ?????????

            Что?


            1. SolidSnack
              17.05.2025 07:41

              Что?

              Почему программисту недоступна закулиса?

              Если мы знаем частоту и количество нужных тактов, мы не можем рассчитать время выполнения??


              1. Vitaly_js
                17.05.2025 07:41

                Потому что для программиста ЦПУ - это набор регистров и команд. Программист сам не управляет конвейером ЦПУ.

                О каком времени вы говорите? Количество тактов и есть единица времени выполнения команды.


                1. SolidSnack
                  17.05.2025 07:41

                  Программист сам не управляет конвейером ЦПУ.

                  Ага, а этот конвеер ЦПУ может что-то делать без операционной системы? Программист операционной системы реализует процессы и их взаимодействие с ЦПУ, он наугад все делает?

                  Или кнопка в диспетчере задач "завершить процесс",на неё не то что программист может нажать и завершить процесс, а даже самый рядовой пользователь и получается повлиять на конвеер ЦПУ?)

                  Стиральная машинка которая у вас дома и скорее всего на линуксе, кто её работу с процессором настраивал если не программист?)

                  Нет. ЯП как и синтаксический сахар - это самостоятельные понятия.

                  Получается синтаксический сахар может существовать без языка? Или всетаки сначала идёт ЯП, а к нему уже синтаксический сахар.


                  1. SolidSnack
                    17.05.2025 07:41

                    А программист, в линуксе, может это сделать вот так


                  1. Vitaly_js
                    17.05.2025 07:41

                    Вам стоит загуглить, что такое конвейер ЦПУ. Что бы понимать, какую ерунду вы написали.

                    Получается синтаксический сахар может существовать без языка? Или всетаки сначала идёт ЯП, а к нему уже синтаксический сахар.

                    А что вас смущает? Я говорю, что это определения, которые выражают свою собственную суть. Например, есть вилка и есть зубчики вилки. Данные определения выражают совершенно самостоятельные сущности хотя одно не отделимо от другого, и что?


        1. Lewigh Автор
          17.05.2025 07:41

          Вас послушать, так вообще любой язык программирования - это синтаксический сахар над ассемблером. И если "чисто логически" для вас не аргумент, то откуда вообще взялись функции? Все в какую-то кашу превратилось.

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

          5 + 5
          add(5, 5)
          5.add(5)

          То это будут совершенно разные операции, хотя по факту это одно и тоже а разница только в синтаксисе. Потому что это все просто текст в котором есть имя операции и параметры который воспринимается транслятором или интерпретатором именно так.
          Можно выполнять сложение так 5 + 5 а можно так add(5, 5), разница будет не в том какое имя будет у операции а в том как они воспринимается сущностью которая переводит это на другой уровень.

          Это как это? Вообще то все примитивные данные типа 32-64-80 бит числа выбраны именно из-за того, что это данные с которыми работает ЦПУ. Это же относится и к операциям.

          Почему то вы вбили себе в голову что операторы это что-то особенное.

          "Hello" + " world"

          Вот это в языке который Вы используете тоже наверное процессором напрямую исполняется? Или это просто имя функции наподобие concat() которой перегрузили знак + ?

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

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

          А по поводу магии, у вас ее действительно очень много, потому что вы в одну кучу смешали логическую и физическую составляющие.

          Это Вы начала на ASM тему переводить. Не нужно меня приплетать.


          1. Vitaly_js
            17.05.2025 07:41

            Язык программирования это набор правил. Функция, метод и оператор - это одно и тоже по смыслу, записанное с разным синтаксисом.

            Именно по смыслу - это разные вещи. Если мы говорим про языки программирования каждая часть речи отличается друг от друга. Это под капотом они могут использовать одинаковые механизмы. Выражайтесь ясно.

             Почему то по Вашей логике если я напишу:

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

            То это будут совершенно разные операции, хотя по факту это одно и тоже а разница только в синтаксисе.

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

            Почему то вы вбили себе в голову что операторы это что-то особенное.

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

            Нет никакого смысла все обзывать функциями только потому, что под капотом все это может быть функциями. Например, оператор логического сдвига влево с точки зрения программиста делает ровно то как называется. Поэтому даже на таком высокоуровневом языке как JS он будет в 10 раз быстрее чем умножение на 2. И нет вообще никакого смысла рассматривать его как функцию даже в рамках вашей статьи.

            "Hello" + " world"

            Это вот ваше желание все скинуть в одну кучу и приводит к таким вот вопросам.

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

            Так я именно на это выше и указал. А вы опять все одну кучу скинули. Зачем?

            Это Вы начала на ASM тему переводить. Не нужно меня приплетать.

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

            Мне просто любопытно, если в коде увидим что-то типа:

            interface makeSound {
              makeSound(): void
            }
            
            class Cat implements makeSound {
              makeSound() {
                //...
              }
            }
            
            class Dog implements makeSound {
              makeSound() {
                //...
              }
            }
            
            function getAnimal(): makeSound {
              return {} as makeSound
            }
            
            getAnimal().makeSound()

            Мы можем говорить, что данный код использует идеи полиморфизма?
            На вид тут вообще все про ООП. И объяснять данную ситуацию в терминах которые не относятся к ООП, потому что: "Как вы могли заметить, полиморфизм - это не про ООП, это история о том как различными способами дать возможность функциям работать гибко с данными разных типов." - по моему, крайне не эффективно.


            1. Lewigh Автор
              17.05.2025 07:41

              Именно по смыслу - это разные вещи. Если мы говорим про языки программирования каждая часть речи отличается друг от друга. 

              Вне зависимости от того что под капотом вот это 1 + 2 будет называется оператором а вот это add(1, 2) или это add 1 2 функцией, или методом или даже процедурой. То что использует спецсимволы и способы способы записи мы привыкли называть операторами. Вот и все. Что оператор что функция, если обобщить, это некое действие, примененное к 0..n параметрам. Действие это может быть применено каким угодно образом, это к тому что мы называем операторами и функциями не имеет никакого значения. Даже если брать особенности языков программирования, сегодня у оператора одна реализация а завтра другая а послезавтра вообще добавят поддержку своих операторов. Это никак не повлияет на то что вот это мы будет называть оператором 1 + 2, как бы оно не было реализовано, и никак не повлияет на то что это продолжит быть действием к 0..n параметрам. Суть это никак не меняет. Если мы рассуждаем абстрактно то понятие перегрузка одинаково применимо что к оператору что к функции что к методу вне зависимости есть перегрузка того или иного в языке, сама идея не меняется. Мы имеет одну идею - действие может быть перегружено, а чем выражено действие не важно, вместо того чтобы на каждый чих придумывать специальное правило и понятие, потому как в общем смысле разница только в синтаксисе а идея одна и тажа. Если человек понимает эту идею он применит ее в том языке программирования которым пользуемся согласно ее особенностям. Если для Вам тяжело это осознать то я не вижу смысла продолжать одно и тоже.

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

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

              То нужно то не нужно. Определитесь уже.
              Вы одни тезисы подкрепляете тем что программисту нужно знать что по капотом другое поэтому операторы это другое и тут же пишите что полиморфизм нужно объяснять через абстракцию. И кто тут мешает в кучу?
              У меня в статье понятия и абстрагированы, я не пишу что оператор - это другое, только потому что у какой то версии какого-то языка у него может быть особая реализация под капотом.

              Мы можем говорить, что данный код использует идеи полиморфизма?На вид тут вообще все про ООП. И объяснять данную ситуацию в терминах которые не относятся к ООП, потому что: "Как вы могли заметить, полиморфизм - это не про ООП, это история о том как различными способами дать возможность функциям работать гибко с данными разных типов." - по моему, крайне не эффективно.

              Хлеб - это не история при отруби, это — пищевой продукт, получаемый при выпечке теста, приготовленного как минимум из муки и воды, разрыхлённого пекарскими дрожжами или закваской.

              И тут заходите Вы:

              Мы можем говорить, что данный продукт(хлеб с отрубями) использует идеи хлеба?На вид тут вообще все про отруби. И объяснять данную ситуацию в терминах которые не относятся к отрубям, потому что: "Как вы могли заметить, хлеб - это не при отруби...." - по моему, крайне не эффективно.

              Если до сих пор не понятно, фраза "полиморфизм - это не про ООП" означает что не нужно думать что полиморфизм придумали в ООП и что весь полиморфизм - это полиморфизм подтипов. Полиморфизм - это не про ООП потому что ООП - это про полиморфизм, в том чисел, а не наоборот.


              1. Vitaly_js
                17.05.2025 07:41

                Так, минуточку

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

                Это вот вы написали теперь отказываясь от того, что началось. Напоминаю с чего все началось

                Строго говоря, это не так. У функциии в программировании есть самостоятельное значение. Тут бы ближе по смыслу был термин "операция".

                Теперь вы отказываетесь от "функции" и выбираете "действие". Собственно, что и требовалось показать.

                Вы одни тезисы подкрепляете тем что программисту нужно знать что по капотом другое поэтому операторы это другое и тут же пишите что полиморфизм нужно объяснять через абстракцию. И кто тут мешает в кучу?

                А вы точно поняли, что я вам сказал и на что вы отвечаете? Выше же все написано и вот этот ваш абзац с этим никак не вяжется.

                Если до сих пор не понятно, фраза "полиморфизм - это не про ООП" означает что не нужно думать что полиморфизм придумали в ООП и что весь полиморфизм - это полиморфизм подтипов. Полиморфизм - это не про ООП потому что ООП - это про полиморфизм, в том чисел, а не наоборот.

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

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


                1. Lewigh Автор
                  17.05.2025 07:41

                  Теперь вы отказываетесь от "функции" и выбираете "действие". Собственно, что и требовалось показать.

                  Нигде написано что я от чего то отказываюсь

                  А вы точно поняли, что я вам сказал и на что вы отвечаете?

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

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

                  А хлеб в который придумали добавлять отруби он про отруби и именно про отруби. У Вас определенно есть чему поучится в обращении с терминологией.

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

                  Термин функция был выбран потому что он наиболее общеупотребимый и понятный подавляющему большинству читателей. Естественно что в Java или C# правильно говорить метод, и не важно с ресивером он или нет, в Scala вообще есть и функции и методы и последние там опять таки не связаны с ресиверами, в каких то языках есть процедуры, в фп языках есть понятия чистых и монадических функций , но никто в здравом уме не полоскает друг другу мозг в стиле "как вы посмели говоря в контексте Java назвать метод функцией это же совсем другой язык тут так нельзя", люди, особенно кто пишет на не на одном языке, называют как им удобно даже не смотря на то про какой язык они говорят, и что характерно все друг друга понимают. И что еще более важно - суть это никак не меняет.
                  Был выбран термин функция, потому что так всем удобнее и понятнее без привязки к конкретному языку и именно функция послужила базой для таких вещей как оператор и конструктор, потому что это просто, понятно, непротиворечиво, там устроено в большинстве языков и главное в сути своей этим и является и все понятия с данной моделью соотносятся.
                  Вы или это не понимаете, ну тогда извените, это статья не для Вас.
                  Либо все понимаете но зачем-то страдаете фигней.

                  Если есть что еще обсудить - милости прошу, но этой демагогией у меня желания заниматься более нет.


            1. nin-jin
              17.05.2025 07:41

              Например, оператор логического сдвига влево с точки зрения программиста делает ровно то как называется. Поэтому даже на таком высокоуровневом языке как JS он будет в 10 раз быстрее чем умножение на 2. И нет вообще никакого смысла рассматривать его как функцию даже в рамках вашей статьи.

              Я просто оставлю это здесь.


              1. Vitaly_js
                17.05.2025 07:41

                А что это за цифры?


  1. Dhwtj
    17.05.2025 07:41

    За многабукф статьи потерялся практический смысл полиморфизма:

    Полиморфизм – это способность через единый способ (интерфейс, т.е. "способы поведения") взаимодействовать с различными сущностями, которые этот интерфейс скрывает/абстрагирует.

    И это полное соответствие принципу минимума знаний (Закону Деметры): клиентскому коду нужно знать только об одном небольшом обещании проведения (интерфейсе), а не о бесчисленных деталях реализации (и даже не надо знать о полном контракте - конкретном классе) каждой конкретной сущности.

    Какой практический смысл этого?

    1. Гибкость и Расширяемость (Flexibility & Extensibility):

      • Смысл: Вы можете добавлять новые типы объектов (новые "сущности" со своими уникальными "способами проведения"), которые соответствуют существующему интерфейсу, без изменения кода, который этот интерфейс использует.

      • Пример: У вас есть система, обрабатывающая различные типы документов (IDocument с методом process()). Изначально есть PdfDocument и WordDocument. Позже вам нужно добавить ExcelDocument. Вы просто создаете новый класс ExcelDocument, реализуете IDocument, и существующий код обработки документов (который работает с IDocument) автоматически сможет обрабатывать и Excel-файлы. Ему не нужно знать, что появился новый тип.

      • Результат: Система легко адаптируется к новым требованиям.

    2. Уменьшение связанности (Decoupling / Loose Coupling):

      • Смысл: Компоненты системы меньше зависят друг от друга. Клиентский код зависит от стабильной абстракции (интерфейса), а не от изменчивых/расширяемых полных контрактов (в ООП - от конкретных классов).

      • Пример: Модуль оплаты (PaymentProcessor) работает с интерфейсом IPaymentGateway (например, processPayment()). Вы можете легко заменить одну платежную систему (например, StripeGateway) на другую (PayPalGateway), если обе реализуют IPaymentGateway. Сам PaymentProcessor не изменится.

      • Результат: Изменения в одной части системы с меньшей вероятностью "ломают" другие части. Легче заменять компоненты.

    3. Упрощение кода и повышение читаемости (Simplified & More Readable Code):

      • Смысл: Вместо громоздких конструкций if-else if-else или switch для обработки разных типов, вы пишете один общий код, работающий с интерфейсом.

      • Пример:

        // Плохо:
        void handleShape(Object shape) {
            if (shape instanceof Circle) ((Circle)shape).drawCircle();
            else if (shape instanceof Square) ((Square)shape).drawSquare();
            // ... и так далее
        }
        
        // Хорошо (с полиморфизмом):
        void handleShape(IShape shape) {
            shape.draw(); // Единый способ, конкретная реализация вызовется сама
        }
        
      • Результат: Код становится короче, понятнее и менее подвержен ошибкам при добавлении новых типов.

    4. Поддержка Принципа Открытости/Закрытости (Open/Closed Principle - OCP):

      • Смысл: Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. Полиморфизм – ключевой механизм для достижения этого.

      • Пример: Вы расширяете функциональность, добавляя новые классы, реализующие интерфейс (открытость для расширения), не изменяя существующий код, который использует этот интерфейс (закрытость для модификации).

      • Результат: Более стабильная система, которую легче развивать.

    5. Облегчение тестирования (Easier Testing):

      • Смысл: При модульном тестировании компонента, который зависит от других через интерфейсы, вы можете легко подменить реальные реализации этих зависимостей на тестовые заглушки (моки или стабы).

      • Пример: Тестируя UserService, который использует IUserRepository для доступа к данным, вы можете передать ему MockUserRepository, который возвращает предопределенные данные без реального обращения к базе. UserService будет работать с MockUserRepository точно так же, как с реальным, через интерфейс IUserRepository.

      • Результат: Тесты становятся проще, быстрее и надежнее.

    6. Создание фреймворков и библиотек:

      • Смысл: Полиморфизм позволяет создавать фреймворки, которые определяют "точки расширения" (интерфейсы), а пользователи фреймворка предоставляют свои конкретные реализации.

      • Пример: GUI-фреймворк может определять интерфейс IEventListener с методом onEvent(). Пользователи создают свои обработчики событий, реализуя этот интерфейс. Фреймворк вызывает onEvent() для нужного слушателя, не зная деталей его реализации.

      • Результат: Мощные, гибкие и переиспользуемые программные компоненты.

    Итог:

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


  1. SolidSnack
    17.05.2025 07:41

    Отличная статья! Если честно пока осилил только половину, получил прозрение на одном моменте:

    Мономорфная функция — это функция способная применяться к конкретному типу данных.

    Полиморфная функция — это функция способная применяется к различным типам данных.

    Буквально сегодня писал код и чувствовал легкий зуд от пробела знания в этом моменте, теперь думаю мне станет чуточку легче)


  1. Dhwtj
    17.05.2025 07:41

    Вот нормальная старая статья в отличие от этой

    https://intro2oop.sdds.ca/E-Polymorphism/overview-of-polymorphism

    И ещё

    https://members.accu.org/index.php/articles/538


    1. FFS_Studios
      17.05.2025 07:41

      Могу я спросить, что вы нашли "ненормального" в данной статье? Хотелось бы услышать конкретику в ответ от человека (а не LLM текст), который полностью прочел статью, а не только заголовки.


      1. Dhwtj
        17.05.2025 07:41

        Написал же, только в разных местах.

        Идёт очень долгая вводная с терминами "базовые понятия", который эти понятия вводит спорным образом, ладно, перемотал. Потом перечисление типов полиморфизма с прыжками в сторону, потом с тем же стилем заголовка внезапно "Disjoint union и Algebraic data types", без объяснений и с резким сваливанием в крен экзотики математических типов и option, потом имитации option с длинным кодом. Чиво??? Мы о чём? Я судорожно пытаюсь вспомнить как это относится к теме, гугление приводит к тому что термин Disjoint union это то же что и discriminated union, которым пестрят статьи, но первый раз слышу Disjoint union.

        Потом вспоминаю классификацию полиморфизма от Карделли, Вегнер, 1985 - дал ссылки внизу. И там этого нет.

        Далее что? Если бы этот пункт был написан внятно, я бы убедился: вот ещё один вид, не вписавшийся в старую классификацию. Но пункт убогий и остаётся только непонимание. Что значит внятно: выше было бы определение вот такое сабж, такое не сабж. Никакой проверки, подтверждения не было дано, жрите что дают.

        Уф, я уже устал писать.

        Дальше уже листал без интереса. Только утиную типизацию прокомментировал

        И за длинным и некорректным описанием уже забыл зачем все это нужно. Проще написать самому.

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

        Да не бомбит у меня! ©™


        1. Dhwtj
          17.05.2025 07:41

          Начиналось как шутка

          А потом выбесили меня


        1. Lewigh Автор
          17.05.2025 07:41

          Хорошо давайте по порядку.
          Я упрощу до Algebraic data types чтобы никого не путать.

          Идёт очень долгая вводная с терминами "базовые понятия", который эти понятия вводит спорным образом, ладно, перемотал.

          Потом вспоминаю классификацию полиморфизма от Карделли, Вегнер, 1985 - дал ссылки внизу. И там этого нет.

          Ваша претензия в том что я когда писал статью не переписал ее с википедии? Хочу обратить внимание, к сожалению у нас нет ГОСТа на такое понятие как полиморфизм. Даже договорится что такое ООП до сих пор за 30 лет не можем. Вы ссылаетесь на Карделли и Вегнера, но это, внимание, тоже статья двух людей, которые написали ее, о ужас расширяя определения уже третьей статьи которую написал Кристофер Стрейчи. Мы не в церкви и не трактуем священные писания чтобы предъявлять претензии касательно того что кто-то посмел выражая свое мнение написать не как в википедии.
          Именно для того чтобы не было путаницы я в начале статьи специально ввел локальные определения что и под чем мы будем понимать. Но Вас было лень это читать, перемотали, зато не лень жаловаться что дальнейшее повествование не понятно и не соответствует Вашим ожиданиям.
          Причем тут алгебраические типы и какое они имеют отношение к теме? Вам уже не один человек предложил прочитать статью внимательно, если бы Вы это сделали то увидели бы локальное определение из, той самой, скучной части:

          Полиморфная функция — это функция способная применяется к различным типам данных.

          Вот определение вокруг которого построено дальнейшее повествование и прочитав его думаю очевидно что, согласно данной статье, если функция принимает алгебраический тип A | B, то она может применятся и к A и к B. А функция которая может примениться к различным типам - полиморфна. Это предельно простое определение.

          Но пункт убогий и остаётся только непонимание.

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


          1. Dhwtj
            17.05.2025 07:41

            Пояснения по классификации:

            ADT (алгебраические типы данных) — это не отдельный тип полиморфизма, а частный случай уже существующего subtype-полиморфизма. Варианты enum рассматриваются как подтипы общего алгебраического типа.

            Основные виды полиморфизма:

            1. Ad-hoc (перегрузка)
              Одна функция, разные реализации для разных типов аргументов.
              Пример (C++ overload):

              cpp

              int abs(int);
              double abs(double);
            2. Ad-hoc (coercion, приведение типов)
              Неявное преобразование одного типа в другой.
              Пример (Java coercion):

              java

              long x = 5; // int → long неявно
              foo(x);
            3. Parametric (параметрический)
              Функция работает одинаково для любых типов, не зная их деталей.
              Пример (Kotlin generic ADT):

              kotlin

              data class Option<T>(val v: T?)
              fun <T> id(x: Option<T>) = x
            4. Subtype (подтип-полиморфизм)
              Функция принимает базовый тип (интерфейс), вызывая методы конкретных подтипов.
              Пример (C# subtype):

              C#

              interface Animal { void Speak(); }
              void talk(Animal a) => a.Speak();

            Частные случаи (сводятся к основным, но имеют свои особенности и названия):

            • Haskell type-class (ad-hoc полиморфизм через автоматический выбор реализации):

              haskell

              (==) :: Eq a => a -> a -> Bool  
              1 == 1  -- True  
              'a' == 'b'  -- False
            • Java F-bounded (ограничение типа самим собой, частный случай parametric):

              java

              class User implements Comparable<User> {
              public int compareTo(User other) { ... }
              }
            • PureScript row polymorphism (структурный подтип-полиморфизм для записей):

              purescript

              greet :: { name :: String | r } -> String  
              greet person = "Hi, " <> person.name
            • Haskell higher-kinded polymorphism (parametric полиморфизм над контейнерами):

              haskell

              
              fmap :: Functor f => (a -> b) -> f a -> f b
              fmap (+1) [1,2,3] -- [2,3,4] fmap (+1) (Just 10) -- Just 11
            • Утиная типизация (duck typing) — неявный структурный подтип-полиморфизм, проверка наличия методов/полей в runtime:

              python

            def len_plus_one(obj):
                return len(obj) + 1
            
            len_plus_one([1,2,3])  # 4
            len_plus_one("abc")    # 4
            • OCaml polymorphic variants (структурный подтип-полиморфизм с открытыми enum'ами):

              ocaml

              let to_string = function
              | `Int i -> string_of_int i
              | `Bool b -> string_of_bool b to_string (Int 5) (* "5" *) to_string (Bool true) ( "true" )
            • Утиная типизация (duck typing) — неявный структурный подтип-полиморфизм, проверка наличия методов/полей в runtime:

              Python

            def len_plus_one(obj):
                return len(obj) + 1
            
            len_plus_one([1,2,3])  # 4
            len_plus_one("abc")    # 4

            Итоговая классификация кратко:

            • Основные виды:

              • Ad-hoc (перегрузка)

              • Ad-hoc (приведение)

              • Parametric (дженерики)

              • Subtype (наследование, интерфейсы)

            • Частные случаи (сводятся к основным, но имеют свои особенности):

              • Type-class (Haskell)

              • F-bounded (Java)

              • Row polymorphism (PureScript)

              • Higher-kinded (Haskell)

              • Existential (Rust)

              • Polymorphic variants (OCaml)

              • Duck typing (Python, JS и др.)

            Хоть Haskell и редкий язык, его примеры полезны для понимания концепций. Java и Rust — более распространённые, поэтому их особенности (F-bounded, existential) важны и интересны. Утиная типизация тоже упомянута.


            1. Dhwtj
              17.05.2025 07:41

              хотя... частные случаи же

              ну ок

              Основные виды:

              • Ad-hoc (перегрузка)

              • Ad-hoc (приведение типов)

              • Parametric (дженерики)

              • Subtype (наследование, интерфейсы)

              Частные случаи (сводятся к основным, но имеют свои особенности):

              • ADT (enum algebraic, явный перечень вариантов + pattern matching) — специальный случай subtype-полиморфизма (варианты enum как подтипы)

              • Duck typing (Python, JS и др.) — специальный случай subtype-полиморфизма (неявный структурный)

              • Type-class (Haskell) — специальный случай ad-hoc полиморфизма

              • F-bounded (Java) — специальный случай parametric полиморфизма

              • Row polymorphism (PureScript) — специальный случай subtype-полиморфизма (структурный)

              • Higher-kinded (Haskell) — специальный случай parametric полиморфизма

              • Existential (Rust) — специальный случай subtype-полиморфизма

              • Polymorphic variants (OCaml) — специальный случай subtype-полиморфизма (структурный)


            1. Lewigh Автор
              17.05.2025 07:41

              ADT (алгебраические типы данных) — это не отдельный тип полиморфизма, а частный случай уже существующего subtype-полиморфизма. Варианты enum рассматриваются как подтипы общего алгебраического типа.

              Здорово конечно что LLM продолжает писать за Вас комментарии но порой не лишним было бы подумать своей головой - полиморфизм подтипов это универсальный полиморфизм а для ADT нужно реализовывать варианты для каждого типа что есть - специальный полиморфизм.
              С нейросетью конечно увлекательно спорить но пожалуй я на этом остановлюсь. Потрачу лучше время на живых людей.


              1. Dhwtj
                17.05.2025 07:41

                Тяжело с этими кожаными


  1. Fitbie
    17.05.2025 07:41

    Я или сам дошёл или подсмотрел где следующее определение:

    Полиморфизм это возможность обращения к разным реализациям через общий интерфейс

    И оно как то засело у меня