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 2 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
123 changes: 68 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,19 @@

![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 is a set of packages that use generics to create useful boolean tests of truthiness and also contain some nice helper functions

## Examples

```
// truthy.Value returns whether a comparable value
// is equal to the zero value for its type.
//
// truthy.ValueAny 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

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

truthy.ValueSlice([]byte(``)) // false
truthy.ValueSlice([]byte(` `)) // true

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

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


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

p = new(int)
// truthy does not check value underlying pointer!
truthy.Value(p) // true
## Package Examples

### `truthy/condition`
```go
// Ever wish Go had ? : ternary operators?
// Now it has a ternary function.
x := truthy.Cond(truthy.Value(""), 1, 10) // x == 10
x := condition.Evaluate(is.Truthy(""), 1, 10) // x == 10

// truthy.Cond cannot lazily evaluate its arguments,
// condition.Evaluate cannot lazily evaluate its arguments,
// but you can use a closure to fake it.
s := truthy.Cond(truthy.ValueSlice([]string{""}),
s := condition.Evaluate(is.TruthySlice([]string{""}),
func() string {
// do some calculation
return "foo"
Expand All @@ -59,32 +24,80 @@ s := truthy.Cond(truthy.ValueSlice([]string{""}),
return "bar"
})()
// s == "foo"
```


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

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

## FAQs
### `truthy/is`
```go
// is.Truthy returns whether a comparable value
// is equal to the zero value for its type.
//
// is.TruthyAny 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.

is.Truthy(0) // false
is.Truthy(1) // true

### Oh god
is.Truthy("") // false
is.Truthy(" ") // true

This is the correct reaction.
is.TruthySlice([]byte(``)) // false
is.TruthySlice([]byte(` `)) // true

### Isn't this just using reflection? Does it even really require generics?
is.TruthySlice([]int{}) // false
is.TruthySlice([]int{1, 2, 3}) // 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.
var err error
is.Truthy(err) // false
is.Truthy(errors.New("hi")) // true
if is.Truthy(err) {
panic(err)
}

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.

### 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.
var p *int
is.Truthy(p) // false

p = new(int)
// Truthy does not check the underlying truthy value for a pointer!
is.Truthy(p) // true
// for that, use TruthyPointer,
is.TruthyPointer(p) // false
// but beware of double pointers!
is.TruthyPointer(&p) // true
```

### `truthy/pointers`
```go
// Start with a null pointer
var strptr *string

// pointers.Deref safely dereferences a nil pointer into its empty value
fmt.Println(pointers.Deref(strptr) == "") // prints true

// pointers.Coalesce lets us specify a default value for a nil pointer
fmt.Println(pointers.Coalesce(strptr, "hello, world")) // prints "hello, world"

// 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!

// pointers.First returns the first pointer that isn't nil.
strptr = pointers.First(strptr, newptr) // returns newptr
```
28 changes: 0 additions & 28 deletions coalesce_example_test.go

This file was deleted.

10 changes: 0 additions & 10 deletions cond.go

This file was deleted.

10 changes: 10 additions & 0 deletions condition/condition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package condition

// Evaluate returns ifVal if cond is true,
// otherwise it returns elseVal.
func Evaluate[T any](cond bool, ifVal, elseVal T) T {
if cond {
return ifVal
}
return elseVal
}
11 changes: 6 additions & 5 deletions cond_example_test.go → condition/condition_example_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package truthy_test
package condition_test

import (
"fmt"

"github.com/carlmjohnson/truthy"
"github.com/carlmjohnson/truthy/condition"
"github.com/carlmjohnson/truthy/is"
)

func ExampleCond_lazy() {
func ExampleEvaluate_lazy() {
i := 1
// Cond cannot lazily evaluate its arguments,
// but you can use a closure to fake it.
s := truthy.Cond(
truthy.Value(i),
s := condition.Evaluate(
is.Truthy(i),
func() string {
// do some calculation
return "true"
Expand Down
37 changes: 0 additions & 37 deletions default.go

This file was deleted.

39 changes: 39 additions & 0 deletions defaults/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package defaults

import "github.com/carlmjohnson/truthy/is"

// GetFirst returns the first value in vs which is non-zero.
func GetFirst[T comparable](vs ...T) (t T) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go 1.22 will have cmp.Or (which I added 😊) that does this same thing, so it's kind of redundant to have it here. OTOH, FirstAny is still useful.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... could it just be removed once it's officially live in Go? that's pretty sweet though

for _, v := range vs {
if is.Truthy(v) {
return v
}
}
return
}

// GetFirstAny returns the first value in vs which is truthy.
func GetFirstAny[T any](vs ...T) (t T) {
for _, v := range vs {
if is.TruthyAny(v) {
return v
}
}
return
}

// SetFirst sets p to the first non-zero value in defaults
// if it is not already non-zero.
func SetFirst[T comparable](p *T, defaults ...T) {
if !is.TruthyPointer(p) {
*p = GetFirst(defaults...)
}
}

// SetFirstAny sets p to the first truthy value in defaults
// if it is not already truthy.
func SetFirstAny[T any](p *T, defaults ...T) {
if !is.TruthyAny(*p) {
*p = GetFirstAny(defaults...)
}
}
16 changes: 8 additions & 8 deletions default_example_test.go → defaults/defaults_example_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package truthy_test
package defaults_test

import (
"fmt"
"time"

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

type MyStruct struct {
Expand All @@ -15,9 +15,9 @@ type MyStruct struct {

func NewMyStruct(port int, host string, timeout time.Duration) *MyStruct {
s := MyStruct{port, host, timeout}
truthy.SetDefault(&s.Port, 80)
truthy.SetDefault(&s.Host, "localhost")
truthy.SetDefault(&s.Timeout, 10*time.Second)
defaults.SetFirst(&s.Port, 80)
defaults.SetFirst(&s.Host, "localhost")
defaults.SetFirst(&s.Timeout, 10*time.Second)
return &s
}

Expand All @@ -31,9 +31,9 @@ func ExampleSetDefault() {

func MakeMyStruct(port int, host string, timeout time.Duration) *MyStruct {
return &MyStruct{
Port: truthy.First(port, 80),
Host: truthy.First(host, "localhost"),
Timeout: truthy.First(timeout, 10*time.Second),
Port: defaults.GetFirst(port, 80),
Host: defaults.GetFirst(host, "localhost"),
Timeout: defaults.GetFirst(timeout, 10*time.Second),
}
}

Expand Down
Loading