(Как) я могу реализовать общий тип `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
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
Четыре эффективных способа центрирования блочных элементов в CSS
Четыре эффективных способа центрирования блочных элементов в CSS
У каждого из нас бывали случаи, когда нам нужно отцентрировать блочный элемент, но мы не знаем, как это сделать. Даже если мы реализуем какой-то...
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)
        }
    }
}

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