Skip to content

Instantly share code, notes, and snippets.

@choonkeat
Last active November 28, 2023 14:15
Show Gist options
  • Save choonkeat/74002057e3c74ebc3b4428b0161a80a7 to your computer and use it in GitHub Desktop.
Save choonkeat/74002057e3c74ebc3b4428b0161a80a7 to your computer and use it in GitHub Desktop.
Discriminated union for Go
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)
}
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
}
package gosumtype
import (
"log"
"strconv"
"testing"
"time"
)
func TestUser(t *testing.T) {
testCases := []struct {
givenUser User
}{
{
givenUser: Anonymous(),
},
{
givenUser: Member("bob@example.com", time.Now()),
},
{
givenUser: Admin("boss@example.com"),
},
}
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)
},
})
})
}
}
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}
}
@choonkeat-govtech
Copy link

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 🌈

@yongchongye
Copy link

https://github.com/alecthomas/go-check-sumtype

but this needs a declaration

@choonkeat
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment