esort

package module
v0.0.0-...-faa66d1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 29, 2023 License: Apache-2.0 Imports: 3 Imported by: 0

README

esort

Package esort provides mechanisms for sorting user-defined types according to compound criteria extensibly. It is mutually compatible with package sort from the standard library. The package name comes from the combination of the words "extensible" and "sort" and shortened as "esort."

Goals

  • Type Safe
  • Zero Allocation (in common workflows if sorting basis is known a priori)
  • Zero Reflection
  • Safe and Accurate
  • Reasonable Degree of Ergonomics

Motivation

This is an experimental library.

Stupid as it sounds: during code review out in the field, I have seen more half-baked (sort.Interface).Less implementations than I am comfortable with, particularly where compound criteria are evaluated. I wondered whether the situation could be solved through an API.

I'm not suggesting anyone should use this. There are potentially other solutions to improve ergonomics and performance. For the time being, it is a minimally-viable prototype.

Its API documentation is available at https://pkg.go.dev/github.com/matttproud/esort.

Documentation

Overview

Package esort provides mechanisms for sorting user-defined types according to compound criteria extensibly. It is mutually compatible with package sort from the standard library. The package name comes from the combination of the words "extensible" and "sort" and shortened as "esort."

Consider a case of sorting a user-defined type of a Person:

type Person struct {
	GivenName string
	ID int
}

Suppose GivenName is to be sorted in descending order and ties of the GivenName by ID in ascending order:

sorter := esort.New().
	ByString(func(p Person) string { return p.GivenName }, esort.Desc).
	ByInt(func(p Person) int { return p.ID }, esort.Asc)

The data can then be sorted:

var data []Person

sort.Slice(data, func(i, j int) bool { return sorter.Less(data[i], data[j]) })

The same mechanism can be used directly with generics:

slices.SortFunc(data, sorter.Less)

Sorting Instructions

The methods prefixed with By copy the current sorting rule set and add a new instruction to the copy. Each rule set is evaluated according to the order in which the instructions were added to the Sorter, so earlier sorting rules carry higher sorting weight than the previous.

A Sorter without rules is invalid and panics upon use.

Due to the copy on write semantics of the instructions API, rule sets can be templatized:

base := esort.New().
	ByString(func(p Person) string { return p.GivenName }, esort.Desc).

idAsc := base.ByInt(func(p Person) int { return p.ID }, esort.Asc)
idDesc := base.ByInt(func(p Person) int { return p.ID }, esort.Desc)

Efficiency

Prefer using the simple, scalar By methods for comparing individual fields versus encoding their logic in the Sorter.ByFunc method. These individual methods are implemented efficiently.

Prefer:

sorter := esort.New().
	ByString(func(p Person) string { return p.GivenName }, esort.Desc).
	ByInt(func(p Person) int { return p.ID }, esort.Asc)

Over:

sorter := esort.New().
	ByFunc(func(l, r Person) bool {
		if l.GivenName != r.GivenName {
			return l.GivenName < r.GivenName
		}
	}, esort.Desc).
	ByInt(func(p Person) int { return p.ID }, esort.Asc)

Prefer defining the sorters as top-level variables if they have a static sorting basis. They will allocate zero memory during later program runtime — provided of course no user-defined functions registered with ByFunc do.

User-Defined Sorting Functions

The ByFunc method enables types to be sorted by user-defined functions. The ByFunc method follows the same logic as the other instructions; it is appended to the rule set and evaluated accordingly.

If the function provided to ByFunc compares only one sorting basis, it may be implemented in a naive fashion whereby it only compares one side of the basis. Consider a data-transmission object (DTO)

func lessDTO(l, r *dto.SomeData) bool {
	return l.GetField() < r.GetField()
}

sorter := esort.New().
	ByFunc(lessDTO, esort.Asc)

The function lessDTO needn't consider the inverse case:

func lessDTO(l, r *dto.SomeData) bool {
	if l.GetField() < r.GetField() {
		return true
	}
	return r.GetField() < l.GetField()
}

The Sorter checks inverse cases automatically.

Ergonomics of Value Accessors

The examples above are implemented using anonymous functions, similar to this:

func(p Person) string { return p.GivenName }

You are free to use free-standing, top-level functions:

func name(p Person) string { return p.GivenName }

sorter := esort.New().
	ByFunc(name, esort.Asc)

And method expressions on the types, though consider whether getters are even appropriate for the API first:

func (p Person) GivenName string { return p.givenName }

sorter := esort.New().
	ByFunc(p.GivenName, esort.Asc)

Performance

A hand-implemented sort.Interface.Less function readily beats a Sorter. As implemented today, a Sorter with a simple double-compound sort criteria takes approximately 5-7x the time of a hand-written Less.

Index

Constants

