Last active
November 28, 2023 14:15
-
-
Save choonkeat/74002057e3c74ebc3b4428b0161a80a7 to your computer and use it in GitHub Desktop.
Discriminated union for Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gosumtype | |
import ( | |
"time" | |
) | |
// To define this sum type: | |
// | |
// type User | |
// = Anonymous | |
// | Member String Time | |
// | Admin String | |
// | |
// Ideally, we just code something like this and the | |
// rest of the boiler plate can be generated | |
type User interface { | |
Switch(s UserScenarios) | |
} | |
type UserScenarios struct { | |
Anonymous func() | |
Member func(email string, since time.Time) | |
Admin func(email string) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gosumtype | |
import ( | |
"log" | |
"time" | |
) | |
// Example usage | |
func Caller() { | |
user1 := Anonymous() | |
user2 := Member("Alice", time.Now()) | |
user3 := Admin("Bob") | |
log.Println( | |
"User1:", UserString(user1), | |
"User2:", UserString(user2), | |
"User3:", UserString(user3), | |
) | |
} | |
func UserString(u User) string { | |
var result string | |
u.Switch(UserScenarios{ | |
Anonymous: func() { | |
result = "anonymous coward" | |
}, | |
Member: func(email string, since time.Time) { | |
result = email + " (member since " + since.String() + ")" | |
}, | |
Admin: func(email string) { | |
result = email + " (admin)" | |
}, | |
}) | |
return result | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gosumtype | |
import ( | |
"log" | |
"strconv" | |
"testing" | |
"time" | |
) | |
func TestUser(t *testing.T) { | |
testCases := []struct { | |
givenUser User | |
}{ | |
{ | |
givenUser: Anonymous(), | |
}, | |
{ | |
givenUser: Member("[email protected]", time.Now()), | |
}, | |
{ | |
givenUser: Admin("[email protected]"), | |
}, | |
} | |
for i, tc := range testCases { | |
t.Run(strconv.Itoa(i), func(t *testing.T) { | |
// using names are very helpful, but loses the exhaustive check at compile time | |
// since Go happily set the undefined scenarios as function zero value: nil | |
// | |
// but we can use https://golangci-lint.run/usage/linters/#exhaustruct | |
// to check at CI instead of suffering from zero value at runtime | |
tc.givenUser.Switch(UserScenarios{ | |
Anonymous: func() { | |
log.Println("i am anonymous") | |
}, | |
Member: func(email string, since time.Time) { | |
log.Println("member", email, since) | |
}, | |
Admin: func(email string) { | |
log.Println("admin", email) | |
}, | |
}) | |
}) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gosumtype | |
import "time" | |
// | |
// Boiler plate code below: | |
// | |
// Anonymous | |
type anonymous struct{} | |
func (a anonymous) Switch(s UserScenarios) { s.Anonymous() } | |
func Anonymous() User { | |
return anonymous{} | |
} | |
// Member string time.Time | |
type member struct { | |
email string | |
since time.Time | |
} | |
func (m member) Switch(s UserScenarios) { s.Member(m.email, m.since) } | |
func Member(email string, since time.Time) User { | |
return member{email, since} | |
} | |
// Admin string | |
type admin struct{ email string } | |
func (a admin) Switch(s UserScenarios) { s.Admin(a.email) } | |
func Admin(email string) User { | |
return admin{email} | |
} |
not really, but it did pose a possible approach. the downside is that it is a runtime check, not a compile-time check
diff --git a/gosumtype_test.go b/gosumtype_test.go
index ac70d67..2738c36 100644
--- a/gosumtype_test.go
+++ b/gosumtype_test.go
@@ -2,6 +2,7 @@ package gosumtype
import (
"log"
+ "reflect"
"strconv"
"testing"
"time"
@@ -59,7 +60,7 @@ func TestFunction(t *testing.T) {
func(email string) {
log.Println("admin", email)
},
- })
+ }.Exhaustive())
// using names are very helpful, but loses the exhaustive check at compile time
// since Go happily set the undefined scenarios as function zero value: nil
@@ -73,7 +74,7 @@ func TestFunction(t *testing.T) {
Admin: func(email string) {
log.Println("admin", email)
},
- })
+ }.Exhaustive())
})
}
}
@@ -82,6 +83,18 @@ func TestFunction(t *testing.T) {
// Boiler plate code below:
//
+// Runtime exhaustive check
+func (s UserScenarios) Exhaustive() UserScenarios {
+ valueOf := reflect.ValueOf(&s).Elem()
+ typeOf := valueOf.Type()
+ for i := 0; i < valueOf.NumField(); i++ {
+ if valueOf.Field(i).IsNil() {
+ panic(typeOf.Field(i).Name + " is not covered")
+ }
+ }
+ return s
+}
+
// Anonymous
type anonymous struct{}
regarding
using names are very helpful, but loses the exhaustive check at compile time
actually we have https://golangci-lint.run/usage/linters/#exhaustruct now 🌈
https://github.com/alecthomas/go-check-sumtype
but this needs a declaration
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Unsure if it meets your requirements, but I used this validator library in this example https://github.com/go-playground/validator