Sobes.tech
Senior

What is a sum of types and how to implement it in Go?

sobes.tech AI

Answer from AI

Sum types (or tagged unions, disjoint unions) are data types that can hold a value of one of several predefined options. Unlike product types (structures), where the value contains all fields simultaneously, sum types contain only one of the possible values.

In Go, there is no direct support for sum types like in languages such as Haskell or Rust (enums). Sum types can be emulated in several ways:

  1. Interfaces and type assertion:

    Create an interface that all possible options will implement. The value of the sum type will be represented by this interface, and a type assertion or type switch is used to determine the specific variant.

    package main
    
    import "fmt"
    
    // Shape - interface representing the sum type
    type Shape interface {
    	Area() float64
    }
    
    // Circle - one variant
    type Circle struct {
    	Radius float64
    }
    
    func (c Circle) Area() float64 {
    	return 3.14 * c.Radius * c.Radius
    }
    
    // Rectangle - another variant
    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 {
    		// Using type switch to determine the variant
    		switch v := s.(type) {
    		case Circle:
    			fmt.Printf("Circle with radius %.2f, area: %.2f\n", v.Radius, v.Area())
    		case Rectangle:
    			fmt.Printf("Rectangle with dimensions %.2f x %.2f, area: %.2f\n", v.Width, v.Height, v.Area())
    		default:
    			fmt.Println("Unknown shape")
    		}
    	}
    }
    
  2. Structures with boolean flags (rarely used):

    A structure contains fields for all possible options and boolean flags to indicate which option is active. This approach is unsafe and hard to maintain.

    package main
    
    import "fmt"
    
    type Result struct {
    	Value int  // field for successful result
    	Err   error // field for error
    
    	IsValue bool // flag indicating if Value is active
    	IsErr   bool // flag indicating if Err is active
    }
    
    func Process(input int) Result {
    	if input > 0 {
    		return Result{Value: input * 2, IsValue: true}
    	}
    	return Result{Err: fmt.Errorf("negative number: %d", input), IsErr: true}
    }
    
    func main() {
    	res1 := Process(10)
    	if res1.IsValue {
    		fmt.Printf("Result: %d\n", res1.Value)
    	} else if res1.IsErr {
    		fmt.Printf("Error: %v\n", res1.Err)
    	}
    
    	res2 := Process(-5)
    	if res2.IsValue {
    		fmt.Printf("Result: %d\n", res2.Value)
    	} else if res2.IsErr {
    		fmt.Printf("Error: %v\n", res2.Err)
    	}
    }
    
  3. Structures with zero value for inactive fields (often used for Optional/Result):

    A structure contains fields for all options, but only one will be non-zero. Works well with pointers or types with a clear "zero" value.

    package main
    
    import "fmt"
    
    // Option - emulation of Some/None from optional type
    type Option struct {
    	Value *int // non-nil if there is a value
    }
    
    // None creates a zero value Option
    func None() Option {
    	return Option{}
    }
    
    // Some creates an Option with a value
    func Some(val int) Option {
    	return Option{Value: &val}
    }
    
    func main() {
    	opt1 := Some(10)
    	if opt1.Value != nil {
    		fmt.Printf("Value exists: %d\n", *opt1.Value)
    	} else {
    		fmt.Println("No value")
    	}
    
    	opt2 := None()
    	if opt2.Value != nil {
    		fmt.Printf("Value exists: %d\n", *opt2.Value)
    	} else {
    		fmt.Println("No value")
    	}
    }
    

The most idiomatic and safe way to emulate sum types in Go is using interfaces and type switch. This guarantees that only one variant is present and provides type safety when working with it.