View Source
const (
	// Asc sorts the results in ascending order.
	Asc = Dir(iota)
	// Desc sorts the results in descending order.
	Desc
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Dir

type Dir int

Dir represents the direction for the sort.

type SortFunc

type SortFunc[T any] func(l, r T) bool

SortFunc sorts the data according to an arbitrary function.

SortFunc mimics a sort.Interface.Less function. Functions that sort by one criterion may return a simple l before r check. Consider:

sorter := esort.New().
	ByFunc(func(l, r T) bool {
		return l.GivenName < r.GivenName
	})

Functions that perform compound comparisons must perform symmetric checks of r before l in order to produce sensible results:

sorter := esort.New().
	ByFunc(func(l, r T) bool {
		if l.GivenName != r.GivenName {
			return l.GivenName < r.GivenName
		}
		return l.ID < r.ID
	})

Implementations of SortFunc must not sort in a way that conflicts with a pre-existing instruction in a Sorter; otherwise inconsistent results may be produced.

Using the native By-prefixed functions to generate sorting rule sets is preferable to using this API.

type Sorter

type Sorter[T any] struct {
	// contains filtered or unexported fields
}

Sorter is the representation of a compound sorting program. A Sorter is safe for concurrent use by multiple goroutines.

func New

func New[T any]() *Sorter[T]

New creates a Sorter.

func (*Sorter[T]) ByBool

func (s *Sorter[T]) ByBool(f func(T) bool, d Dir) *Sorter[T]

ByBool sorts the data by a given boolean value.

func (*Sorter[T]) ByByte

func (s *Sorter[T]) ByByte(f func(T) byte, d Dir) *Sorter[T]

ByByte sorts the data by a given byte value.

func (*Sorter[T]) ByBytes

func (s *Sorter[T]) ByBytes(f func(T) []byte, d Dir) *Sorter[T]

ByBytes sorts the data by a given byte slice value.

func (*Sorter[T]) ByFloat32

func (s *Sorter[T]) ByFloat32(f func(T) float32, d Dir) *Sorter[T]

ByFloat32 sorts the data by a given float32 value.

func (*Sorter[T]) ByFloat64

func (s *Sorter[T]) ByFloat64(f func(T) float64, d Dir) *Sorter[T]

ByFloat64 sorts the data by a given float64 value.

func (*Sorter[T]) ByFunc

func (s *Sorter[T]) ByFunc(f SortFunc[T], d Dir) *Sorter[T]

ByFunc sorts the data according to an arbitrary given SortFunc. The SortFunc must not the underlying data by that any pre-existing intruction does.

func (*Sorter[T]) ByInt

func (s *Sorter[T]) ByInt(f func(T) int, d Dir) *Sorter[T]

ByInt sorts the data by a given int value.

func (*Sorter[T]) ByInt16

func (s *Sorter[T]) ByInt16(f func(T) int16, d Dir) *Sorter[T]

ByInt16 sorts the data by a given int16 value.

func (*Sorter[T]) ByInt32

func (s *Sorter[T]) ByInt32(f func(T) int32, d Dir) *Sorter[T]

ByInt32 sorts the data by a given int32 value.

func (*Sorter[T]) ByInt64

func (s *Sorter[T]) ByInt64(f func(T) int64, d Dir) *Sorter[T]

ByInt64 sorts the data by a given int64 value.

func (*Sorter[T]) ByInt8

func (s *Sorter[T]) ByInt8(f func(T) int8, d Dir) *Sorter[T]

ByInt8 sorts the data by a given int8 value.

func (*Sorter[T]) ByPointer

func (s *Sorter[T]) ByPointer(f func(T) uintptr, d Dir) *Sorter[T]

ByPointer sorts the data by a given uintptr value.

func (*Sorter[T]) ByRune

func (s *Sorter[T]) ByRune(f func(T) rune, d Dir) *Sorter[T]

ByRune sorts the data by a given rune value.

func (*Sorter[T]) ByString

func (s *Sorter[T]) ByString(f func(T) string, d Dir) *Sorter[T]

ByString sorts the data by a given string value.

func (*Sorter[T]) ByUint

func (s *Sorter[T]) ByUint(f func(T) uint, d Dir) *Sorter[T]

ByUint sorts the data by a given uint value.

func (*Sorter[T]) ByUint16

func (s *Sorter[T]) ByUint16(f func(T) uint16, d Dir) *Sorter[T]

ByUint16 sorts the data by a given uint16 value.

func (*Sorter[T]) ByUint32

func (s *Sorter[T]) ByUint32(f func(T) uint32, d Dir) *Sorter[T]

ByUint32 sorts the data by a given uint32 value.

func (*Sorter[T]) ByUint64

func (s *Sorter[T]) ByUint64(f func(T) uint64, d Dir) *Sorter[T]

ByUint64 sorts the data by a given uint64 value.

func (*Sorter[T]) ByUint8

func (s *Sorter[T]) ByUint8(f func(T) uint8, d Dir) *Sorter[T]

ByUint8 sorts the data by a given uint8 value.

func (*Sorter[T]) Less

func (s *Sorter[T]) Less(l, r T) bool

Less is a sort ordering function that fulfills the contract expected by sort.Interface.Less and related APIs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL