🤗

Underlying Typeを使ってGoのスライス操作をちょっとだけ便利にする

2023/09/18に公開

go1.18でGenericsが導入されてから、samber/loがスライスの操作に便利でよく使っています。

https://github.com/samber/lo

しかし、不満もあります 😢
以下のようにスライスの型にメソッド定義していたとしましょう。

type UserName string

type User struct {
	Name UserName
}
type Users []*User

func (us Users) JoinNames(sep string) string {
	return strings.Join(us.Names(), sep)
}

func (us Users) Names() []string {
	return lo.Map(us, func(item *User, _ int) string {
		return string(item.Name)
	})
}

lo.Filterで対象のユーザーを絞り込んでから、
Usersに定義したメソッドを使って名前を表示してみます。

func main() {
	users := Users{{Name: "Tom"}, {Name: "Bob"}, {Name: "Alice"}, {Name: "Bem"}}
	fmt.Println(lo.Filter(users, func(user *User, _ int) bool {
		return strings.HasSuffix(string(user.Name), "m")
	}).JoinNames(", "))
}

よーし、めっちゃcoolに書けたやん?と思いきや
Nooooooo 😱😱😱

lo.Filter(users, func(user *User, _ int) bool {…}).JoinNames undefined (type []*User has no field or method JoinNames)

そうです。
lo.Filterの戻り値は[]*UserになるのでUsersにキャストしてあげなければUsersに定義したメソッドは使えません。

func main() {
	users := Users{{Name: "Tom"}, {Name: "Bob"}, {Name: "Alice"}, {Name: "Bem"}}
	fmt.Println(Users(lo.Filter(users, func(user *User, _ int) bool {
		return strings.HasSuffix(string(user.Name), "m")
	})).JoinNames(", "))
	// "Tom, Bem"
}

本題です

Underlying Typeを使って戻り値でUsersを受け取れるFilterを作ってみましょう。
これでキャストが不要になるはずです。

func Filter[T any, L ~[]T](list L, fn func(item T) bool) L {
	result := make(L, 0, len(list))
	for i := range list {
		if fn(list[i]) {
			result = append(result, list[i])
		}
	}
	return result
}
func main() {
	users := Users{{Name: "Tom"}, {Name: "Bob"}, {Name: "Alice"}, {Name: "Bem"}}

	fmt.Println(Filter(users, func(user *User) bool {
		return strings.HasSuffix(string(user.Name), "m")
	}).JoinNames(", "))
	// "Tom, Bem"
}

無事にキャストなしでUsersのメソッドを使えるようになりました 🤗🤗🤗

ちょっと応用してstringをUnderlying Typeに持っていれば受けられる
strings.Join相当の関数を作ってみます。

func JoinString[S ~string, L ~[]S](list L, sep string) string {
	sb := new(strings.Builder)
	for i := range list {
		if i > 0 {
			sb.WriteString(sep)
		}
		sb.WriteString(string(list[i]))
	}
	return sb.String()
}
type UserName string
type UserNames []UserName
type UserNames2 []string

func main() {
	fmt.Println(JoinString([]string{"Tom", "Bob", "Alice"}, " | "))
	// "Tom | Bob | Alice"
	fmt.Println(JoinString([]UserName{"Tom", "Bob", "Alice"}, " | "))
	// "Tom | Bob | Alice"
	fmt.Println(JoinString(UserNames{"Tom", "Bob", "Alice"}, " | "))
	// "Tom | Bob | Alice"
	fmt.Println(JoinString(UserNames2{"Tom", "Bob", "Alice"}, " | "))
	// "Tom | Bob | Alice"
}

めっちゃ便利🤩🤩🤩

動かせるコードはplaygroundに置いておきました。
https://go.dev/play/p/qPLJC-2SH8s

Discussion