Sobes.tech
Senior

Czym jest suma typów i jak ją zaimplementować w Go?

sobes.tech AI

Odpowiedź od AI

Suma typów (lub unia etykietowana, rozłączna unia) to typ danych, który może przyjąć wartość jednej z kilku z góry zdefiniowanych opcji. W przeciwieństwie do iloczynu typów (struktur), gdzie wartość zawiera wszystkie pola jednocześnie, suma typów zawiera tylko jedną z możliwych wartości.

W Go nie ma bezpośredniego wsparcia dla sum typów jak w językach takich jak Haskell czy Rust (enum). Sumę typów można emulować na kilka sposobów:

  1. Interfejsy i asercja typu (Type Assertion):

    Utwórz interfejs, który implementują wszystkie możliwe warianty. Wartość sumy typów będzie tym interfejsem, a do określenia konkretnego wariantu używa się asercji typu lub type switch.

    package main
    
    import "fmt"
    
    // Shape - interfejs reprezentujący sumę typów
    type Shape interface {
    	Area() float64
    }
    
    // Circle - jeden z wariantów
    type Circle struct {
    	Radius float64
    }
    
    func (c Circle) Area() float64 {
    	return 3.14 * c.Radius * c.Radius
    }
    
    // Rectangle - inny wariant
    type Rectangle struct {
    	Width, Height float64
    }
    
    func (r Rectangle) Area() float64 {
    	return r.Width * r.Height
    }
    
    func main() {
    	shapes := []Shape{Circle{Radius: 5}, Rectangle{Width: 3, Height: 4}}
    
    	for _, s := range shapes {
    		// Użycie switcha typu do określenia wariantu
    		switch v := s.(type) {
    		case Circle:
    			fmt.Printf("Koło o promieniu %.2f, pole: %.2f\n", v.Radius, v.Area())
    		case Rectangle:
    			fmt.Printf("Prostokąt o wymiarach %.2f x %.2f, pole: %.2f\n", v.Width, v.Height, v.Area())
    		default:
    			fmt.Println("Nieznana forma")
    		}
    	}
    }
    
  2. Struktury z flagami booleanowymi (rzadko używane):

    Struktura zawiera pola dla wszystkich możliwych wariantów i flagi booleanowe wskazujące, który wariant jest aktywny. Jest to niebezpieczne i trudne do utrzymania.

    package main
    
    import "fmt"
    
    type Result struct {
    	Value int  // pole dla wyniku sukcesu
    	Err   error // pole dla błędu
    
    	IsValue bool // flaga wskazująca, czy wartość jest aktywna
    	IsErr   bool // flaga wskazująca, czy błąd jest aktywny
    }
    
    func Process(input int) Result {
    	if input > 0 {
    		return Result{Value: input * 2, IsValue: true}
    	}
    	return Result{Err: fmt.Errorf("liczba ujemna: %d", input), IsErr: true}
    }
    
    func main() {
    	res1 := Process(10)
    	if res1.IsValue {
    		fmt.Printf("Wynik: %d\n", res1.Value)
    	} else if res1.IsErr {
    		fmt.Printf("Błąd: %v\n", res1.Err)
    	}
    
    	res2 := Process(-5)
    	if res2.IsValue {
    		fmt.Printf("Wynik: %d\n", res2.Value)
    	} else if res2.IsErr {
    		fmt.Printf("Błąd: %v\n", res2.Err)
    	}
    }
    
  3. Struktury z wartością null dla nieaktywnych pól (często używane dla Optional/Result):

    Struktura zawiera pola dla wszystkich wariantów, ale tylko jedno z nich będzie nie null. Działa dobrze z wskaźnikami lub typami mającymi wyraźną "wartość null".

    package main
    
    import "fmt"
    
    // Option - emulacja Some/None z typu opcjonalnego
    type Option struct {
    	Value *int // nie null, jeśli jest wartość
    }
    
    // None tworzy nullową wartość Option
    func None() Option {
    	return Option{}
    }
    
    // Some tworzy Option z wartością
    func Some(val int) Option {
    	return Option{Value: &val}
    }
    
    func main() {
    	opt1 := Some(10)
    	if opt1.Value != nil {
    		fmt.Printf("Wartość: %d\n", *opt1.Value)
    	} else {
    		fmt.Println("Brak wartości")
    	}
    
    	opt2 := None()
    	if opt2.Value != nil {
    		fmt.Printf("Wartość: %d\n", *opt2.Value)
    	} else {
    		fmt.Println("Brak wartości")
    	}
    }
    

Najbardziej idiomatycznym i bezpiecznym sposobem emulacji sum typów w Go jest użycie interfejsów i type switch. To zapewnia, że tylko jedna z opcji jest obecna i gwarantuje bezpieczeństwo typów podczas pracy z nią.