Skip to content

Commit

Permalink
Merge pull request #2 from porfirion/typed
Browse files Browse the repository at this point in the history
Typed implementation
  • Loading branch information
porfirion authored Jun 2, 2023
2 parents 057f53f + c638a41 commit 243ff2b
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 201 deletions.
42 changes: 28 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
# Trie - compact and efficient radix tree (Patricia trie) implementation in go
# Trie - compact and efficient generic radix tree (Patricia trie) implementation in go

Efficient implementation with zero allocation for read operations (Get and Search) and 1 or 2 allocations per Add
Efficient generic implementation with zero allocation for read operations (Get and Search) and 1 or 2 allocations per Put operation

[![Go Reference](https://pkg.go.dev/badge/github.com/porfirion/trie.svg)](https://pkg.go.dev/github.com/porfirion/trie)
[![Go Report Card](https://goreportcard.com/badge/github.com/porfirion/trie)](https://goreportcard.com/report/github.com/porfirion/trie)
[![Coverage Status](https://coveralls.io/repos/github/porfirion/trie/badge.svg?branch=master)](https://coveralls.io/github/porfirion/trie?branch=master)

Current implementation (due to lack of generics) uses interface{} to store values. But it's type defined as an alias, and you can easily copy source file and replace alias with any other nil'able type (pointer or other interface) and get a definitely typed implementation:

```go
type ValueType = interface{}
```

## Installation

go get github.com/porfirion/trie

## Usage

```go
tr := &trie.Trie[int]
tr.PutString("hello", 1) // same as tr.Put([]byte("hello"), 1)
// OR
tr := trie.BuildFromMap(map[string]int{
"hello": 1
})

v, ok := tr.GetByString("hello")
fmt.Println(v)
```

Trie can be used in different ways:

1. Primarily I created it for searching emojis :smile: in text (in Telegram messages). There are about 3,3k emojis in current standard (https://www.unicode.org/emoji/charts-13.0/emoji-counts.html) and checking them one by one is very costly. For this purpose I added export package: you can generate source code for trie with all available emojis and compile it in your program.
1. Primarily I created it for searching emojis :smile: in text (in Telegram messages). There are about 3,3k emojis
in current standard (https://www.unicode.org/emoji/charts-13.0/emoji-counts.html) and checking them one by one
is very costly. For this purpose I added export package: you can generate source code for trie with all available
emojis and compile it in your program.

2. You can use it as map, where key is a slice of arbitrary bytes (`map[[]byte]interface{}` which is not possible in language because slices are not comparable and can't be used as keys).
2. You can use it as map, where key is a slice of arbitrary bytes (`map[[]byte]interface{}` which is not possible
in language because slices are not comparable and can't be used as keys).

3. You can use this trie to check for any string prefixes (possibly storing some payload for each prefix). See example below.

4. You can build some router using this Trie. For this purpose I added `SubTrie(mask []byte)` method that returns sub trie with all entries, prefixed by specified mask, and `GetAll(mask []byte)` that returns all entries containing specified mask. See example below.
4. You can build some http router using this Trie. For this purpose I added `SubTrie(mask []byte)` method that returns
sub trie with all entries, prefixed by specified mask, and `GetAll(mask []byte)` that returns all entries containing
specified mask. See example below.

Also, this implementation supports zero-length prefix (`[]byte{}` or `nil`). Value associated with this prefix can be used as fallback when no other entries found. Or it can serve as universal prefix for all entries.
Also, this implementation supports zero-length prefix (`[]byte{}` or `nil`). Value associated with this prefix can be
used as fallback when no other entries found. Or it can serve as universal prefix for all entries.

Limitation: `nil` can't be stored as value, because node containing nil value considered empty.
Note: Trie stores pointers to values inside, but if your values are really large (structures or arrays) - consider
using pointers as type parameter: Trie[*MyStruct]. It will prevent copying of large structures when getting result of Get.

## Examples

Expand All @@ -51,7 +65,7 @@ import (
// prefixes.PutString("three", 3)
// prefixes.PutString("", 0)
//
var prefixes = trie.BuildFromMap(map[string]interface{}{
var prefixes = trie.BuildFromMap[int](map[string]interface{}{
"one": 1,
"two": 2,
"three": 3,
Expand Down
19 changes: 9 additions & 10 deletions constructors.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
package trie

// BuildFromMap may be useful for var declaration
func BuildFromMap(inputs map[string]ValueType) *Trie {
t := &Trie{}
func BuildFromMap[T any](inputs map[string]T) *Trie[T] {
t := &Trie[T]{}
for key, value := range inputs {
t.Put([]byte(key), value)
}
return t
}

// BuildFromList can be used to create Trie with arbitrary bytes slice as key (not valid strings, etc)
func BuildFromList(inputs []struct {
func BuildFromList[T any](inputs []struct {
Key []byte
Value ValueType
}) *Trie {
t := &Trie{}
Value T
}) *Trie[T] {
t := &Trie[T]{}
for i := range inputs {
t.Put(inputs[i].Key, inputs[i].Value)
}
return t
}

// BuildPrefixesOnly used to create just searching prefixes without any data
func BuildPrefixesOnly(strs ...string) *Trie {
type dummy struct{}
func BuildPrefixesOnly(strs ...string) *Trie[struct{}] {

t := &Trie{}
t := &Trie[struct{}]{}

for i := range strs {
t.Put([]byte(strs[i]), dummy{})
t.Put([]byte(strs[i]), struct{}{})
}

return t
Expand Down
14 changes: 7 additions & 7 deletions example_prefixes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
)

// Also can be created with
// prefixes := &trie.Trie{}
// prefixes.PutString("one", 1)
// prefixes.PutString("two", 2)
// prefixes.PutString("three", 3)
// prefixes.PutString("", 0)
//
var prefixes = BuildFromMap(map[string]ValueType{
// prefixes := &trie.Trie{}
// prefixes.PutString("one", 1)
// prefixes.PutString("two", 2)
// prefixes.PutString("three", 3)
// prefixes.PutString("", 0)
var prefixes = BuildFromMap(map[string]int{
"one": 1,
"two": 2,
"three": 3,
Expand All @@ -28,7 +28,7 @@ func Example_prefixes() {

for _, inp := range inputs {
if val, prefixLen, ok := prefixes.SearchPrefixInString(inp); ok {
fmt.Println(strings.Repeat(inp[prefixLen:], val.(int)))
fmt.Println(strings.Repeat(inp[prefixLen:], val))
}
}

Expand Down
39 changes: 38 additions & 1 deletion example_routing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package trie

import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)

var routes = BuildFromMap(map[string]interface{}{
var routes = BuildFromMap(map[string]string{
"": "root", // as universal prefix
"/api/user": "user",
"/api/user/list": "usersList",
Expand Down Expand Up @@ -37,3 +40,37 @@ func Example_routing() {
// /api/group : handler group (route [root group])
// /api/unknown : handler not found (route [root])
}

func Example_handlers() {
tr := &Trie[http.Handler]{}

tr.PutString("/", http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
_, _ = fmt.Fprint(w, "Index page")
}))
tr.PutString("/home", http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
_, _ = fmt.Fprint(w, "Home page")
}))
tr.PutString("/profile", http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
_, _ = fmt.Fprint(w, "Profile page")
}))

requests := []*http.Request{
httptest.NewRequest("GET", "/profile", nil),
httptest.NewRequest("GET", "/", nil),
httptest.NewRequest("GET", "/home", nil),
}

for _, req := range requests {
if f, ok := tr.GetByString(req.RequestURI); ok {
rec := httptest.NewRecorder()
f.ServeHTTP(rec, req)
resp, err := io.ReadAll(rec.Result().Body)
fmt.Println(string(resp), err)
}
}

// Output:
// Profile page <nil>
// Index page <nil>
// Home page <nil>
}
22 changes: 11 additions & 11 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ func ExampleBuildPrefixesOnly() {
}

func ExampleTrie_String() {
example := &T{Prefix: []byte{0xF0, 0x9F, 0x91}, Value: "short", Children: &[256]*T{
0x10: {Prefix: []byte{0x10}, Value: "modified"},
0xA8: {Prefix: []byte{0xA8}, Value: "nokey", Children: &[256]*T{
0xE2: {Prefix: []byte{0xE2, 0x80, 0x8D}, Value: "withsep", Children: &[256]*T{
0xF0: {Prefix: []byte{0xF0, 0x9F, 0x94, 0xA7}, Value: "withkey"},
example := &T{Prefix: []byte{0xF0, 0x9F, 0x91}, Value: ptr("short"), Children: &[256]*T{
0x10: {Prefix: []byte{0x10}, Value: ptr("modified")},
0xA8: {Prefix: []byte{0xA8}, Value: ptr("nokey"), Children: &[256]*T{
0xE2: {Prefix: []byte{0xE2, 0x80, 0x8D}, Value: ptr("withsep"), Children: &[256]*T{
0xF0: {Prefix: []byte{0xF0, 0x9F, 0x94, 0xA7}, Value: ptr("withkey")},
}},
}},
}}
Expand All @@ -39,15 +39,15 @@ func ExampleTrie_String() {
}

func ExampleTrie_Iterate() {
example := &T{Prefix: []byte{0xF0, 0x9F, 0x91}, Value: "short", Children: &[256]*T{
0x10: {Prefix: []byte{0x10}, Value: "modified"},
0xA8: {Prefix: []byte{0xA8}, Value: "nokey", Children: &[256]*T{
0xE2: {Prefix: []byte{0xE2, 0x80, 0x8D}, Value: "withsep", Children: &[256]*T{
0xF0: {Prefix: []byte{0xF0, 0x9F, 0x94, 0xA7}, Value: "withkey"},
example := &T{Prefix: []byte{0xF0, 0x9F, 0x91}, Value: ptr("short"), Children: &[256]*T{
0x10: {Prefix: []byte{0x10}, Value: ptr("modified")},
0xA8: {Prefix: []byte{0xA8}, Value: ptr("nokey"), Children: &[256]*T{
0xE2: {Prefix: []byte{0xE2, 0x80, 0x8D}, Value: ptr("withsep"), Children: &[256]*T{
0xF0: {Prefix: []byte{0xF0, 0x9F, 0x94, 0xA7}, Value: ptr("withkey")},
}},
}},
}}
example.Iterate(func(prefix []byte, value interface{}) {
example.Iterate(func(prefix []byte, value string) {
fmt.Printf("[%v] %+v\n", prefix, value)
})
// Output:
Expand Down
50 changes: 42 additions & 8 deletions export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ package export

import (
"fmt"
"reflect"
"strconv"
"strings"

"github.com/porfirion/trie"
)

//go:generate go run gen.go

type Exportable interface {
Export() string
}

var (
// string representation of byte (0x01..0xFF)
bytesRep [256]string
Expand All @@ -20,7 +27,7 @@ func init() {
}

// Exports Trie as go code (compatible with gofmt).
func Export(t *trie.Trie, settings ExportSettings) string {
func Export[T any](t *trie.Trie[T], settings ExportSettings) string {
if t == nil {
return settings.CurrentPadding + "nil"
}
Expand Down Expand Up @@ -87,19 +94,44 @@ func (settings ExportSettings) ForChild() ExportSettings {
return settings
}

type Exportable interface {
Export() string
func exportGenericType[T any]() string {
var v T
res := reflect.TypeOf(&v).Elem().String()
if res == "interface {}" {
return "[any]"
} else {
return "[" + res + "]"
}
}

func exportValue(v interface{}) string {
func exportValue[T any](v *T) (res string) {
if v == nil {
return `nil`
}
switch val := v.(type) {
defer func() {
res = "ptr" + exportGenericType[T]() + "(" + res + ")"
}()
defer func() {
if reflect.ValueOf(v).Elem().Type().Kind() != reflect.Interface {
// │ │ └ T type itself
// │ └ value T
// └ pointer to T

//
return
}
tp := reflect.ValueOf(v).Elem().Elem().Type()
switch tp.Kind() {
case reflect.String, reflect.Bool, reflect.Int:
return
default:
res = "(" + tp.String() + ")" + "(" + res + ")"
return
}
}()
switch val := any(*v).(type) {
case Exportable:
return val.Export()
case *string:
return `"` + *val + `"` // + fmt.Sprintf(`/*%s*/`, stringToBytes(val))
case string:
return `"` + val + `"` // + fmt.Sprintf(`/*%s*/`, stringToBytes(val))
case int:
Expand All @@ -121,7 +153,9 @@ func exportValue(v interface{}) string {
case bool:
return strconv.FormatBool(val)
default:
return fmt.Sprintf(`%#v`, v)
// There can be wrong formatting.
// If so - you should just implement Exportable interface
return fmt.Sprintf(`%+v`, *v)
}
}

Expand Down
Loading

0 comments on commit 243ff2b

Please sign in to comment.