Pull to refresh

Comments 24

enum’ы

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

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

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

Я вот тоже совершенно не понимаю, зачем это надо. Мантры «шаг в сторону реализации алгебраического типа данных» («алгебраического типа данных» произносится шепотом и с придыханием) — тоже нифига не достаточно. Алгебраический тип данных сам по себе — как артефакт — никакой ценности не представляет.


Опыт rust — это лучше, чем ничего, но бесконечно мало, для того, чтобы делать такие заявления. Сопоставление с образцом для извлечения данных — это уже теплее, но алгебраические типы тут ни при чем (так умеет даже физический триод, и то, что алгебраические типы — тоже, пока не делает их лучше). Гошная кондовая проверка на ошибку — тоже сопоставление с образцом, в какой-то мере. switch в js.


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

Конечно речь об увеличении строгости типизации, в эту сторону двигается PHP.


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


Вот предлагаемый enum и есть тип-сумма, то есть перечисление других типов:


enum Color {
    case Red;
    case Orange;
    case Yellow;
    case Green;
    case Blue;
    case Indigo;
    case Violet;
}

Аналог на Rust:


enum Color {
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet,
}

Смысл в том, что эти значения — это не константные значения, это — варианты типа. Их именно столько, сколько определено в конкретном типе, и переменная типа Color может принимать одно из этих значений, и никакие другие:


// PHP
$color = Color::Yellow;

// Rust
let color = Color::Yellow;

Теперь, в качестве вариантов могут быть не только единичные типы (без полей), но и типы-произведения:


// PHP
enum Color {
    case Red;
    case Orange;
    case Yellow;
    case Green;
    case Blue;
    case Indigo;
    case Violet;
    case Rgb(public $r, public $g, public $b);
}

// Rust
enum Color {
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet,
    Rgb {
        r: u8,
        g: u8,
        b: u8,
    }
}

То есть, теперь вы цвет можете задать еще и покомпонентно:


// PHP
$color_a = Color::Yellow;
$color_b = Color::Rgb(r: 100, g: 25, b: 25);

// Rust
let color_a = Color::Yellow;
let color_b = Color::Rgb { r: 100, g: 25, b: 25 };

Фактически вариант Rgb — это отдельная структура, и в Rust ее действительно можно отделить:


struct Red;

struct Orange;

struct Rgb {
    r: u8,
    g: u8,
    b: u8,
}

enum Color {
    Red(Red),
    Orange(Orange),
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet,
    Rgb(Rgb)
}

let rgb = Rgb { r: 100, g: 25, b: 25 };
let color = Color::Rgb(rgb);

Это чтобы было понятно, что имена вариантов — это просто метки типа Color вводимые для некоторых других типов, которые тип-перечисление в себе объединяет.


Теперь, для типа Color можно создать общие методы, которые будут присутствовать во всех его вариантах:


// PHP
enum Color {
    case Red;
    case Orange;
    case Yellow;
    case Green;
    case Blue;
    case Indigo;
    case Violet;
    case Rgb(public $r, public $g, public $b);

    public function get_r(): mixed {
        return match type ($this) {
            Color::Red => 255,
            Color::Orange => 255,
            Color::Yellow => 255,
            Color::Green => 0,
            Color::Blue => 0,
            Color::Indigo => 75,
            Color::Violet => 139,
            Color::Rgb => $this->r,
        };
    }
}

$colors = [Color::Yellow, Color::Rgb(r: 100, g: 25, b: 25)];
foreach ($colors as $color) {
    echo $color->get_r();
}

// Rust
enum Color {
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet,
    Rgb {
        r: u8,
        g: u8,
        b: u8,
    }
}

impl Color {
    fn get_r(&self) -> u8 {
        match self {
            Color::Red => 255,
            Color::Orange => 255,
            Color::Yellow => 255,
            Color::Green => 0,
            Color::Blue => 0,
            Color::Indigo => 75,
            Color::Violet => 139,
            Color::Rgb { r, .. } => r,
        }
    }
}

let colors = [Color::Yellow, Color::Rgb { r: 100, g: 25, b: 25 }];
for color in colors {
    println!("{}", color.get_r());
}

