Поговорим сегодня о новом языковом механизме языка Go, — generics или шаблонах, если говорить в терминах C++, появившемся в языке, начиная с версии 1.18. Речь пойдет не об объяснении этого механизма своими словами (по сути пересказ документации или других статей, но своими словами), а о популярном утверждении о том, что их удобно использовать для реализации обобщенных деревьев (структура данных), не приколоченных гвоздями к какому-то конкретному типу данных.

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

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

type Node struct {
   key Comparable
   color  int
   parent *Node
   left   *Node
   right  *Node
}

Пояснения тут думаю излишни, за исключением поля key — это собственно сами данные (ключи), хранящиеся в дереве. Эти данные имеют в нашем определении тип Comparable. Что же это такое? В нашем случае это интерфейс, который имеет всего два метода:

type Comparable interface {
    Less(y Comparable) bool
    Equal(y Comparable) bool
}

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

func (n *Node) search(value Comparable) (*Node, bool) {
    if value == nil {
        return nil, false
    }
    var x *Node
    x = n
    for x.isNotNil() && !value.Equal(x.key) {
        if value.Less(x.key) {
            x = x.left
        } else {
            x = x.right
        }
    }

    if x.isNil() {
        return nil, false
    }

    return x, true
}

func (n *Node) isNil() bool {
    return n == nil || n.key == nil
}

func (n *Node) isNotNil() bool {
    return n != nil && n.key != nil
}

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

 type Node[T comparable] struct {
    key T
    color  int
    parent *Node
    left   *Node
    right  *Node
}

func (n *Node[T]) search(value T) (*Node[T], bool) {
    if value == nil {
        return nil, false
    }
    var x *Node
    x = n
    for x.isNotNil() && value != x.key {
        if value < x.key {
            x = x.left
        } else {
            x = x.right
        }
    }

    if x.isNil() {
        return nil, false
    }

    return x, true
}

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

import (
    "github.com/akutz/sortfold"
    "strings"
)

// ...

type Folder struct {
    Content *FolderContent
    Path    string
}

func (x *Folder) Less(y Comparable) bool {
    return sortfold.CompareFold(x.Path, y.(*Folder).Path) < 0
}

func (x *Folder) Equal(y Comparable) bool {
    return strings.EqualFold(x.Path, y.(*Folder).Path)
}

// ...

Да, у подхода с интерфейсом тоже есть минусы, — например для системных типов, вместо непосредственного использования в дереве, необходимо делать псевдоним и реализацию интерфейса:

type Int64 int64
type String string

func (x Int64) Less(y Comparable) bool {
    return x < y.(Int64)
}

func (x Int64) Equal(y Comparable) bool {
    return x == y
}

func (x *String) Less(y Comparable) bool {
    return *x < *(y.(*String))
}

func (x *String) Equal(y Comparable) bool {
    return *x == *(y.(*String))
}

func NewString(v string) Comparable {
    s := String(v)
    return &s
}

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

Все описанное выше, можно посмотреть и использовать в своих проектах с помощью моей маленькой библиотеки godatastuct.

2024-04-25 04:52:37 UTC generics go golang noweb programming rbtree snippet