Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split truthy into sub-packages #2

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 56 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,76 @@

![Truthiness](https://user-images.githubusercontent.com/222245/136619462-f2bc5858-067f-4277-a813-b95c64b3cdac.png)

Truthy is a package which uses generics to create useful boolean tests of truthiness and helper functions.
`truthy` leverages generics to create a useful collection of boolean tests of truthiness and helper functions

## Examples
## Package Examples

```
// truthy.Value returns whether a comparable value
### `truthy/bools`
```go
// bools.Comparable returns whether a comparable value
// is equal to the zero value for its type.
//
// truthy.ValueAny returns the truthiness of any argument.
// bools.Any returns the truthiness of any argument.
// If the value's type has a Bool() bool method, the method is called and returned.
// If the type has an IsZero() bool method, the opposite value is returned.
// Slices and maps are truthy if they have a length greater than zero.
// All other types are truthy if they are not their zero value.

truthy.Value(0) // false
truthy.Value(1) // true
bools.Comparable(0) // false
bools.Comparable(1) // true

truthy.Value("") // false
truthy.Value(" ") // true
bools.Comparable("") // false
bools.Comparable(" ") // true

truthy.ValueSlice([]byte(``)) // false
truthy.ValueSlice([]byte(` `)) // true
bools.Map(map[string]string{}) // false
bools.Map(map[string]string{"truthy": "is awesome"}) // true

truthy.ValueSlice([]int{}) // false
truthy.ValueSlice([]int{1, 2, 3}) // true
bools.Slice([]int{}) // false
bools.Slice([]int{1, 2, 3}) // true

var err error
truthy.Value(err) // false
truthy.Value(errors.New("hi")) // true
if truthy.Value(err) {
bools.Comparable(err) // false
bools.Comparable(errors.New("hi")) // true
if bools.Comparable(err) {
panic(err)
}


var p *int
truthy.Value(p) // false
bools.Comparable(p) // false

p = new(int)
// truthy does not check value underlying pointer!
truthy.Value(p) // true
// Comparable does not check the underlying truthy value for a pointer!
bools.Comparable(p) // true
// for that, use Pointer,
bools.Pointer(p) // false
// but beware of double pointers!
bools.Pointer(&p) // true

// How about an equivalent of the nullish coalescing operator ??
// as seen in C#, JavaScript, PHP, etc.:
var s string
bools.GetFirst(s, "default") // "default"
s = "something"
bools.GetFirst(s, "default") // "something"
bools.GetFirst(0, 0*1, 1-1, 0x10-10) // 6

// Easily set defaults
n := getUserInput()
bools.SetFirst(&n, 42)

// Want to be able to convert from Boolean to Int?
bools.Int(true) // 1
bools.Int(10 > 100) // 0
bools.Int(bools.Comparable("")) // 0

// Ever wish Go had ? : ternary operators?
// Now it has a ternary function.
x := truthy.Cond(truthy.Value(""), 1, 10) // x == 10
x := bools.Ternary(bools.Comparable(""), 1, 10) // x == 10

// truthy.Cond cannot lazily evaluate its arguments,
// bools.Ternary cannot lazily evaluate its arguments,
// but you can use a closure to fake it.
s := truthy.Cond(truthy.ValueSlice([]string{""}),
s := bools.Ternary(bools.Slice([]string{""}),
func() string {
// do some calculation
return "foo"
Expand All @@ -59,32 +81,21 @@ s := truthy.Cond(truthy.ValueSlice([]string{""}),
return "bar"
})()
// s == "foo"


// How about an equivalent of the nullish coalescing operator ??
// as seen in C#, JavaScript, PHP, etc.:
var s string
truthy.First(s, "default") // "default"
s = "something"
truthy.First(s, "default") // "something"
truthy.First(0, 0*1, 1-1, 0x10-10) // 6

// Easily set defaults
n := getUserInput()
truthy.SetDefault(&n, 42)
```

## FAQs

### Oh god

This is the correct reaction.
### `truthy/pointers`
```go
// Start with a null pointer
var strptr *string

### Isn't this just using reflection? Does it even really require generics?
// pointers.Deref safely dereferences a nil pointer into its empty value
fmt.Println(pointers.Deref(strptr) == "") // prints true

I tried to write a non-generic version of this package first, but you can’t reflect on an interface type. When you do `reflect.Value(x)`, you lose the fact that x was, e.g. an error, because `reflect.Value()` only takes `interface{}` and the conversion loses the interface type. You’d end up saying whether the underlying concrete type was empty or not, which is typically not what you want. To work around that, you could require that everything is passed as a pointer, e.g. `reflect.Value(&err)`, but `truthy.Value(&err)` sucks as an API. If you look at how `truthy.Value()` works, it accepts a value of type `T`, and then passes `*T` to `reflect.Value()` and calls `value.Elem()` to finally get the correct reflection type. So, on a technical level, you couldn’t quite make this API work without generics, although it could be close. However, `truthy.Filter()`, `truthy.SetDefault()`, `truthy.Any()`, and `truthy.All()` could be implemented with pure reflection, although the implementation would be a lot uglier.
// pointers.Coalesce lets us specify a default value for a nil pointer
fmt.Println(pointers.Coalesce(strptr, "hello, world")) // prints "hello, world"

Then there’s `truthy.First()`. To be honest, `truthy.First()` is the only part of the package that I consider actually useful, and even that, I mostly expect it to be used for picking a string or default. Anyhow, it requires generics to avoid the cast back from interface type to the concrete type.
// We can create a pointer to a string or other primitive type with pointers.New
newptr := pointers.New("meaning of life") // makes a pointer to a string, wow!

### Should I use this package?
Probably not. It's a little bit of a joke package, but the `truthy.First()` and `truthy.SetDefault()` functionality seem useful, especially for strings. Time will tell what best practices around the use of generics in Go turn out to be.
// pointers.First returns the first pointer that isn't nil.
strptr = pointers.First(strptr, newptr) // returns newptr
56 changes: 34 additions & 22 deletions value.go → bools/bools.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package truthy
package bools

import (
"reflect"
)

// ValueAny returns the truthy value of anything.
// Comparable returns the truthy value of comparable types.
// Values are truthy if they are not equal to the zero value for the type.
// Use the Pointer method to evaluate underlying pointer values, otherwise
// this will return true if the pointer is not nil.
func Comparable[T comparable](v T) bool {
return v != *new(T)
}

// Any returns the truthy value of anything.
// If the value's type has a Bool() bool method, the method is called and returned.
// If the type has an IsZero() bool method, the opposite value is returned.
// Slices and maps are truthy if they have a length greater than zero.
Expand All @@ -13,9 +21,9 @@ import (
// Note that the usual Go type system caveats apply around a nil pointer value not being a nil interface value.
//
// In benchmark testing,
// ValueAny is approximately 25 times slower than Value,
// Any is approximately 25 times slower than Comparable,
// and 50 times slower than native Go comparisons.
func ValueAny[T any](v T) bool {
func Any[T any](v T) bool {
switch m := any(v).(type) {
case interface{ Bool() bool }:
return m.Bool()
Expand All @@ -25,29 +33,33 @@ func ValueAny[T any](v T) bool {
return reflectValue(&v)
}

func reflectValue(vp any) bool {
switch rv := reflect.ValueOf(vp).Elem(); rv.Kind() {
case reflect.Map, reflect.Slice:
return rv.Len() != 0
default:
return !rv.IsZero()
}
// Map returns true if the length of the map is greater than 0.
// Note that it does not distinguish nil maps from empty maps.
func Map[K comparable, V any, M ~map[K]V](v M) bool {
return len(v) > 0
}

// ValueSlice returns true if the length of the slice is greater than 0.
// Note that it does not distinguish nil slices from empty slices.
func ValueSlice[T any, S ~[]T](v S) bool {
return len(v) > 0
// Pointer returns the truthy value of dereferenced pointers for comparable types.
// Values are truthy if they are not equal to the zero value for the dereferenced type.
// Note that it will evaluate to true for double pointers.
func Pointer[T comparable](v *T) bool {
if v == nil {
return false
}
return Comparable(*v)
}

// ValueMap returns true if the length of the map is greater than 0.
// Note that it does not distinguish nil maps from empty maps.
func ValueMap[K comparable, V any, M ~map[K]V](v M) bool {
// Slice returns true if the length of the slice is greater than 0.
// Note that it does not distinguish nil slices from empty slices.
func Slice[T any, S ~[]T](v S) bool {
return len(v) > 0
}

// Value returns the truthy value of comparable types.
// Values are truthy if they are not equal to the zero value for the type.
func Value[T comparable](v T) bool {
return v != *new(T)
func reflectValue(vp any) bool {
switch rv := reflect.ValueOf(vp).Elem(); rv.Kind() {
case reflect.Map, reflect.Slice:
return rv.Len() != 0
default:
return !rv.IsZero()
}
}
8 changes: 4 additions & 4 deletions value_test.go → bools/bools_any_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package truthy_test
package bools_test

import (
"fmt"
"testing"
"time"

"github.com/carlmjohnson/truthy"
"github.com/carlmjohnson/truthy/bools"
)

type errT struct{}
Expand All @@ -14,13 +14,13 @@ func (*errT) Error() string { return "" }

func test[T any](t *testing.T, v T, ok bool) {
t.Run(fmt.Sprintf("%T-%v-%v", v, v, ok), func(t *testing.T) {
if got := truthy.ValueAny(v); got != ok {
if got := bools.Any(v); got != ok {
t.Fatal()
}
})
}

func TestValueAny(t *testing.T) {
func TestAny(t *testing.T) {
var err error
test(t, err, false)
err = (*errT)(nil)
Expand Down
28 changes: 14 additions & 14 deletions value_bench_test.go → bools/bools_bench_test.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
package truthy_test
package bools_test

import (
"errors"
"testing"

"github.com/carlmjohnson/truthy"
"github.com/carlmjohnson/truthy/bools"
)

func BenchmarkValueAny_error(b *testing.B) {
func BenchmarkAny_error(b *testing.B) {
fillVal := errors.New("something")
fill := false
for i := 0; i < b.N; i++ {
var value error
if fill {
value = fillVal
}
if truthy.ValueAny(value) != fill {
if bools.Any(value) != fill {
b.FailNow()
}
fill = !fill
}
}

func BenchmarkValue_error(b *testing.B) {
func BenchmarkComparable_error(b *testing.B) {
fillVal := errors.New("something")
fill := false
for i := 0; i < b.N; i++ {
var value error
if fill {
value = fillVal
}
if truthy.Value(value) != fill {
if bools.Comparable(value) != fill {
b.FailNow()
}
fill = !fill
Expand All @@ -52,30 +52,30 @@ func Benchmark_error(b *testing.B) {
}
}

func BenchmarkValueAny_string(b *testing.B) {
func BenchmarkAny_string(b *testing.B) {
fillVal := "something"
fill := false
for i := 0; i < b.N; i++ {
var value string
if fill {
value = fillVal
}
if truthy.ValueAny(value) != fill {
if bools.Any(value) != fill {
b.FailNow()
}
fill = !fill
}
}

func BenchmarkValue_string(b *testing.B) {
func BenchmarkComparable_string(b *testing.B) {
fillVal := "something"
fill := false
for i := 0; i < b.N; i++ {
var value string
if fill {
value = fillVal
}
if truthy.Value(value) != fill {
if bools.Comparable(value) != fill {
b.FailNow()
}
fill = !fill
Expand All @@ -97,30 +97,30 @@ func Benchmark_string(b *testing.B) {
}
}

func BenchmarkValueAny_int(b *testing.B) {
func BenchmarkAny_int(b *testing.B) {
fillVal := 1
fill := false
for i := 0; i < b.N; i++ {
var value int
if fill {
value = fillVal
}
if truthy.ValueAny(value) != fill {
if bools.Any(value) != fill {
b.FailNow()
}
fill = !fill
}
}

func BenchmarkValue_int(b *testing.B) {
func BenchmarkComparable_int(b *testing.B) {
fillVal := 1
fill := false
for i := 0; i < b.N; i++ {
var value int
if fill {
value = fillVal
}
if truthy.Value(value) != fill {
if bools.Comparable(value) != fill {
b.FailNow()
}
fill = !fill
Expand Down
Loading