├── go.mod ├── go.sum ├── ternary_test.go ├── result_test.go ├── result.go ├── ternary.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Southclaws/result 2 | 3 | go 1.18 4 | 5 | require github.com/stretchr/testify v1.7.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 7 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /ternary_test.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTernary(t *testing.T) { 10 | r1 := Ternary(true, 69, 420) 11 | assert.Equal(t, 69, r1) 12 | 13 | r2 := Ternary(false, 69, 420) 14 | assert.Equal(t, 420, r2) 15 | } 16 | 17 | func TestTernaryFn(t *testing.T) { 18 | r1 := TernaryFn(true, 19 | func() int { return 69 }, 20 | func() int { return 420 }, 21 | ) 22 | assert.Equal(t, 69, r1) 23 | 24 | r2 := TernaryFn(false, 25 | func() int { return 69 }, 26 | func() int { return 420 }, 27 | ) 28 | assert.Equal(t, 420, r2) 29 | } 30 | 31 | func TestTernaryResult(t *testing.T) { 32 | r1 := TernaryResult(true, 33 | Wrap(Fails()), 34 | Wrap(Succeeds()), 35 | ) 36 | assert.False(t, r1.Valid()) 37 | assert.Error(t, r1.Error()) 38 | assert.Nil(t, r1.Value()) 39 | assert.Equal(t, ErrFailed, r1.Error()) 40 | 41 | r2 := TernaryResult(false, 42 | Wrap(Fails()), 43 | Wrap(Succeeds()), 44 | ) 45 | assert.True(t, r2.Valid()) 46 | assert.NoError(t, r2.Error()) 47 | assert.NotNil(t, r2.Value()) 48 | assert.Equal(t, &Fixture, r2.Value()) 49 | } 50 | -------------------------------------------------------------------------------- /result_test.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type Example struct { 11 | Name string 12 | } 13 | 14 | var ErrFailed = errors.New("I have failed you") 15 | 16 | var Fixture = Example{"Óðinn"} 17 | 18 | func Fails() (*Example, error) { 19 | return nil, ErrFailed 20 | } 21 | 22 | func Succeeds() (*Example, error) { 23 | return &Fixture, nil 24 | } 25 | 26 | func TestResultWrapError(t *testing.T) { 27 | r := Wrap(Fails()) 28 | 29 | assert.False(t, r.Valid()) 30 | assert.Error(t, r.Error()) 31 | assert.Nil(t, r.Value()) 32 | assert.Equal(t, ErrFailed, r.Error()) 33 | } 34 | 35 | func TestResultWrapSuccess(t *testing.T) { 36 | r := Wrap(Succeeds()) 37 | 38 | assert.True(t, r.Valid()) 39 | assert.NoError(t, r.Error()) 40 | assert.NotNil(t, r.Value()) 41 | assert.Equal(t, &Fixture, r.Value()) 42 | } 43 | 44 | func TestResultUnwrapError(t *testing.T) { 45 | r, err := Unwrap(Wrap(Fails())) 46 | 47 | assert.Error(t, err) 48 | assert.Nil(t, r) 49 | assert.Equal(t, ErrFailed, err) 50 | } 51 | 52 | func TestResultUnwrapSuccess(t *testing.T) { 53 | r, err := Unwrap(Wrap(Succeeds())) 54 | 55 | assert.NoError(t, err) 56 | assert.NotNil(t, r) 57 | assert.Equal(t, &Fixture, r) 58 | } 59 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | // Result describes a type that may either be some value or an error. 4 | type Result[T any] interface { 5 | Valid() bool 6 | Value() T 7 | Error() error 8 | } 9 | 10 | // result is a hidden type that implements the Result interface. It's basically 11 | // a discriminated union where `e != nil` is the discriminator. 12 | type result[T any] struct { 13 | v T 14 | e error 15 | } 16 | 17 | // Valid implements Result 18 | func (r result[T]) Valid() bool { return r.e == nil } 19 | 20 | // Valid implements Result 21 | func (r result[T]) Value() T { return r.v } 22 | 23 | // Valid implements Result 24 | func (r result[T]) Error() error { return r.e } 25 | 26 | // Wrap takes a typical Go function that returns (T, error) and wraps it in a 27 | // result type. This is useful for converting from the normal Go pattern of 28 | // (T, error) to a wrapped result type. It is used like so: 29 | // 30 | // r := Wrap(SomeFunction()) 31 | // if r.Valid() { 32 | // // do something with r.Value() 33 | // } 34 | // 35 | func Wrap[T any](t T, err error) Result[T] { 36 | if err != nil { 37 | return result[T]{e: err} 38 | } else { 39 | return result[T]{t, nil} 40 | } 41 | } 42 | 43 | // Unwrap takes a result type value and turns it back into the (T, error) that 44 | // is commonly used in idiomatic Go. 45 | func Unwrap[T any](r Result[T]) (T, error) { 46 | return r.Value(), r.Error() 47 | } -------------------------------------------------------------------------------- /ternary.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | // Ternary simply returns `a` or `b` based on `cond`. 4 | func Ternary[T any](cond bool, a, b T) T { 5 | if cond { 6 | return a 7 | } else { 8 | return b 9 | } 10 | } 11 | 12 | // Ternary executes either `a` or `b` based on `cond` and returns its value. 13 | func TernaryFn[T any](cond bool, a, b func() T) T { 14 | if cond { 15 | return a() 16 | } else { 17 | return b() 18 | } 19 | } 20 | 21 | // TernaryResult returns either `a` or `b` as a result type based on `cond`. 22 | // This is useful for picking function calls that return (T, error) like so: 23 | // 24 | // result := TernaryResult( 25 | // x > 1, 26 | // Wrap(A()), 27 | // Wrap(B()), 28 | // ) 29 | // 30 | // It's cleaner compared to the idiomatic mutable variable approach: 31 | // 32 | // var result T 33 | // var err error 34 | // if x > 1 { 35 | // result, err = A() 36 | // } else { 37 | // result, err = B() 38 | // } 39 | // 40 | // You can combine Wrap() and Unwrap() so you end up with normal Go error types: 41 | // 42 | // result, err := Unwrap(TernaryResult( 43 | // x > 1, 44 | // Wrap(A()), 45 | // Wrap(B()), 46 | // )) 47 | // if err != nil { 48 | // return nil, err 49 | // } 50 | // 51 | func TernaryResult[T any](cond bool, a, b Result[T]) Result[T] { 52 | if cond { 53 | return a 54 | } else { 55 | return b 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note: this was more of an experiment, for an actual real-world library that's well tested and used in production codebases, [check out opt](https://github.com/Southclaws/result).** 2 | 3 | --- 4 | 5 | # Result Type for Go 6 | 7 | A super simple generic result type for Go. 8 | 9 | ## Why? 10 | 11 | The idiomatic ternary operation in Go is to declare a zero-value mutable variable and mutate it behind an if-else branch. 12 | 13 | This is readable and simple, though it can make some code quite terse and overly explicit. 14 | 15 | Take this for example: 16 | 17 | ```go 18 | var u User 19 | var err error 20 | 21 | if someCondition { 22 | u, err = db.GetUserByID(id) 23 | } else { 24 | u, err = db.GetUserByEmail(email) 25 | } 26 | 27 | if err != nil { 28 | return nil, err 29 | } 30 | ``` 31 | 32 | It's quite explicit, compared to the equivalents in Rust and TypeScript: 33 | 34 | ```rust 35 | let u = if some_condition { 36 | db.get_user_by_id(id) 37 | } else { 38 | db.get_user_by_email(email) 39 | }; 40 | ``` 41 | 42 | ```typescript 43 | const u = await someCondition 44 | ? db.getUserByID(id) 45 | : db.getUserByEmail(email); 46 | ``` 47 | 48 | And Go also has no immutable types, which isn't a huge issue but sometimes code reads simpler if you can assume things are assigned once. 49 | 50 | So, this library was written partly as an experiment to see how ergonomic it can be in a notoriously un-ergonomic language. 51 | 52 | ## Usage 53 | 54 | ### Wrapping Errors 55 | 56 | See the package documentation for full details. 57 | 58 | Given some function: 59 | 60 | ```go 61 | func (u *Users) GetByID(id string) (*User, error) 62 | ``` 63 | 64 | ```go 65 | r := result.Wrap(u.GetByID(id)) 66 | ``` 67 | 68 | This constructs a result type from the return value of the API function, this result type contains either the value or the error as a single entity. 69 | 70 | ```go 71 | r.Valid() // if err == nil 72 | r.Value() // return the user if r.Valid() == true 73 | r.Error() // return the error if r.Valid() == false 74 | ``` 75 | 76 | Now this is fairly similar to what is idiomatic, there's an error check and a valid state. But, there are two other invalid states... 77 | 78 | One issue with Go's idiomatic approach to return values and error handling is that there's no ergonomic way to express a discriminated union via the type system. 79 | 80 | This means that, given the following function: 81 | 82 | ```go 83 | func F() (T, error) 84 | ``` 85 | 86 | There are not *two* possible return states encoded in the type system, but **four!** 87 | 88 | - T == nil, error == nil 89 | - T != nil, error == nil 90 | - T == nil, error != nil 91 | - T != nil, error != nil 92 | 93 | Of course, in practice this isn't a huge issue as most programmers can make the assumption that, given `err == nil` then `T` *must* be valid. And given `err != nil` then `T` *must* be undefined/invalid. 94 | 95 | But type systems should be there to aid us in not needing to rely on *assumptions* to write safe predictable code. Result types in strongly typed languages (often functional languages) encode the possibility of *only* two states. 96 | 97 | You're probably thinking "that's a great point, but this library doesn't solve that problem at all" and you'd be right, you can still end up with multiple invalid states with this result type but that's just because Go's generics are still very basic and cannot express a proper result type yet. But I hope it will do in the future! 98 | 99 | One great example of a discriminated union result type is in TypeScript, because you can encode literal values as types and combine that with type unions in order to create types that can *only* be one value or the other, not any other combination of states such as the 4 states in the list above. 100 | 101 | ```typescript 102 | type Result = { 103 | value: T; 104 | error: undefined; 105 | } | { 106 | value: undefined; 107 | error: E; 108 | } 109 | ``` 110 | 111 | This type makes it impossible to construct an instance where both `value` and `error` are defined. You either have one or the other. And the type system enforces that. Building code with such a concrete and fundamental concept baked into the core APIs leads to viral strictness to spread throughout the code (for better or worse.) 112 | 113 | ### Ternary Operations 114 | 115 | So, going back to the beginning of this readme, the end goal is to facilitate an ergonomic ternary switch operation purely for performing conditional assignment once. 116 | 117 | With the same example of the two example user APIs: `GetUserByID` and `GetUserByEmail` we can rewrite the initial ternary operation as: 118 | 119 | ```go 120 | r := result.TernaryResult( 121 | someCondition, 122 | result.Wrap(db.GetUserByID(id)), 123 | result.Wrap(db.GetUserByEmail(email)), 124 | ) 125 | 126 | if !r.Valid() { 127 | return nil, r.Error() 128 | } 129 | ``` 130 | 131 | In my opinion (which is a dangerous thing to say while writing Go libraries!) this is cleaner and easier to read for certain people. 132 | 133 | The downside here is that it's not as *simple* and *explicit* as the initial version, which is one of Go's benefits for onboarding newcomers to the language. Code is generally pretty easy to read because there aren't many concepts compared to other languages. 134 | 135 | It also doesn't really solve the possible states problem outlined above. There's nothing in the type system that can prevent `r.Value()` and `r.Error()` both returning `nil`. This is simply not possible (as far as I can figure out) with the current implementation of Go 1.18 generics. 136 | 137 | ## Is this idiomatic? 138 | 139 | Short answer: no. 140 | 141 | Long answer: not sure yet. The idioms and common practices with generics are still evolving. I've been using optional types a ton at work and in my own code, those are a real life saver and a welcomed alternative to ~~hacking~~ using pointers to encode optionality into data structures. 142 | 143 | I don't imagine this library wil catch on, but I do hope to see something similar used more widely in the near future. I know Go library authors are stuck with `(T, error)` for good and a combination of stubbornness, conforming to standards and keeping things simple will prevent these sorts of experimental progressive ideas. And that's probably a good thing tbh. 144 | --------------------------------------------------------------------------------