Мощь и сложность Union(Enum) в Zig

Эд Ю (@edyu на Github и @edyu в Twitter) 13 июня 2023 г.

Введение

Zig — это современный язык системного программирования, и, хотя он претендует на звание лучшего C, многих людей, изначально не нуждавшихся в системном программировании, он привлек простота его синтаксиса по сравнению с альтернативами, такими как C++ или Rust.

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

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

АТД

Одной из наиболее широко используемых функций и базовой основой системы типов Haskell является ADT или Алгебраические типы данных (не путают с Абстрактными типами данных). Вы можете посмотреть разницу на StackOverflow.

Однако для нас, программистов, вы можете просто думать об Абстрактных типах данных либо как о структуре, либо как о простом классе (простом, как и не вложенном). .

Для ADT или алгебраических типов данных нам нужен доступ к union для тех, кто сталкивался с ним раньше в языках, предоставляющих такую ​​конструкцию, как C или в нашем случае Zig.

Примечание. Чтобы ADT назывался Algebraic, он должен поддерживать как сумму, так и произведение. . Sum означает, что тип должен поддерживать A или B, но не оба вместе, тогда как product означает, что тип должен поддерживать A и B вместе.

Почему мы заботимся?

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

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

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

Однако, чтобы показать, как мы можем использовать ADT в Zig, мы должны сначала объяснить некоторые другие концепции.

Зиг-структура

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

struct в Zig, вероятно, наиболее близка к классу в большинстве объектно-ориентированных языков программирования.

Вот основная идея:

// if you want to try yourself, you must import `std`
const std = @import("std");
// let's construct a binary tree node
const BinaryTree = struct {
    // a binary tree has a left subtree and a right subtree
    left: ?*BinaryTree,
    // for simplicity, let's just say we have an unsigned 32-bit integer value
    value: u32,
    right: ?*BinaryTree,
};
const tree = BinaryTree{ .left = null, .value = 42, .right = null };

В приведенном выше коде есть несколько замечаний:

  1. Если вы не знакомы с ?, добро пожаловать на Zig If - WTF. В основном это означает, что переменная может либо иметь значение типа после ?, либо, если это не так, она примет значение null.
  2. Мы имеем в виду тип BinaryTree внутри определения типа BinaryTree, поскольку дерево является рекурсивной структурой.
  3. Однако вы должны использовать * для обозначения того, что left и right являются указателями на другую структуру BinaryTree. Если вы пропустите указатель, компилятор будет жаловаться, потому что тогда размер BinaryTree является динамическим, поскольку он может увеличиваться до произвольного размера по мере добавления дополнительных поддеревьев.

Следующий код покажет немного более сложную древовидную структуру. Обратите внимание, что мы должны использовать &, чтобы получить указатель на структуру BinaryTree.

var left = BinaryTree{ .left = null, .value = 21, .right = null };
    var far_right = BinaryTree{ .left = null, .value = 168, .right = null };
    var right = BinaryTree{ .left = null, .value = 84, .right = &far_right };
    const tree2 = BinaryTree{ .left = &left, .value = 42, .right = &right };

Зиг Энум

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

// sorry if I left our your favorite pet
const Pet = enum { Dog, Cat, Fish, Iguana, Platypus };
const fav: Pet = .Cat;
// Each of the value of an enum is called a tag
std.debug.print("Ed's favorite pet is {s}.\n", .{@tagName(Pet.Cat)});
// you can specify what type and what value the enum takes
const Binary = enum(u1) { Zero = 0, One = 1 };
std.debug.print("There are {d}{d} types of people in this world, those understand binary and those who don't.\n", .{
    @enumToInt(Binary.One),
    @enumToInt(Binary.Zero)
});

Включить перечисление

Одной из наиболее удобных конструкций для enum является выражение switch. В Haskell причина, по которой ADT так полезна, заключается в возможности сопоставления шаблонов в выражении switch. На самом деле определение функции в Haskell — это, по сути, суперзаряженный оператор switch.

Так как же использовать оператор switch в Zig?

const fav: Pet = .Cat;
std.debug.print("{s} is ", .{@tagName(fav)});
switch (fav) {
    .Dog => std.debug.print("needy!\n", .{}),
    .Cat => std.debug.print("perfect!\n", .{}),
    .Fish => std.debug.print("so much work!\n", .{}),
    .Iguana => std.debug.print("not tasty!\n", .{}),
    else => std.debug.print("legal?\n", .{}),
}
const score = switch (fav) {
    .Dog => 50,
    .Cat => 100,
    .Fish => 25,
    .Iguana => 15,
    else => 75,
};

Союз

В C и в Zig union похож на struct, за исключением того, что вместо структуры, имеющей все поля , только одно из полей объединения активно. Те, кто знаком с объединением C, должны иметь в виду, что объединение Zig нельзя использовать для переинтерпретации памяти. Другими словами, вы не можете использовать одно поле объединения для приведения значения, определенного другим типом поля.

const Value = union {
    int: i32,
    float: f64,
    string: []const u8,
};
var value = Value{ .int = 42 };
// you can't do this
var fval = value.float;
std.debug.print("{d}\n", .{fval});
// you can't do this, either
var bval = value.string;
std.debug.print("{c}\n", .{bval[0]});

Включить Союз

Ну, вы не можете использовать switch в union; по крайней мере, не в простом union.

