(Как) я могу реализовать общий тип `Ether` в go?

Я подумал, что с новыми дженериками в Go 1.18 можно создать тип «Either[A,B]», который можно использовать для выражения того, что что-то может быть либо типа A, либо типа B.

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

Я знаю, что «идиоматический» Go для ошибок будет возвращать как «нормальное» значение, так и значение ошибки, возвращая nil либо для ошибки, либо для значения. Но... меня немного беспокоит, что мы, по сути, говорим "это возвращает A и B" в типе, тогда как на самом деле мы хотим сказать "это возвращает A или B".

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

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

//A value of type `Either[A,B]` holds one value which can be either of type A or type B.
type Either[A any, B any] interface {

    // Call either one of two functions depending on whether the value is an A or B
    // and return the result.
    Switch[R any]( // <=== ERROR: interface methods must have no type parameters
        onA func(a A) R),
        onB func(b B) R),
    ) R
}

К сожалению, это довольно быстро терпит неудачу, потому что объявление этого интерфейса не разрешено Go. Очевидно, потому что «методы интерфейса не должны иметь параметров типа».

Как обойти это ограничение? Или просто нет способа создать «тип» в Go, который бы точно выражал идею о том, что «эта вещь является/возвращает либо A, либо B» (в отличие от кортежа из A и B).

тип Либо[A любой, B любой, R любой] .

Volker 09.04.2022 19:35
Either[A any, B any, R any] идея интересная, но мне кажется нелогичной. Тип R не имеет ничего общего с выражением идеи о том, что «вещь, которую мы здесь возвращаем, является либо A, либо B», поэтому, когда мы делаем значение «A or B», мы должны указать также третий нерелевантный тип, который делает нет смысла на самом деле.
Kris 09.04.2022 19:49
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
1
2
61
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Решение наконец пришло ко мне. Ключом было определение типа «Любой» как «структуры» вместо интерфейса.

type Either[A any, B any] struct {
    isA bool
    a   A
    b   B
}

func Switch[A any, B any, R any](either Either[A, B],
    onA func(a A) R,
    onB func(b B) R,
) R {
    if either.isA {
        return onA(either.a)
    } else {
        return onB(either.b)
    }
}

func MakeA[A any, B any](a A) Either[A, B] {
    var result Either[A, B]
    result.isA = true
    result.a = a
    return result
}

func MakeB[A any, B any](b B) Either[A, B] {
  ... similar to MakeA...
}

Это работает, но ценой того, что на самом деле все еще используется «кортежеподобная» реализация под капотом, когда мы храним как A и, так и B, но гарантируем, что только один из них можно использовать через общедоступный API.

Я подозреваю, что это лучшее, что мы можем сделать, учитывая ограничения, которые Go накладывает на нас.

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

Хранение логического значения для различения типов кажется излишним.

colm.anseo 09.04.2022 20:19

Тогда как бы вы выбрали, какую из двух функций вызывать?

Kris 09.04.2022 20:20

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

colm.anseo 09.04.2022 20:27

Пример с ошибкой просто дал понять, почему вы можете захотеть сделать что-то подобное, но я искал что-то, что может выражать «либо A, либо B» в более общем смысле. Например, «эта функция возвращает либо число, либо строку» Either[int,string].

Kris 10.04.2022 00:11

С практической точки зрения, я думаю, что вы, вероятно, сделали бы в реальных случаях, когда вам нужно что-то вроде «Either[int,string]», было бы просто определить его как возвращаемое any и использовать переключатель типа для работы со значением. Но если два разных типа вещей, которые вы возвращаете, не имеют общего интерфейса, который характеризует их обоих, кажется, что go не предлагает никакого способа действительно сказать, что ваша функция «может вернуть либо то, либо это». Такая точность просто невозможна в системе типов go. Если go было «английским», это как если бы у них было слово для «и», но забыли добавить слово для «или».

Kris 10.04.2022 00:28
Ответ принят как подходящий

Either можно смоделировать как тип структуры с одним неэкспортируемым полем типа any/interface{}. Параметры типа будут использоваться для обеспечения некоторой степени безопасности типов во время компиляции:

type Either[A, B any] struct {
    value any
}

func (e *Either[A,B]) SetA(a A) {
    e.value = a
}

func (e *Either[A,B]) SetB(b B) {
    e.value = b
}

func (e *Either[A,B]) IsA() bool {
    _, ok := e.value.(A)
    return ok
}

func (e *Either[A,B]) IsB() bool {
    _, ok := e.value.(B)
    return ok
}

Если Switch должен быть объявлен как метод, он не может быть параметризован в R сам по себе. Дополнительный параметр типа должен быть объявлен в определении типа, однако это может сделать использование немного громоздким, поскольку тогда при создании экземпляра необходимо выбрать R.

Отдельная функция кажется лучше — в том же пакете для доступа к неэкспортированному полю:

func Switch[A,B,R any](e *Either[A,B], onA func(A) R, onB func(B) R) R {
    switch v := e.value.(type) {
        case A:
            return onA(v)
        case B:
            return onB(v)
    }
}

Игровая площадка с некоторым кодом и использованием: https://go.dev/play/p/g-NmE4KZVq2

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

Kris 10.04.2022 00:00

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

package main

import (
    "errors"
    "fmt"
    "os"
)

type Optional[T any] interface {
    get() (T, error)
}

type None[T any] struct {
}

func (None[T]) get() (T, error) {
    var data T
    return data, errors.New("No data present in None")
}

type Some[T any] struct {
    data T
}

func (s Some[T]) get() (T, error) {
    return s.data, nil
}

func CreateNone[T any]() Optional[T] {
    return None[T]{}
}

func CreateSome[T any](data T) Optional[T] {
    return Some[T]{data}
}

type Either[A, B any] interface {
    is_left() bool
    is_right() bool
    find_left() Optional[A]
    find_right() Optional[B]
}

type Left[A, B any] struct {
    data A
}

func (l Left[A, B]) is_left() bool {
    return true
}

func (l Left[A, B]) is_right() bool {
    return false
}

func left[A, B any](data A) Either[A, B] {
    return Left[A, B]{data}
}

func (l Left[A, B]) find_left() Optional[A] {
    return CreateSome(l.data)
}

func (l Left[A, B]) find_right() Optional[B] {
    return CreateNone[B]()
}

type Right[A, B any] struct {
    data B
}

func (r Right[A, B]) is_left() bool {
    return false
}

func (r Right[A, B]) is_right() bool {
    return true
}

func right[A, B any](data B) Either[A, B] {
    return Right[A, B]{data}
}

func (r Right[A, B]) find_left() Optional[A] {
    return CreateNone[A]()
}

func (r Right[A, B]) find_right() Optional[B] {
    return CreateSome(r.data)
}

func main() {
    var e1 Either[int, string] = left[int, string](4143)
    var e2 Either[int, string] = right[int, string]("G4143")
    fmt.Println(e1)
    fmt.Println(e2)
    if e1.is_left() {
        if l, err := e1.find_left().get(); err == nil {
            fmt.Printf("The int is: %d\n", l)
        } else {
            fmt.Fprintln(os.Stderr, err)
        }
    }
    if e2.is_right() {
        if r, err := e2.find_right().get(); err == nil {
            fmt.Printf("The string is: %s\n", r)
        } else {
            fmt.Fprintln(os.Stderr, err)
        }
    }
}

Другие вопросы по теме