Skip to content

Commit

Permalink
Option: Support omitempty
Browse files Browse the repository at this point in the history
This requires changing the underlying type, which *may* be a breaking change.

AFAICT there isn't any way to do this with the struct based type, but can
be done with a slice type (or a map, but maps are heavier weight, so chose slice).

When the slice is empty (len == 0) it is considered absent, when it has an item
it is considered present.

This also allows for creating a value via:

`Option[T]{value}`

Any values appended to the slice after the first are ignored.
  • Loading branch information
freeformz committed Dec 18, 2024
1 parent 8e2839f commit 3e4bfee
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 95 deletions.
4 changes: 2 additions & 2 deletions fold_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ func ExampleFold_either() {
}

func ExampleFold_option() {
option1 := Option[int]{isPresent: true, value: 42}
option2 := Option[int]{isPresent: false}
option1 := Option[int]{42}
option2 := Option[int]{}

successFunc := func(val int) string {
return fmt.Sprintf("Success with value %d", val)
Expand Down
123 changes: 55 additions & 68 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,13 @@ var optionNoSuchElement = fmt.Errorf("no such element")
// Some builds an Option when value is present.
// Play: https://go.dev/play/p/iqz2n9n0tDM
func Some[T any](value T) Option[T] {
return Option[T]{
isPresent: true,
value: value,
}
return Option[T]{value}
}

// None builds an Option when value is absent.
// Play: https://go.dev/play/p/yYQPsYCSYlD
func None[T any]() Option[T] {
return Option[T]{
isPresent: false,
}
return make(Option[T], 0)
}

// TupleToOption builds a Some Option when second argument is true, or None.
Expand Down Expand Up @@ -63,149 +58,144 @@ func PointerToOption[T any](value *T) Option[T] {

// Option is a container for an optional value of type T. If value exists, Option is
// of type Some. If the value is absent, Option is of type None.
type Option[T any] struct {
isPresent bool
value T
}
type Option[T any] []T

// IsPresent returns false when value is absent.
// Play: https://go.dev/play/p/nDqIaiihyCA
func (o Option[T]) IsPresent() bool {
return o.isPresent
return len(o) > 0
}

// IsAbsent returns false when value is present.
// Play: https://go.dev/play/p/23e2zqyVOQm
func (o Option[T]) IsAbsent() bool {
return !o.isPresent
return len(o) == 0
}

// Size returns 1 when value is present or 0 instead.
// Play: https://go.dev/play/p/7ixCNG1E9l7
func (o Option[T]) Size() int {
if o.isPresent {
return 1
}

return 0
return len(o)
}

// Get returns value and presence.
// Play: https://go.dev/play/p/0-JBa1usZRT
func (o Option[T]) Get() (T, bool) {
if !o.isPresent {
if len(o) == 0 {
return empty[T](), false
}

return o.value, true
return o[0], true
}

// MustGet returns value if present or panics instead.
// Play: https://go.dev/play/p/RVBckjdi5WR
func (o Option[T]) MustGet() T {
if !o.isPresent {
if len(o) == 0 {
panic(optionNoSuchElement)
}

return o.value
return o[0]
}

// OrElse returns value if present or default value.
// Play: https://go.dev/play/p/TrGByFWCzXS
func (o Option[T]) OrElse(fallback T) T {
if !o.isPresent {
if len(o) == 0 {
return fallback
}

return o.value
return o[0]
}

// OrEmpty returns value if present or empty value.
// Play: https://go.dev/play/p/SpSUJcE-tQm
func (o Option[T]) OrEmpty() T {
return o.value
if len(o) == 0 {
return empty[T]()
}
return o[0]
}

// ForEach executes the given side-effecting function of value is present.
func (o Option[T]) ForEach(onValue func(value T)) {
if o.isPresent {
onValue(o.value)
if len(o) > 0 {
onValue(o[0])
}
}

// Match executes the first function if value is present and second function if absent.
// It returns a new Option.
// Play: https://go.dev/play/p/1V6st3LDJsM
func (o Option[T]) Match(onValue func(value T) (T, bool), onNone func() (T, bool)) Option[T] {
if o.isPresent {
return TupleToOption(onValue(o.value))
if len(o) == 0 {
return TupleToOption(onNone())
}
return TupleToOption(onNone())
return TupleToOption(onValue(o[0]))
}

// Map executes the mapper function if value is present or returns None if absent.
// Play: https://go.dev/play/p/mvfP3pcP_eJ
func (o Option[T]) Map(mapper func(value T) (T, bool)) Option[T] {
if o.isPresent {
return TupleToOption(mapper(o.value))
if len(o) == 0 {
return None[T]()
}

return None[T]()
return TupleToOption(mapper(o[0]))
}

// MapNone executes the mapper function if value is absent or returns Option.
// Play: https://go.dev/play/p/_KaHWZ6Q17b
func (o Option[T]) MapNone(mapper func() (T, bool)) Option[T] {
if o.isPresent {
return Some(o.value)
if len(o) == 0 {
return TupleToOption(mapper())
}

return TupleToOption(mapper())
return Some(o[0])
}

// FlatMap executes the mapper function if value is present or returns None if absent.
// Play: https://go.dev/play/p/OXO-zJx6n5r
func (o Option[T]) FlatMap(mapper func(value T) Option[T]) Option[T] {
if o.isPresent {
return mapper(o.value)
if len(o) == 0 {
return None[T]()
}

return None[T]()
return mapper(o[0])
}

// ToPointer returns value if present or a nil pointer.
// Play: https://go.dev/play/p/N43w92SM-Bs
func (o Option[T]) ToPointer() *T {
if !o.isPresent {
if len(o) == 0 {
return nil
}

return &o.value
return &(o[0])
}

// MarshalJSON encodes Option into json.
func (o Option[T]) MarshalJSON() ([]byte, error) {
if o.isPresent {
return json.Marshal(o.value)
if len(o) == 0 {
// if anybody find a way to support `omitempty` param, please contribute!
return json.Marshal(nil)
}

// if anybody find a way to support `omitempty` param, please contribute!
return json.Marshal(nil)
return json.Marshal(o[0])
}

// UnmarshalJSON decodes Option from json.
func (o *Option[T]) UnmarshalJSON(b []byte) error {
if bytes.Equal(b, []byte("null")) {
o.isPresent = false
*o = make(Option[T], 0)
return nil
}

err := json.Unmarshal(b, &o.value)
var v T
err := json.Unmarshal(b, &v)
if err != nil {
return err
}

o.isPresent = true
*o = Option[T]{v}
return nil
}

Expand All @@ -221,14 +211,14 @@ func (o *Option[T]) UnmarshalText(data []byte) error {

// MarshalBinary is the interface implemented by an object that can marshal itself into a binary form.
func (o Option[T]) MarshalBinary() ([]byte, error) {
if !o.isPresent {
if len(o) == 0 {
return []byte{0}, nil
}

var buf bytes.Buffer

enc := gob.NewEncoder(&buf)
if err := enc.Encode(o.value); err != nil {
if err := enc.Encode(o[0]); err != nil {
return []byte{}, err
}

Expand All @@ -242,19 +232,19 @@ func (o *Option[T]) UnmarshalBinary(data []byte) error {
}

if data[0] == 0 {
o.isPresent = false
o.value = empty[T]()
*o = make(Option[T], 0)
return nil
}

buf := bytes.NewBuffer(data[1:])
dec := gob.NewDecoder(buf)
err := dec.Decode(&o.value)
var v T
err := dec.Decode(&v)
if err != nil {
return err
}

o.isPresent = true
*o = Option[T]{v}
return nil
}

Expand All @@ -271,8 +261,7 @@ func (o *Option[T]) GobDecode(data []byte) error {
// Scan implements the SQL sql.Scanner interface.
func (o *Option[T]) Scan(src any) error {
if src == nil {
o.isPresent = false
o.value = empty[T]()
*o = make(Option[T], 0)
return nil
}

Expand All @@ -284,15 +273,13 @@ func (o *Option[T]) Scan(src any) error {
return fmt.Errorf("failed to scan: %w", err)
}

o.isPresent = true
o.value = t
*o = Option[T]{t}
return nil
}

if av, err := driver.DefaultParameterConverter.ConvertValue(src); err == nil {
if v, ok := av.(T); ok {
o.isPresent = true
o.value = v
*o = Option[T]{v}
return nil
}
}
Expand All @@ -302,18 +289,18 @@ func (o *Option[T]) Scan(src any) error {

// Value implements the driver Valuer interface.
func (o Option[T]) Value() (driver.Value, error) {
if !o.isPresent {
if len(o) == 0 {
return nil, nil
}

return driver.DefaultParameterConverter.ConvertValue(o.value)
return driver.DefaultParameterConverter.ConvertValue(o[0])
}

// leftValue returns an error if the Option is None, otherwise nil
//
//nolint:unused
func (o Option[T]) leftValue() error {
if !o.isPresent {
if len(o) == 0 {
return optionNoSuchElement
}
return nil
Expand All @@ -323,16 +310,16 @@ func (o Option[T]) leftValue() error {
//
//nolint:unused
func (o Option[T]) rightValue() T {
if !o.isPresent {
if len(o) == 0 {
var zero T
return zero
}
return o.value
return o[0]
}

// hasLeftValue returns true if the Option represents a None state
//
//nolint:unused
func (o Option[T]) hasLeftValue() bool {
return !o.isPresent
return len(o) == 0
}
3 changes: 1 addition & 2 deletions option_go122.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ func (o *Option[T]) scanConvertValue(src any) error {
// Value type than the one we expect here.
var st sql.Null[T]
if err := st.Scan(src); err == nil {
o.isPresent = true
o.value = st.V
*o = append(*o, st.V)
return nil
}
return fmt.Errorf("failed to scan Option[T]")
Expand Down
Loading

0 comments on commit 3e4bfee

Please sign in to comment.