// won't compile
switch (value) {
    .int => std.debug.print("value is int={d}\n", .{value.int}),
    .float => std.debug.print("value is float={d}\n", .{value.float}),
    .string => std.debug.print("value is string={s}!\n", .{value.string}),
}

Union(Enum) - это союз с тегами

Сообщение об ошибке в предыдущем примере на самом деле будет говорить: note: consider 'union(enum)' here.

Номенклатура Zig для union(enum) на самом деле называется тегированным объединением. Как мы упоминали ранее, отдельные поля enum называются тегами.

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

// first define the tags
const ValueType = enum {
    int,
    float,
    string,
    unknown,
};
// not too different from simple union
const Value = union(ValueType) {
    int: i32,
    float: f64,
    string: []const u8,
    unknown: void,
};
// just like the simple union
var value = Value{ .float = 42.21 };
switch (value) {
    .int => std.debug.print("value is int={d}\n", .{value.int}),
    .float => std.debug.print("value is float={d}\n", .{value.float}),
    .string => std.debug.print("value is string={s}\n", .{value.string}),
    else => std.debug.print("value is unknown!\n", .{}),
}

Захват помеченного значения объединения

Вы можете использовать захват в выражении switch, если вам нужно получить доступ к значению.

switch (value) {
    .int => |v| std.debug.print("value is int={d}\n", .{v}),
    .float => |v| std.debug.print("value is float={d}\n", .{v}),
    .string => |v| std.debug.print("value is string={s}\n", .{v}),
    else => std.debug.print("value is unknown!\n", .{}),
}

Изменить объединение с тегами

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

switch (value) {
    .int => |*v| v.* += 1,
    .float => |*v| v.* ^= 2,
    .string => |*v| v.* = "I'm not Ed",
    else => std.debug.print("value is unknown!\n", .{}),
}

Отмечен союз как ADT

Теперь у нас есть все необходимое для реализации Zig версии ADT. Что делает ADT полезным, так это то, что он сообщит вам не только о состоянии, но и о контексте состояния.

Например, при использовании Zig активный тег в union сообщит вам о состоянии, и если тег является типом, имеющим значение, то значение является контекстом.

// this example is fairly involved, please see the full code on github
// You can find the code at https://github.com/edyu/wtf-zig-adt/blob/master/testadt.zig
const NodeType = enum {
    tip,
    node,
};
const Tip = struct {};
const Node = struct {
    left: *const Tree,
    value: u32,
    right: *const Tree,
};
const Tree = union(NodeType) {
    tip: Tip,
    node: *const Node,
}
const leaf = Tip{};
// this is meant to reimplement the binary tree example on https://wiki.haskell.org/Algebraic_data_type
// if you call tree.toString(), it will print out:
// Node (Node (Node (Tip 1 Tip) 3 Node (Tip 4 Tip)) 5 Node (Tip 7 Tip))
const tree = Tree{ .node = &Node{
    .left = &Tree{ .node = &Node{
        .left = &Tree{ .node = &Node{
            .left = &Tree{ .tip = leaf },
            .value = 1,
            .right = &Tree{ .tip = leaf } } },
        .value = 3,
        .right = &Tree{ .node = &Node{
            .left = &Tree{ .tip = leaf },
            .value = 4,
            .right = &Tree{ .tip = leaf } } } } },
    .value = 5,
    .right = &Tree{ .node = &Node{
        .left = &Tree{ .tip = leaf },
        .value = 7,
        .right = &Tree{ .tip = leaf } } } } };
// see the full example on github

Бонус

В Zig также есть нечто, называемое неполное перечисление.

Неполное перечисление должно быть определено с целочисленным типом тега в (). Затем вы помещаете _ в качестве последнего тега в определение enum.

Вместо else вы можете использовать _, чтобы убедиться, что вы обработали все случаи в выражении switch.

const Eds = enum(u8) {
    Ed,
    Edward,
    Edmond,
    Eduardo,
    Edwin,
    Eddy,
    Eddie,
    _,
};
const ed = Eds.Ed;
std.debug.print("All your code are belong to ", .{});
switch (ed) {
    // Zig switch uses , not | for multiple options
    .Ed, .Edward => std.debug.print("{s}!\n", .{@tagName(ed)}),
    // can use capture
    .Edmond, .Eduardo, .Edwin, .Eddy, .Eddie => |name| std.debug.print("this {s}!\n", .{@tagName(name)}),
    // else works but look at the code below for _ vs else
    else => std.debug.print("us\n", .{}),
}
// obviously no such enum predefined
const not_ed = @intToEnum(Eds, 241);
std.debug.print("All your base are belong to ", .{});
switch (not_ed) {
    .Ed, .Edward => std.debug.print("{s}!\n", .{@tagName(ed)}),
    .Edmond, .Eduardo, .Edwin, .Eddy, .Eddie => |name| std.debug.print("this {s}!\n", .{@tagName(name)}),
    // _ will force you to handle all defined cases
    // if any of the previous .Ed, .Edward ... .Eddie is missing, this won't compile
    // for example, if you forgot .Edurdo
    // and wrote: .Edmond, .Eduardo, .Edwin, .Eddy, .Eddie => ...
    // the code won't compile
    _ => std.debug.print("us\n", .{}),
}

Кстати, вы можете добавлять функции в enum, union, union(enum) так же, как и в struct. Вы можете увидеть примеры этого в коде ниже.

Конец

Код можно найти здесь.