Что же такое Алгебраические Типы Данных(Algebraic Data Types(ADT))? Обычно определение состоит из терминов теории типов и обязательно с примером на Haskell. Но на практике всё не так сложно.
Типы данных
Сначала разберемся с типами данных в общем случае.
Тип данных - это допустимое множество всех значений именнованой абстракции.
Например
Самый простой тип данных bit
. Его значения: {0, 1}.
Тип bool
= {true, false}.
Тип byte
= {0,1,..,255} или {-128, -127,…, 127} или {ASCII Table}
Обычно на уровне языка реализовано несколько “примитивных” типов: логический, целочисленные, числа с плавающей запятой, строковые и указатели/ссылки.
Множество может быть пустым, а еще может состоять из одного элемента. Такие типы тоже бывают.
Единичный тип, он же unit, обычно обозначается ()
. Этот тип указывает на отсутствие определенного значения. unit
имеет только одно значение, которое выступает в качестве заполнителя, если другое значение не существует или не требуется. В C подобных языках похожую функцию выполняет void
.
Пустое множество. Тип без значения. Подобный тип данных называют ненаселенным (uninhabited type). Обычно он используется, чтобы показать невычислимость выражения, например, брошено исключение, выход из программы, бесконечный цикл, и т.п. В Rust это never type.
Но этого мало. Нужно больше. Мы хотим объединять и комбинировать типы.
Алгебраический тип
Сделаем из простых типов другие, составные и более сложные. Это и будут алгебраические тип данных. А называются они так потому, что новые типы получаются с помощью сложения и умножения.
Тип Произведения (Product Type)
Cамый простой способ получить новый тип данных на основе существующих - объединить их вместе. Такие типы реализованы через struct
, class
, tuple
, record
и пр.
Создадим новый тип произведения:
type NewProductType struct {
a bool
b byte
}
NewProductType
состоит из типа bool
И типа byte
.
NewProductType
= {true, false} * {0,1,…,255} = {(true,0),(false,0),(true,1),(false,1),..,(true,255),(false,255)}
Получаем множество/тип NewProductType
с мощностью 2*256=512.
Это то чем мы ежедневно пользуемся, создавая новые типы c помощью классов, структур и пр.
Тип Сумма (Sum Type)
Он же tagged union или просто variant. Встречается реже чем product type и обычно когда говорят про ADT имеют ввиду именно тип sum type.
Sum Type — значения этого типа могут быть одним из нескольких взаимоисключающих вариантов.
Например
Тип bool
. Его значения - это True
ИЛИ False
— два взаимоисключающих варианта.
Если смотреть в сторону rust, то это enum
.
Option
можете принимать два значения None
ИЛИ любой тип.
enum Option<T> {
None,
Some(T),
}
Растовский ответ null ссылкам. The Option Enum and Its Advantages Over Null Values
Если функция может вернуть ошибку(Err
) ИЛИ результат успешного завершения(OK
), тогда нам поможет enum Result
.
enum Result<T, E> {
Ok(T),
Err(E),
}
Pattern Matching
Остался вопрос о том как определить значения такого типа. В идеале нужен pattern matching. Гибкий и удобный механизм.
Например
Функция find
ищет указанное значение в слайсе. Нам нужен индекс найденного элемента или сообщения о том, что такого элемента в слайсе нет.
fn find(value: i32, slice: &[i32]) -> Option<usize> {
for (index, &element) in slice.iter().enumerate() {
if element == value {
// Return a value
return Some(index);
}
}
// Return no value
None
}
Используем конструкцию match
, что бы обработать полученные значения:
fn main() {
let array = [1, 2, 3, 4, 5];
match find(2, &array) {
Some(index) => println!("The element 2 is at index {}.", index),
None => println!("The element 2 is not in the array.")
}
}
Как дела в Go
В Go нет sum type в классическом виде и что более печально нет pattern matching. Подробнее почему это так можно прочитать в faq.
Если кратко, то ответ такой: в go есть интерфейсы и type switch, они не такие элегантные, гибкие и безопасные, но основные потребности перекрывают.
Пример из wiki - это бинарное дерево. Дерево может быть пустым, листом или содержать левых\правых детей:
data Tree = Empty
| Leaf Int
| Node Tree Tree
функция поиска глубины дерева:
depth :: Tree -> Int
depth Empty = 0
depth (Leaf n) = 1
depth (Node l r) = 1 + max (depth l) (depth r)
Вариант на Go:
type Tree interface {
}
type Empty struct {
}
type Leaf struct {
v int
}
type Node struct {
left, right Tree
}
func depth(t Tree) int {
switch nt := t.(type) {
case Empty:
return 0
case Leaf:
return 1
case Node:
return 1 + max(depth(nt.left), depth(nt.right))
default:
log.Fatalf("unexpected type %T", nt)
}
return 0
}
На сегодня все. Спасибо!
Комментарии (15)
NeoCode
12.10.2023 07:23Кроме типа-суммы и типа-произведения есть еще тип-объединение и тип-пересечение (я сам узнал из обсуждения к этой статье). Было бы интересно рассмотреть эти вопросы и здесь, причем в практическом аспекте.
includedlibrary
12.10.2023 07:23Судя по статье по ссылке, Union Types - это и есть сумма типов, просто в TS нужно через typeof тип проверять, а не через pattern matching
justwack Автор
12.10.2023 07:23Не стал я сильно усложнять.
Union vs sum types - тут можно посмотреть разницу на примере реализаций этих типов в Julia и Rust.
includedlibrary
12.10.2023 07:23На примере Rust по идее не вышло бы про типы-объединения рассказать, так как в нём они не поддерживаются, потому что Rust компилируемый язык, а при компиляции всегда нужно знать, какой тип у переменной. То есть если в интерпретируемых языках я могу сделать проверку вида:
typeof(var) == "int"
, то в компилируемых языках для этого придётся использовать теги, что собственно в типах-суммах и делается.Jianke
12.10.2023 07:23В расте разве отсутствует сишный typeid().name() для проверки типа?! O_O
(с растом не знаком, знаю только, что его создали как желание улучшить си)
includedlibrary
12.10.2023 07:23В си такого нету. Есть typeof, но он работает только потому что компилятор знает тип переменной. Что-то вроде такого:
int var = 123;
typeof(var) y = 345;
Нормального примера использования typeof сходу не вспомнил.
Jianke
12.10.2023 07:23Под "сишкой" имеется в виду С++.
includedlibrary
12.10.2023 07:23Я с крестами не особо знаком, но разве typeid не во время компиляции вычисляется?
UPD:
Если брать union в c++, то там нет возможности определить какой член является установленным, насколько я знаю:
union MyUnion { int x; double y; } void myfunc(MyUnion u) { // Можно ли понять без тега, какой член в u был // установлен? }
DarkEld3r
12.10.2023 07:23В расте разве отсутствует сишный typeid().name() для проверки типа?! O_O
Есть и
std::any::type_name
иTypeId::of()
, но и в расте и в С++ это никак не поможет для определения, что именно сейчас находится внутриunion
— в рантайме этой информации нет.
DarkEld3r
12.10.2023 07:23На примере Rust по идее не вышло бы про типы-объединения рассказать, так как в нём они не поддерживаются, потому что Rust компилируемый язык, а при компиляции всегда нужно знать, какой тип у переменной
На всякий случай, уточню, что
union
аналигичный сишному в расте есть. Мне кажется, что этого достаточно чтобы продемонстрировать типы-объединения. Я бы даже сказал, что получение реального типа в рантайме из метаинформации — это читерство. Ведь по факту у нас тег есть, просто его хитро хранит рантайм.
Joysi
Опечатка? мощностью 2*256=512 ?
justwack Автор
ага, спасибо! исправил.