diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..38cd050 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,163 @@ +package cache + +import ( + "errors" + "sync" + "time" + + "github.com/snple/types" +) + +var ( + ErrNotFound = errors.New("cache: key not found") +) + +type Value[T any] struct { + Data T + TTL time.Duration + Updated time.Time +} + +func newValue[T any](data T, ttl time.Duration) Value[T] { + return Value[T]{ + Data: data, + TTL: ttl, + Updated: time.Now(), + } +} + +func (v *Value[T]) Alive() bool { + return v != nil && (v.TTL == 0 || time.Since(v.Updated) <= v.TTL) +} + +type Cache[T any] struct { + data map[string]Value[T] + lock sync.RWMutex + miss func(key string) (T, time.Duration, error) +} + +func NewCache[T any](miss func(key string) (T, time.Duration, error)) *Cache[T] { + return &Cache[T]{ + data: make(map[string]Value[T]), + lock: sync.RWMutex{}, + miss: miss, + } +} + +func (c *Cache[T]) GetValue(key string) types.Option[Value[T]] { + c.lock.RLock() + defer c.lock.RUnlock() + + if value, ok := c.data[key]; ok { + return types.Some(value) + } + + return types.None[Value[T]]() +} + +func (c *Cache[T]) Get(key string) types.Option[T] { + c.lock.RLock() + defer c.lock.RUnlock() + + if value, ok := c.data[key]; ok && value.Alive() { + return types.Some(value.Data) + } + + return types.None[T]() +} + +func (c *Cache[T]) GetWithMiss(key string) (types.Option[T], error) { + if v := c.Get(key); v.IsSome() { + return v, nil + } + + if c.miss != nil { + value, ttl, err := c.miss(key) + if err != nil { + return types.None[T](), err + } + + c.Set(key, value, ttl) + return types.Some(value), nil + } + + return types.None[T](), ErrNotFound +} + +func (c *Cache[T]) Set(key string, value T, ttl time.Duration) { + c.lock.Lock() + defer c.lock.Unlock() + + c.data[key] = newValue(value, ttl) +} + +func (c *Cache[T]) AutoSet(key string, fn func(key string) (T, time.Duration, error), duration time.Duration) chan<- struct{} { + quit := make(chan struct{}) + + go func() { + ticker := time.NewTicker(duration) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if value, ttl, err := fn(key); err == nil { + c.Set(key, value, ttl) + } + case <-quit: + return + } + } + }() + + return quit +} + +func (c *Cache[T]) Delete(key string) { + c.lock.Lock() + defer c.lock.Unlock() + delete(c.data, key) +} + +func (c *Cache[T]) DeleteAll() { + c.lock.Lock() + defer c.lock.Unlock() + c.data = make(map[string]Value[T]) +} + +func (c *Cache[T]) Size() int { + c.lock.RLock() + defer c.lock.RUnlock() + return len(c.data) +} + +func (c *Cache[T]) GC() { + c.lock.Lock() + defer c.lock.Unlock() + + for key, value := range c.data { + if !value.Alive() { + delete(c.data, key) + } + } +} + +func (c *Cache[T]) AutoGC(duration time.Duration) chan<- struct{} { + quit := make(chan struct{}) + + go func() { + ticker := time.NewTicker(duration) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + c.GC() + case <-quit: + return + } + } + }() + + return quit +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fb5b3de --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/snple/types + +go 1.18 diff --git a/option.go b/option.go new file mode 100644 index 0000000..10df2c7 --- /dev/null +++ b/option.go @@ -0,0 +1,63 @@ +package types + +type Option[T any] struct { + value *T +} + +func Some[T any](value T) Option[T] { + return Option[T]{ + value: &value, + } +} + +func None[T any]() Option[T] { + return Option[T]{ + value: nil, + } +} + +func (o *Option[T]) IsSome() bool { + return o.value != nil +} + +func (o *Option[T]) IsNone() bool { + return o.value == nil +} + +func (o *Option[T]) Unwrap() T { + if o.IsNone() { + panic("called `Option.Get()` on a `None` value") + } + + return *o.value +} + +func (o *Option[T]) Unchecked(msg string) *T { + return o.value +} + +func (o *Option[T]) Take() Option[T] { + if o.IsNone() { + return Option[T]{ + value: nil, + } + } + + ret := Option[T]{ + value: o.value, + } + + o.value = nil + + return ret +} + +func (o *Option[T]) Replace(value T) Option[T] { + tmp := *o + + o = &Option[T]{ + value: o.value, + } + + return tmp +} diff --git a/option_test.go b/option_test.go new file mode 100644 index 0000000..13974fa --- /dev/null +++ b/option_test.go @@ -0,0 +1,37 @@ +package types + +import "testing" + +func TestOption(t *testing.T) { + if res := Some(123); res.IsSome() { + t.Log("the result is", res.Get()) + } else { + panic("result should be some") + } + + if res := None[int](); res.IsNone() { + t.Log("the result is none") + } else { + panic("result should be none") + } + + if res := Some(123); res.IsSome() { + t.Log("the result is", res.Get()) + + take := res.Take() + t.Log("the result is", take.Get()) + t.Log("the result is", res.IsNone()) + + } else { + panic("result should be some") + } + + res := Some(456) + t.Log("the result is", res.Get()) + + res2 := res.Replace(789) + t.Log("the result is", res2.Get()) + res2 = Some(789) + t.Log("the result is", res2.Get()) + t.Log("the result is", res.Get()) +} diff --git a/queue/queen_test.go b/queue/queen_test.go new file mode 100644 index 0000000..59d4ce2 --- /dev/null +++ b/queue/queen_test.go @@ -0,0 +1,188 @@ +package queue + +import "testing" + +func TestQueueSimple(t *testing.T) { + q := New[int]() + + for i := 0; i < minQueueLen; i++ { + q.Push(i) + } + for i := 0; i < minQueueLen; i++ { + n := q.Peek() + if n.Get() != i { + t.Error("peek", i, "had value", q.Peek()) + } + x := q.Pop() + if x.Get() != i { + t.Error("remove", i, "had value", x) + } + } +} + +func TestQueueWrapping(t *testing.T) { + q := New[int]() + + for i := 0; i < minQueueLen; i++ { + q.Push(i) + } + for i := 0; i < 3; i++ { + q.Pop() + q.Push(minQueueLen + i) + } + + for i := 0; i < minQueueLen; i++ { + n := q.Peek() + if n.Get() != i+3 { + t.Error("peek", i, "had value", q.Peek()) + } + q.Pop() + } +} + +func TestQueueLength(t *testing.T) { + q := New[int]() + + if q.Length() != 0 { + t.Error("empty queue length not 0") + } + + for i := 0; i < 1000; i++ { + q.Push(i) + if q.Length() != i+1 { + t.Error("adding: queue with", i, "elements has length", q.Length()) + } + } + for i := 0; i < 1000; i++ { + q.Pop() + if q.Length() != 1000-i-1 { + t.Error("removing: queue with", 1000-i-i, "elements has length", q.Length()) + } + } +} + +func TestQueueGet(t *testing.T) { + q := New[int]() + + for i := 0; i < 1000; i++ { + q.Push(i) + for j := 0; j < q.Length(); j++ { + n := q.Get(j) + if n.Get() != j { + t.Errorf("index %d doesn't contain %d", j, j) + } + } + } +} + +func TestQueueGetNegative(t *testing.T) { + q := New[int]() + + for i := 0; i < 1000; i++ { + q.Push(i) + for j := 1; j <= q.Length(); j++ { + n := q.Get(-j) + if n.Get() != q.Length()-j { + t.Errorf("index %d doesn't contain %d", -j, q.Length()-j) + } + } + } +} + +func TestQueueGetOutOfRangePanics(t *testing.T) { + q := New[int]() + + q.Push(1) + q.Push(2) + q.Push(3) + + assertPanics(t, "should panic when negative index", func() { + n := q.Get(-4) + n.Get() + }) + + assertPanics(t, "should panic when index greater than length", func() { + n := q.Get(4) + n.Get() + }) +} + +func TestQueuePeekOutOfRangePanics(t *testing.T) { + q := New[int]() + + assertPanics(t, "should panic when peeking empty queue", func() { + n := q.Peek() + n.Get() + }) + + q.Push(1) + q.Pop() + + assertPanics(t, "should panic when peeking emptied queue", func() { + n := q.Peek() + n.Get() + }) +} + +func TestQueueRemoveOutOfRangePanics(t *testing.T) { + q := New[int]() + + assertPanics(t, "should panic when removing empty queue", func() { + n := q.Pop() + n.Get() + }) + + q.Push(1) + q.Pop() + + assertPanics(t, "should panic when removing emptied queue", func() { + n := q.Pop() + n.Get() + }) +} + +func assertPanics(t *testing.T, name string, f func()) { + defer func() { + if r := recover(); r == nil { + t.Errorf("%s: didn't panic as expected", name) + } + }() + + f() +} + +// General warning: Go's benchmark utility (go test -bench .) increases the number of +// iterations until the benchmarks take a reasonable amount of time to run; memory usage +// is *NOT* considered. On my machine, these benchmarks hit around ~1GB before they've had +// enough, but if you have less than that available and start swapping, then all bets are off. + +func BenchmarkQueueSerial(b *testing.B) { + q := New[interface{}]() + for i := 0; i < b.N; i++ { + q.Push(nil) + } + for i := 0; i < b.N; i++ { + q.Peek() + q.Pop() + } +} + +func BenchmarkQueueGet(b *testing.B) { + q := New[int]() + for i := 0; i < b.N; i++ { + q.Push(i) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + q.Get(i) + } +} + +func BenchmarkQueueTickTock(b *testing.B) { + q := New[interface{}]() + for i := 0; i < b.N; i++ { + q.Push(nil) + q.Peek() + q.Pop() + } +} diff --git a/queue/queue.go b/queue/queue.go new file mode 100644 index 0000000..1ce8255 --- /dev/null +++ b/queue/queue.go @@ -0,0 +1,101 @@ +/* +Package queue provides a fast, ring-buffer queue based on the version suggested by Dariusz Górecki. +Using this instead of other, simpler, queue implementations (slice+append or linked list) provides +substantial memory and time benefits, and fewer GC pauses. +The queue implemented here is as fast as it is for an additional reason: it is *not* thread-safe. +*/ +package queue + +import "github.com/snple/types" + +// minQueueLen is smallest capacity that queue may have. +// Must be power of 2 for bitwise modulus: x % n == x & (n - 1). +const minQueueLen = 16 + +// Queue represents a single instance of the queue data structure. +type Queue[T any] struct { + buf []types.Option[T] + head, tail, count int +} + +// New constructs and returns a new Queue. +func New[T any]() Queue[T] { + return Queue[T]{ + buf: make([]types.Option[T], minQueueLen), + } +} + +// Length returns the number of elements currently stored in the queue. +func (q *Queue[T]) Length() int { + return q.count +} + +// resizes the queue to fit exactly twice its current contents +// this can result in shrinking if the queue is less than half-full +func (q *Queue[T]) resize() { + newBuf := make([]types.Option[T], q.count<<1) + + if q.tail > q.head { + copy(newBuf, q.buf[q.head:q.tail]) + } else { + n := copy(newBuf, q.buf[q.head:]) + copy(newBuf[n:], q.buf[:q.tail]) + } + + q.head = 0 + q.tail = q.count + q.buf = newBuf +} + +// Push puts an element on the end of the queue. +func (q *Queue[T]) Push(elem T) { + if q.count == len(q.buf) { + q.resize() + } + + q.buf[q.tail] = types.Some(elem) + // bitwise modulus + q.tail = (q.tail + 1) & (len(q.buf) - 1) + q.count++ +} + +// Peek returns the element at the head of the queue. +func (q *Queue[T]) Peek() types.Option[T] { + if q.count <= 0 { + return types.None[T]() + } + return q.buf[q.head] +} + +// Get returns the element at index i in the queue. +// This method accepts both positive and negative index values. +// Index 0 refers to the first element, and index -1 refers +// to the last. +func (q *Queue[T]) Get(i int) types.Option[T] { + // If indexing backwards, convert to positive index. + if i < 0 { + i += q.count + } + if i < 0 || i >= q.count { + return types.None[T]() + } + // bitwise modulus + return q.buf[(q.head+i)&(len(q.buf)-1)] +} + +// Pop removes and returns the element from the front of the queue. +func (q *Queue[T]) Pop() types.Option[T] { + if q.count <= 0 { + return types.None[T]() + } + ret := q.buf[q.head] + q.buf[q.head] = types.None[T]() + // bitwise modulus + q.head = (q.head + 1) & (len(q.buf) - 1) + q.count-- + // Resize down if buffer 1/4 full. + if len(q.buf) > minQueueLen && (q.count<<2) == len(q.buf) { + q.resize() + } + return ret +}