From 807a25b638e38d7c0446bb728412fb16fd035035 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Sat, 4 Jan 2025 15:08:53 +0100 Subject: init --- option/option.go | 362 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 option/option.go (limited to 'option/option.go') diff --git a/option/option.go b/option/option.go new file mode 100644 index 0000000..1ea2e79 --- /dev/null +++ b/option/option.go @@ -0,0 +1,362 @@ +/******************************************************************************* +* Copyright 2025 Stefan Majewsky +* SPDX-License-Identifier: Apache-2.0 +* Refer to the file "LICENSE" for details. +*******************************************************************************/ + +// Package optional provides an Option type for Go. +// A value of the Option type will be in one of two states: "Some" (containing a value) or "None" (containing no value). +// +// The purpose of the Option type is to more clearly distinguish the two situations that standard Go uses pointer types for: +// - to provide a way to edit the inside of a value without changing the value itself +// - to allow for a value to be either present or absent (with absence being represented as "nil") +// +// Using the Option type, pointers may be reserved for the first purpose. +// For instance, a type like *int32 would clearly represent an editable value, whereas Option[int32] clearly represents an optional value. +// +// # Clean import guarantee +// +// This package is supposed to be used as a dot-import: +// +// import . "github.com/majewsky/go-option" +// +// To avoid backwards incompatibilities, we guarantee that, at least throughout the 1.x series, +// newer versions of this package will never export any more names than it currently does ("None", "Option" and "Some"). +// +// # Marshalling considerations +// +// Marshaling into and from YAML using https://github.com/go-yaml/yaml is supported. +// The "omitempty" flag works as expected. +// +// Marshaling into and from JSON using encoding/json is supported, but the "omitempty" flag does not work. +// You must use the "omitzero" flag to get the same effect, but note that this flag is only supported by Go 1.24 and newer. +// +// # How to replace pointer types with Option types +// +// This abridged example shows the most common ways to interact with pointer types that represent optional values: +// +// type ServerConfiguration struct { +// CrashLogPath *string +// ListenAddress *string +// ThreadCount *uint64 +// } +// +// func RunServer(cfg ServerConfiguration) { +// // inform about a value being absent +// if cfg.CrashLogPath == nil { +// log.Print("crash logging disabled because no CrashLogPath is given") +// } +// +// // fill a default value if nil is given +// listenAddress := "127.0.0.1:8080" +// if cfg.ListenAddress != nil { +// listenAddress = *listenAddress +// } +// +// // access contained value if present, or proceed without contained value if absent +// if cfg.ThreadCount != nil { +// startMultiThreadedServer(listenAddress, *cfg.ThreadCount) +// } else { +// startSingleThreadedServer(listenAddress) +// } +// } +// +// This is how the same code snippet looks when replacing the pointer types with Option types and taking advantage of the methods on type Option: +// +// type ServerConfiguration struct { +// CrashLogPath Option[string] +// ListenAddress Option[string] +// ThreadCount Option[uint64] +// } +// +// func RunServer(cfg ServerConfiguration) { +// // "if x == nil" becomes "if x.IsNone()"; the opposite check is called IsSome() +// if cfg.CrashLogPath.IsNone() { +// log.Print("crash logging disabled because no CrashLogPath is given") +// } +// +// // default values can be filled with UnwrapOr() +// listenAddress := cfg.ListenAddress.UnwrapOr("127.0.0.1:8080") +// +// // Unpack() provides the contained value and a success flag, similar to the double-return-value form of map indexing +// if threadCount, ok := cfg.ThreadCount.Unpack(); ok { +// startMultiThreadedServer(listenAddress, *cfg.ThreadCount) +// } else { +// startSingleThreadedServer(listenAddress) +// } +// } +// +// # Differences to Rust +// +// This package's API is obviously modeled after the Option type in the Rust standard library, but with some exceptions. +// +// - Go does not allow to introduce additional type parameters for individual methods. +// Any methods that, in Rust, introduce the second type parameter U, cannot be represented in Go. +// Some methods like and() or zip() could be allowed if the argument is restricted to Option[T] instead of Option[U], +// but this restriction degrades their usefulness beyond reasonable limits. +// - Go does not allow to introduce additional type restrictions in individual methods. +// This makes methods like unzip() or cloned() unrepresentable in Go. +// We might make these available as free-standing functions in the future, but if we do, +// they will definitely not be in this package (see "Clean import guarantee" above). +// - Mixing of struct receiver methods and pointer receiver methods on the same type is discouraged to avoid unintentional copies and data races. +// Since most of the useful methods require only a struct receiver, we forego those that require a pointer receiver, like get_or_insert() or take(). +// The only exception to this is the methods implementing Unmarshaler interfaces, where concurrency bugs are very unlikely. +// +// Finally, the unwrap() function is not provided since its meaningless error message is not helpful in most contexts. +// We only provide expect(), but under the different name UnwrapOrPanic(), since expect() reads terribly in context. Consider the following example: +// +// dbURL := GetDatabaseURLFromEnvironment().Expect("no DB connection found") +// // ^ This reads like we *expect* to not find a DB connection, even though the opposite is true. +// +// dbURL := GetDatabaseURLFromEnvironment().UnwrapOrPanic("no DB connection found") +// // ^ This is a much clearer phrasing. +package option + +import ( + "encoding/json" + "fmt" + "iter" +) + +// Option is a type that contains either one or no instances of T. +type Option[T any] struct { + value T + isSome bool +} + +//////////////////////////////////////////////////////////////////////////////// +// constructors + +// None constructs an Option instance that contains no value. +func None[T any]() Option[T] { + var empty T + return Option[T]{empty, false} +} + +// Some constructs an Option instance that contains the provided value. +func Some[T any](value T) Option[T] { + return Option[T]{value, true} +} + +// NOTE: Cannot use options.FromPointer() here because of import cycle. +func fromPointer[T any](value *T) Option[T] { + if value == nil { + return None[T]() + } else { + return Some(*value) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// core API (methods sorted by name) + +// AsPointer converts this Option into a pointer type. +// +// Usage of this function is discouraged because it breaks the clear distinction +// between interior mutability (pointer) vs. optionality (Option). +// It is provided for when an Option value needs to be passed to a library +// function that requires a pointer value. +func (o Option[T]) AsPointer() *T { + if o.isSome { + return &o.value + } else { + return nil + } +} + +// AsSlice returns a slice with either the contained value or nothing in it. +func (o Option[T]) AsSlice() []T { + if o.isSome { + return []T{o.value} + } else { + return nil + } +} + +// Filter removes the contained value (if any) from the Option if it does not match the predicate. +func (o Option[T]) Filter(predicate func(T) bool) Option[T] { + if o.isSome && predicate(o.value) { + return o + } else { + return None[T]() + } +} + +// IsNone returns whether the Option contains no value. +// Its inverse is IsSome(). +func (o Option[T]) IsNone() bool { + return !o.isSome +} + +// IsSome returns whether the Option contains a value. +// Its inverse is IsNone(). +func (o Option[T]) IsSome() bool { + return o.isSome +} + +// IsSomeAnd returns whether the Option contains a value that matches the given predicate. +func (o Option[T]) IsSomeAnd(predicate func(T) bool) bool { + return o.isSome && predicate(o.value) +} + +// IsNoneOr returns whether the Option is either empty, or contains a value that matches the given predicate. +func (o Option[T]) IsNoneOr(predicate func(T) bool) bool { + return !o.isSome || predicate(o.value) +} + +// Iter returns an iterator that yields the contained value once (if any). +// If the Option is empty, the iterator yields nothing. +func (o Option[T]) Iter() iter.Seq[T] { + if o.isSome { + return func(yield func(T) bool) { yield(o.value) } + } else { + return func(yield func(T) bool) {} + } +} + +// Or returns the option itself if it contains a value, or otherwise returns "other". +// +// If you are passing the result of a function call, consider using OrElse() to avoid calling the function unless necessary. +func (o Option[T]) Or(other Option[T]) Option[T] { + if o.isSome { + return o + } else { + return other + } +} + +// Or returns the option itself if it contains a value, or otherwise runs the provided closure to produce the return value. +func (o Option[T]) OrElse(closure func() Option[T]) Option[T] { + if o.isSome { + return o + } else { + return closure() + } +} + +// Unpack returns the contained value (or the zero value if None), as well as if there was a contained value. +func (o Option[T]) Unpack() (T, bool) { + return o.value, o.isSome +} + +// UnwrapOr returns the contained value. +// If the Option is empty, the provided fallback value is returned instead. +func (o Option[T]) UnwrapOr(fallback T) T { + if o.isSome { + return o.value + } else { + return fallback + } +} + +// UnwrapOrElse returns the contained value. +// If the Option is empty, the provided closure is used to produce the return value. +func (o Option[T]) UnwrapOrElse(closure func() T) T { + if o.isSome { + return o.value + } else { + return closure() + } +} + +// UnwrapOrPanic returns the contained value, or panics with the given error message if it is empty. +func (o Option[T]) UnwrapOrPanic(msg any) T { + if o.isSome { + return o.value + } else { + panic(msg) + } +} + +// UnwrapOrPanicf is a shorthand for UnwrapOrPanic(fmt.Sprintf(msg, args...)). +func (o Option[T]) UnwrapOrPanicf(msg string, args ...any) T { + if o.isSome { + return o.value + } else { + panic(fmt.Sprintf(msg, args...)) + } +} + +// Xor returns an option containing a value if exactly one of the two given options contains a value, or None otherwise. +func (o Option[T]) Xor(other Option[T]) Option[T] { + if o.isSome == other.isSome { + return None[T]() + } else { + return o.Or(other) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// formatting/marshalling support + +// Format implements the fmt.Formatter interface. +// +// If there is a contained value, it will be formatted as if it was given directly. +// Otherwise, the string "" will be formatted according to the specified width and flags. +func (o Option[T]) Format(f fmt.State, verb rune) { + if o.isSome { + fmt.Fprintf(f, fmt.FormatString(f, verb), o.value) + } else { + fmt.Fprintf(f, fmt.FormatString(f, 's'), "") + } +} + +// IsZero implements the IsZeroer interface as understood by encoding/json and github.com/go-yaml/yaml. +// It is an alias of IsNone(). +func (o Option[T]) IsZero() bool { + return !o.isSome +} + +type yamlMarshaler interface { + MarshalYAML() (any, error) +} + +// MarshalJSON implements the json.Marshaler interface. +func (o Option[T]) MarshalJSON() ([]byte, error) { + if o.isSome { + return json.Marshal(o.value) + } else { + return []byte("null"), nil + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (o *Option[T]) UnmarshalJSON(buf []byte) error { + var data *T + err := json.Unmarshal(buf, &data) + if err != nil { + return err + } + *o = fromPointer(data) + return nil +} + +// MarshalYAML implements the yaml.Marshaler interface. +func (o Option[T]) MarshalYAML() (any, error) { + if o.isSome { + // If we just return o.value directly here, MarshalYAML will not be called + // on the value even if it exists. For this one specific case, we have to + // take care ourselves. + if m, ok := any(o.value).(yamlMarshaler); ok { + return m.MarshalYAML() + } else { + return o.value, nil + } + } else { + return nil, nil + } +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +// +// This function signature is compatible with both v2 and v3 of github.com/go-yaml/yaml, +// so we intentionally do not use the v3-only signature that refers to the yaml.Node type. +func (o *Option[T]) UnmarshalYAML(unmarshal func(any) error) error { + var data *T + err := unmarshal(&data) + if err != nil { + return err + } + *o = fromPointer(data) + return nil +} -- cgit v1.2.3