Skip to content

Commit

Permalink
value: add a Maybe[T] type
Browse files Browse the repository at this point in the history
A Maybe[T] is a container for an optional single value of arbirary type, which
may be either present or absent.
  • Loading branch information
creachadair committed Sep 3, 2024
1 parent 4894a3c commit 3b59cc3
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 0 deletions.
33 changes: 33 additions & 0 deletions value/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package value_test

import (
"fmt"

"github.com/creachadair/mds/value"
)

var randomValues = []int{1, 6, 16, 19, 4}

func ExampleMaybe() {
even := make([]value.Maybe[int], 5)
for i, r := range randomValues {
if r%2 == 0 {
even[i] = value.Just(r)
}
}

var count int
for _, v := range even {
if v.Present() {
count++
}
}

fmt.Println("input:", randomValues)
fmt.Println("result:", even)
fmt.Println("count:", count)
// Output:
// input: [1 6 16 19 4]
// result: [Absent[int] 6 16 Absent[int] 4]
// count: 3
}
81 changes: 81 additions & 0 deletions value/maybe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package value

import "fmt"

// Maybe is a container that can hold a value of type T.
// Just(v) returns a Maybe holding the value v.
// Absent() returns a Maybe that holds no value.
// A zero Maybe is ready for use and is equivalent to Absent().
//
// It is safe to copy and assign a Maybe value, but note that if a value is
// present, only a shallow copy of the underlying value is made. Maybe values
// are comparable if and only if T is comparable.
type Maybe[T any] struct {
value T
present bool
}

// Just returns a Maybe holding the value v.
func Just[T any](v T) Maybe[T] { return Maybe[T]{value: v, present: true} }

// Absent returns a Maybe holding no value.
// A zero Maybe is equivalent to Absent().
func Absent[T any]() Maybe[T] { return Maybe[T]{} }

// Present reports whether m holds a value.
func (m Maybe[T]) Present() bool { return m.present }

// GetOK reports whether m holds a value, and if so returns that value.
// If m is empty, GetOK returns the zero of T.
func (m Maybe[T]) GetOK() (T, bool) { return m.value, m.present }

// Get returns value held in m, if present; otherwise it returns the zero of T.
func (m Maybe[T]) Get() T { return m.value }

// Or returns m if m holds a value; otherwise it returns Just(o).
func (m Maybe[T]) Or(o T) Maybe[T] {
if m.present {
return m
}
return Just(o)
}

// String returns the string representation of m. If m holds a value v, the
// string representation of m is that of v.
func (m Maybe[T]) String() string {
if m.present {
return fmt.Sprint(m.value)
}
return fmt.Sprintf("Absent[%T]", m.value)
}

// Check returns Just(v) if err == nil; otherwise it returns Absent().
func Check[T any](v T, err error) Maybe[T] {
if err == nil {
return Just(v)
}
return Maybe[T]{}
}

// MapMaybe returns a function from Maybe[T] to Maybe[U].
// If the argument is present and has value v, the result is present and has
// value f(v). Otherwise, the result is absent and f is not called.
func MapMaybe[T, U any](f func(T) U) func(Maybe[T]) Maybe[U] {
return func(a Maybe[T]) Maybe[U] {
if v, ok := a.GetOK(); ok {
return Just(f(v))
}
return Absent[U]()
}
}

// First returns the element of vs that holds a value, if any exists; otherwise
// it returns Absent().
func First[T any](vs ...Maybe[T]) Maybe[T] {
for _, v := range vs {
if v.Present() {
return v
}
}
return Maybe[T]{}
}
125 changes: 125 additions & 0 deletions value/maybe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package value_test

import (
"strconv"
"testing"

"github.com/creachadair/mds/value"
)

func TestMaybe(t *testing.T) {
t.Run("Zero", func(t *testing.T) {
var v value.Maybe[int]
if v.Present() {
t.Error("Zero maybe should not be present")
}
if got := v.Get(); got != 0 {
t.Errorf("Get: got %d, want 0", got)
}
})

t.Run("Present", func(t *testing.T) {
v := value.Just("apple")
if got, ok := v.GetOK(); !ok || got != "apple" {
t.Errorf("GetOK: got %q, %v; want apple, true", got, ok)
}
if got := v.Get(); got != "apple" {
t.Errorf("Get: got %q, want apple", got)
}
if !v.Present() {
t.Error("Value should be present")
}
})

t.Run("Or", func(t *testing.T) {
v := value.Just("pear")
absent := value.Absent[string]()
tests := []struct {
lhs value.Maybe[string]
rhs, want string
}{
{absent, "", ""},
{v, "", "pear"},
{absent, "plum", "plum"},
{v, "plum", "pear"},
}
for _, tc := range tests {
if got := tc.lhs.Or(tc.rhs); got != value.Just(tc.want) {
t.Errorf("%v.Or(%v): got %v, want %v", tc.lhs, tc.rhs, got, tc.want)
}
}
})

t.Run("String", func(t *testing.T) {
v := value.Just("pear")
if got := v.String(); got != "pear" {
t.Errorf("String: got %q, want pear", got)
}

var w value.Maybe[string]
if got, want := w.String(), "Absent[string]"; got != want {
t.Errorf("String: got %q, want %q", got, want)
}
})
}

func TestCheck(t *testing.T) {
t.Run("OK", func(t *testing.T) {
got := value.Check(strconv.Atoi("1"))
if want := value.Just(1); got != want {
t.Errorf("Check(1): got %v, want %v", got, want)
}
})
t.Run("Error", func(t *testing.T) {
got := value.Check(strconv.Atoi("bogus"))
if got.Present() {
t.Errorf("Check(bogus): got %v, want absent", got)
}
})
}

func TestMapMaybe(t *testing.T) {
length := value.MapMaybe(func(s string) int {
return len(s)
})

tests := []struct {
input value.Maybe[string]
want value.Maybe[int]
}{
{value.Just(""), value.Just(0)},
{value.Just("plum"), value.Just(4)},
{value.Absent[string](), value.Absent[int]()},
}
for _, tc := range tests {
if got := length(tc.input); got != tc.want {
t.Errorf("Length %q: got %v, want %v", tc.input, got, tc.want)
}
}
}

func TestFirst(t *testing.T) {
type tv = value.Maybe[int]
var absent tv
v1 := value.Just(1)
v2 := value.Just(2)
tests := []struct {
input []tv
want tv
}{
{nil, absent},
{[]tv{}, absent},
{[]tv{absent}, absent},
{[]tv{absent, absent, absent}, absent},

{[]tv{v1}, v1},
{[]tv{absent, v1}, v1},
{[]tv{absent, v1, v2}, v1},
{[]tv{absent, absent, v1, absent, v2}, v1},
}
for _, tc := range tests {
if got := value.First(tc.input...); got != tc.want {
t.Errorf("First %+v: got %v, want %v", tc.input, got, tc.want)
}
}
}

0 comments on commit 3b59cc3

Please sign in to comment.