Как это применять? Существует огромное число случаев, в которых удобно использовать тип-сумму (перечисление) и АТД. Например, вместо булевых флагов для обозначения состояний (которых может быть больше двух). При этом еще в каждом состоянии могут храниться свои наборы данных особого типа. АТД привносит преимущества нестрогой типизации в строго-типизированные языки, не выходя за пределы строгой типизации.

Ух. Я столько кода даже на ревью не осилю.


Конечно речь об увеличении строгости типизации, в эту сторону двигается PHP.

Неясно, почему это аксиоматично хорошо. Но допустим.


Алгебраический тип данных — это довольно простая штука [...]

Я в курсе, что это такое, спасибо.


Существует огромное число случаев, в которых удобно использовать тип-сумму (перечисление) и АТД.

Как я уже заметил в том комментарии, на который вы отвечаете, ваши нужды конкретно в этом вопросе покрываются сравнением с образцом. Вы приписываете эти чудо-свойства АТД просто потому, что в АТД оно используется, но это не единственный (и не лучший) вариант его использования.


вместо булевых флагов для обозначения состояний

Это смешной аргумент. Есть миллиард ничуть не худших способов сделать то же самое. Фабрика и приватный конструктор в банальной джаве умеют это с 1996 года.


АТД привносит преимущества нестрогой типизации в строго-типизированные языки, не выходя за пределы строгой типизации.

Так если у нестрогой типизации есть преимущества, может надо ее и использовать, а не тащить их повсюду?




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

ваши нужды конкретно в этом вопросе покрываются сравнением с образцом

Это как? Сопоставление не может ограничить множество типов, которое может принимать переменная. Этим занимется тип-сумма.


Есть миллиард ничуть не худших способов сделать то же самое. Фабрика и приватный конструктор в банальной джаве умеют это с 1996 года.

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

Сопоставление не может ограничить множество типов, которое может принимать переменная.

Переменная? При чем тут вообще переменная? Переменных в языке может вообще не быть (если не рассматривать хаскелевский синтаксический сахар let in как переменную, конечно) — и в языках с нативными функциями высшего порядка их обычно и нет.


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

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

а уж опыт эрланга (где вообще типов нет, и оттого все прямо прекрасно)

Опечатался в названии атома — и всё, актор уже не ловит нужные сообщения, потому что там-то код без опечатки. Очень удобно, да.

У меня более, чем тридцатилетний опыт разработки, и я ни разу не видел в продакшене код, который сбоит из-за того, что кто-то где-то опечатался (даже в руби, где можно object.send(:method)). Есть миллиарды причин, по которым типы действительно могут в чем-то помочь, но вот этим бессмысленным аргументом с опечаткой неимоверно достали, если честно.

У меня более, чем тридцатилетний опыт разработки, и я ни разу не видел в продакшене код, который сбоит из-за того, что кто-то где-то опечатался

Я бы сказал, что логично, что не видели.

Справедливости ради, АТД можно строить не только на тип-суммах, но и на тип-объединениях (как в typescript), которые языку с исходно-динамической типизацией подошли бы больше.

> Каждый член enum — это отдельный класс.
Возможно ни чего такого ввиду не имелось, но для тех, кто мимо проходил может звучать странно:
— создаем какое — то перечисление SomeEnum
— указываем одним из значений SOME_VALUE
— раз SOME_VALUE класс, значит можно породить потомка?

фишка в том что Enum как в C — никому не нужен, поэтому что это нельзы будет никак контролировать в сигнатуре функции.
когда у нас набор констант, типа
class Enum {
public const VAL_ONE = 1;
public const VAL_TWO = 2;
}
function foo(int $constValue) : void

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


все хотят чтоб было
enum Values {
case ONE = 1;
case TWO = 2;
}
function bar(Values $value) : void

чтоб никакая сволочь не просунула в функцию, например, значение 65536.


скорей всего под капотом Никита видит это как порезанный класс без возможности ручного инстанцинирования (а-ля Closure или Generator) и его объекты/наследников, чтоб сохранялась возможность проверки true === Values::ONE instanceof Values

В вашем примере каждый член enum это экземпляр класса, если я правильно понимаю. А смущает именно фраза "каждый член enum это отдельный класс"


Вообще в php давно пользуюсь https://github.com/myclabs/php-enum
который как раз позволяет в контроль тайпхинтингом

Все правильно, каждый член перечисления должен быть отдельным типом. Вот пример на Rust (откуда эту фичу тянут в PHP):


enum Foo {
    Unit,           // Единичный тип
    First(i32),     // Целое 32-х разрядное число
    Second(i32),    // Другое целое число
    Text(String),   // Строка
    Point { x: f32, y: f32 },   // Тип-произведение (запись)
}

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

Я не большой знаток rust, но в нем элементами перечисления могут быть просто значения, кортежи и структуры (да поправят меня знатоки раст, если я ошибаюсь).
Самые близкие аналоги в пыхе это скаляры, индексированные и ассоциированные массивы.
Может стоит обернуть их, чтобы в коде можно было писать, например:
enum Foo {
    Unit,           // Единичный тип
    First(int),     // Целое 32-х разрядное число
    Second(int),    // Другое целое число
    Text(string),   // Строка
    Point(x: float, y: float),   // Тип-произведение (запись)
}

Foo $my_var=Foo::Point(x: 1, y: 2);
print($my_var.x);

и бросать ексепшн, если $my_var не содержит в текущем значении этого свойства?
Отдельный синтаксис, отдельная реализация, и точно ни какого влияния на текущую реализацию class-a.
Я не большой знаток rust, но в нем элементами перечисления могут быть просто значения, кортежи и структуры

То, что вы называете "просто значения" — это значения дискриминанта, которые есть всегда. То есть, допустим у нас есть перечисление:


enum Foo {
    First(i32),
    Second(i32),
}

Оба варианта — это кортежные структуры с анонимным полем типа i32. Сколько места должен занимать тип Foo? Кажется, что 4 байта, но ведь в объекте типа Foo еще нужно хранить информацию о том, какой именно вариант выбран (First или Second). Эта дополнительная информация хранится в так называемом дискриминанте, который добавляет веса типу:


println!("{}", std::mem::size_of::<Foo>());  // 8

4 дополнительных байта занял дискриминант. Но ведь это просто число! Почему бы не позволить пользователю переопределять его? В случае


enum Number {
    Zero,
    One,
    Two,
}

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


println!("zero is {}", Number::Zero as i32); // zero is 0
println!("one is {}", Number::One as i32);   // one is 1

А теперь сконструируем enum, состоящий из единичных типов, но с явно указанными значениями дискриминанта:


enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

println!("roses are #{:06x}", Color::Red as i32);   // roses are #ff0000
println!("violets are #{:06x}", Color::Blue as i32);// violets are #0000ff

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

А потом появляется Option<&T>, и вся эта красивая картина разлетается вдребезги. Я уж молчу о том, что варианты перечислений в Rust не являются типами.

Да нет, не разлетается. Просто сишный указатель логично представлять как тип сумму: один вариант — это ненулевое целочисленное значение, другой вариант — это значение типа null (примерно так и устроено, допустим, в Java: там любая ссылка — это тип-сумма с null). Поэтому Option<&T> мапится на указатель однозначно и дополнительного места под дискриминант не требуется.

Это я к тому, что явно выделенного дискриминанта в Rust enum может и не быть.

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

варианты перечислений в Rust не являются типами

Да, это правда.

В своем комментарии я имел ввиду что — то такое (псвдокод, максимально близкий к php):
# Запись такого вида (можно рассматривать как предложение синтаксиса)
enum Foo {
    Unit,
    SomeValue = 111,
    First(int),
    Text(string),
    Point(x: float, y: float),
}
# При разборе могла бы разворачиваться в такую реализацию
# Возможно, это будет существовать только в виде AST в памяти
enum Foo {
    private enum FooKind {
        Unit,
        SomeValue,
        ...
    } __kind,
    # Размер array фиксируется наибольшей длиной списка аргументов функций
    private array __value,

    public static function Unit(): Foo {...}
    public static function SomeValue(int v): Foo {...}
    ...
    public static function Point(x: float, y: float): Foo {...}
}
Sign up to leave a comment.