sealed

package module
v0.0.0-...-d2a1c7e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 12, 2024 License: MIT Imports: 3 Imported by: 0

README

Sealed Slices and Maps for Go

Package sealed is a small library implementing immutable slices and maps for Golang.

In other languages this is also known as constant vectors, read-only collections, persistent data-structures, final instances, or frozen lists and hash maps.

Usage

import (
	"cmp"
	"fmt"
	"github.com/kamstrup/sealed"
)

// Sealed slices are created via a fluent API based on sealed.Builder
s := sealed.NewBuilder[string](0, 10). // allocate builder with len=0, cap=10
  Append("hello", "world"). // var args
  Collect(seq). // Append a Go 1.23 iterator
  Sort(cmp.Compare[string]).
  Seal() // converts the builder into a sealed.Slice[string]

// Print the elements in the slice
for _, str := range s.All {
	fmt.Println(str)
}

Motivation

Sealed has grown out of the need for having const slices and maps. After writing a lot of Go code, in particular in big projects with varying team members, it has become clear to me that maps and slices are not well suited for public APIs in Go. Neither as parameters nor return values.

Slices and maps seem so simple and easy to use, but there are so many subtle pitfalls around ownership and life cycle, making them extremely hard to use correctly everywhere in the long run.

Consider a simple function

func (udb *UserDatabase) AddUsers(users []User) error {
	// do stuff with users
}

If the UserDatabase wants to hold on to the users slice after AddUsers returns, it must copy it. On a small team it might, for example, be possible to agree (and document) that AddUsers steals the users slice, for performance reasons, and callers should take heed and never use the users slice again after the call.

However; with time, complexity, and changing developers, this is guaranteed to lead to tricky data sharing bugs. No matter the clear warnings in the docs and the thoroughness of the code reviews.

Similarly, if we have a function that returns a slice of values

func (udb *UserDatabase) Users() []Users {
	return udb.users
}

The developer team must agree through docs and conventions that no one ever changes the returned slice from Users()! Given time, complexity, and changing team members, this assumption will inevitably break. The only recourse is to always copy the returned slice.

To put it short: If you want to be absolutely safe about the integrity of your slices you must copy them every time they cross an API boundary! The same thing goes for maps. If you are dealing with performance sensitive software, this can be a severe restriction.

In an ideal world where "constness" could be enforced by the Go compiler, AddUsers() would look something like the following

// not valid Go
func (udb *UserDatabase) AddUsers(users const [] const User) error {
	// do stuff with users
}

The first const indicating that the wrapping slice cannot be modified, and the second const signifying that the User elements in the slice cannot be changed either.

This library, sealed, deals with the first of these consts. We can use a sealed.Slice[User] to efficiently pass a read-only slice of users to the function. The second const on the User struct itself must still be solved by the developer team themselves. Fx. by ensuring that the User struct is effectively immutable, having no public fields and no mutator methods.

The completely safe variants of the discussed methods looks like

func (udb *UserDatabase) AddUsers(users sealed.Slice[User]) error {
	// we can hold on to the users slice if we want, no need to copy because it is read-only.
}

func (udb *UserDatabase) Users() sealed.Slice[User] {
    // callers may hold on to the users slice we return, no need to copy because it is read-only.
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Builder

type Builder[T any] struct {
	// contains filtered or unexported fields
}

Builder is a write-only data structure used to create sealed Slices. The zero builder is valid, but a nil builder is not valid and will trigger panics if any method is invoked.

func NewBuilder

func NewBuilder[T any](length, capacity int) *Builder[T]

func (*Builder[T]) Append

func (b *Builder[T]) Append(elem ...T) *Builder[T]

func (*Builder[T]) AppendSeq

func (b *Builder[T]) AppendSeq(seq iter.Seq[T]) *Builder[T]

AppendSeq appends all values from an iter.Seq and returns the builder.

If you know the number of elements in the seq it is usually worthwhile calling Grow before calling AppendSeq2, or ensure that the builder is created with enough initial capacity when calling NewBuilder.

func (*Builder[T]) AppendSeq2

func (b *Builder[T]) AppendSeq2(seq iter.Seq2[int, T]) *Builder[T]

AppendSeq2 appends all values from an iter.Seq2 and returns the builder.

If you know the number of elements in the seq it is usually worthwhile calling Grow before calling AppendSeq2, or ensure that the builder is created with enough initial capacity when calling NewBuilder.

func (*Builder[T]) AppendSlice

func (b *Builder[T]) AppendSlice(other Slice[T]) *Builder[T]

func (*Builder[T]) Cap

func (b *Builder[T]) Cap() int

func (*Builder[T]) Grow

func (b *Builder[T]) Grow(n int) *Builder[T]

Grow ensures there is underlying capacity for appending another n elements without allocations.

func (*Builder[T]) Len

func (b *Builder[T]) Len() int

func (*Builder[T]) Reverse

func (b *Builder[T]) Reverse() *Builder[T]

func (*Builder[T]) Seal

func (b *Builder[T]) Seal() Slice[T]

Seal clears the Builder and returns a Slice with the elements.

All internal state is cleared after calling Seal. The Builder can be reused, but should generally not be.

func (*Builder[T]) Sort

func (b *Builder[T]) Sort(cmp func(a, b T) int) *Builder[T]

type Map

type Map[K comparable, V any] struct {
	// contains filtered or unexported fields
}

func (*Map[K, V]) All

func (m *Map[K, V]) All(yield func(K, V) bool)

func (*Map[K, V]) Contains

func (m *Map[K, V]) Contains(k K) bool

func (*Map[K, V]) Empty

func (m *Map[K, V]) Empty() bool

func (*Map[K, V]) Get

func (m *Map[K, V]) Get(k K) (V, bool)

func (*Map[K, V]) GetOr

func (m *Map[K, V]) GetOr(k K, def V) V

func (*Map[K, V]) Len

func (m *Map[K, V]) Len() int

type Mapper

type Mapper[K comparable, V any] struct {
	// contains filtered or unexported fields
}

func (*Mapper[K, V]) Collect

func (m *Mapper[K, V]) Collect(seq iter.Seq2[K, V]) *Mapper[K, V]

func (*Mapper[K, V]) Copy

func (m *Mapper[K, V]) Copy(src map[K]V) *Mapper[K, V]

func (*Mapper[K, V]) CopyMap

func (m *Mapper[K, V]) CopyMap(other Map[K, V]) *Mapper[K, V]

func (*Mapper[K, V]) Len

func (m *Mapper[K, V]) Len() int

func (*Mapper[K, V]) Put

func (m *Mapper[K, V]) Put(k K, v V) *Mapper[K, V]

func (*Mapper[K, V]) Seal

func (m *Mapper[K, V]) Seal() Map[K, V]

type Slice

type Slice[T any] struct {
	// contains filtered or unexported fields
}

Slice is an immutable representation of a standard Go slice. Use Builder.Seal to create a new slice. The zero-value of Slice is a valid representation of an empty slice.

func (Slice[T]) All

func (s Slice[T]) All(yield func(int, T) bool)

All iterates over all elements in s. Return false to stop early.

func (Slice[T]) Empty

func (s Slice[T]) Empty() bool

func (Slice[T]) First

func (s Slice[T]) First() (T, bool)

func (Slice[T]) Get

func (s Slice[T]) Get(i int) T

Get return the ith element of s. Exactly like normal raw slice access this method will panic if i is out of bounds.

func (Slice[T]) Last

func (s Slice[T]) Last() (T, bool)

func (Slice[T]) Len

func (s Slice[T]) Len() int

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL