nil かもしれないメンバのある構造体の整列


これは何?

という記事を見て、自分ならどう書くかなと思って書いたもの。
generics の練習を兼ねて

まずはソース

go1.18
package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"sort"

	"golang.org/x/exp/constraints"
)

type Sample struct {
	Name        *string
	Description *string
	Note        *string
}

func ToPtr[T any](x T) *T {
	return &x
}

func NewSample(name, desc, note *string) *Sample {
	return &Sample{
		Name:        name,
		Description: desc,
		Note:        note,
	}
}

func comparePtr[T constraints.Ordered](x, y *T) int {
	if x == y {
		return 0
	}
	if x == nil {
		return -1
	}
	if y == nil {
		return 1
	}
	if *x == *y {
		return 0
	}
	if *x < *y {
		return -1
	}
	return 1
}

func compareSampleMemberStrPtr(sx, sy *Sample, proc func(*Sample) *string) int {
	return comparePtr(proc(sx), proc(sy))
}

func SampleCompare(a, b *Sample) int {
	if c := compareSampleMemberStrPtr(a, b, func(s *Sample) *string { return s.Name }); c != 0 {
		return c
	}
	if c := compareSampleMemberStrPtr(a, b, func(s *Sample) *string { return s.Description }); c != 0 {
		return c
	}
	return compareSampleMemberStrPtr(a, b, func(s *Sample) *string { return s.Note })
}

func show(title string, samples []*Sample) {
	fmt.Println(title)
	for _, v := range samples {
		jsonBytes, err := json.Marshal(v)
		if err != nil {
			panic(err)
		}
		fmt.Println("   " + string(jsonBytes))
	}
}

func shuffled(i int) []int {
	s := make([]int, i)
	for ix := 0; ix < i; ix++ {
		s[ix] = ix
	}
	rand.Shuffle(i, func(x, y int) {
		s[x], s[y] = s[y], s[x]
	})
	return s
}

func main() {
	samples := []*Sample{}
	for _, k := range shuffled(27) {
		name := []*string{ToPtr("名前01"), ToPtr("名前02"), nil}[k%3]
		desc := []*string{ToPtr("説明01"), ToPtr("説明02"), nil}[(k/3)%3]
		note := []*string{ToPtr("メモ01"), ToPtr("メモ02"), nil}[(k/9)%3]
		samples = append(samples, NewSample(name, desc, note))
	}
	show("before sorting", samples)
	sort.Slice(samples, func(i, j int) bool {
		return SampleCompare(samples[i], samples[j]) < 0
	})
	show("after sorting", samples)
}

ちょっと説明

ToPtr[T any]

何かを何かへのポインタに変換する。
変換元へのポインタではなく、変換元のコピーへのポインタになるので要注意だけど、便利。

そもそも go で &("hoge") って書かせてくれれば要らなくなる関数だけど、いまは書けないので。

comparePtr[T constraints.Ordered]

nil は非 nil よりも小さい、という論理で比較する。
両方とも非 nil なら、ポインタのサス先の値の大小関係を使う。
比較結果は -1, 0, 1 で表現する。

compareSampleMemberStrPtr

Sample のメンバ(じゃなくてもいいけど)である文字列へのポインタを比較する。
メンバを参照するための関数を受けるのは

go1.18
comparePtr(a.Name, b.Note)

みたいなミスを避けるため。
C++ なら「メンバへのポインタ型」の値を使うんだけどそういうの無いからね。

SampleCompare

主要部はこれ。
メンバを一個比較して、等しかったら次のメンバを比較、また等しかったら次のメンバを比較。
という流れ。

今回はループにしなかったけど、ループにするのも悪くない。

書いてみて思ったこと

やっぱりなんか go で書くと長くなるよなと思う。

go1.18
	if *x == *y {
		return 0
	}
	if *x < *y {
		return -1
	}
	return 1

は、C/C++ だと

C / C++
return (*y<*x) - (*x<*y);

だよなぁとか思ったり。