Coder Social home page Coder Social logo

immutable's Introduction

Immutable release test coverage license

This repository contains generic immutable collection types for Go. It includes List, Map, SortedMap, Set and SortedSet implementations. Immutable collections can provide efficient, lock free sharing of data by requiring that edits to the collections return new collections.

The collection types in this library are meant to mimic Go built-in collections such asslice and map. The primary usage difference between Go collections and immutable collections is that immutable collections always return a new collection on mutation so you will need to save the new reference.

Immutable collections are not for every situation, however, as they can incur additional CPU and memory overhead. Please evaluate the cost/benefit for your particular project.

Special thanks to the Immutable.js team as the List & Map implementations are loose ports from that project.

List

The List type represents a sorted, indexed collection of values and operates similarly to a Go slice. It supports efficient append, prepend, update, and slice operations.

Adding list elements

Elements can be added to the end of the list with the Append() method or added to the beginning of the list with the Prepend() method. Unlike Go slices, prepending is as efficient as appending.

// Create a list with 3 elements.
l := immutable.NewList[string]()
l = l.Append("foo")
l = l.Append("bar")
l = l.Prepend("baz")

fmt.Println(l.Len())  // 3
fmt.Println(l.Get(0)) // "baz"
fmt.Println(l.Get(1)) // "foo"
fmt.Println(l.Get(2)) // "bar"

Note that each change to the list results in a new list being created. These lists are all snapshots at that point in time and cannot be changed so they are safe to share between multiple goroutines.

Updating list elements

You can also overwrite existing elements by using the Set() method. In the following example, we'll update the third element in our list and return the new list to a new variable. You can see that our old l variable retains a snapshot of the original value.

l := immutable.NewList[string]()
l = l.Append("foo")
l = l.Append("bar")
newList := l.Set(2, "baz")

fmt.Println(l.Get(1))       // "bar"
fmt.Println(newList.Get(1)) // "baz"

Deriving sublists

You can create a sublist by using the Slice() method. This method works with the same rules as subslicing a Go slice:

l = l.Slice(0, 2)

fmt.Println(l.Len())  // 2
fmt.Println(l.Get(0)) // "baz"
fmt.Println(l.Get(1)) // "foo"

Please note that since List follows the same rules as slices, it will panic if you try to Get(), Set(), or Slice() with indexes that are outside of the range of the List.

Iterating lists

Iterators provide a clean, simple way to iterate over the elements of the list in order. This is more efficient than simply calling Get() for each index.

Below is an example of iterating over all elements of our list from above:

itr := l.Iterator()
for !itr.Done() {
	index, value, _ := itr.Next()
	fmt.Printf("Index %d equals %v\n", index, value)
}

// Index 0 equals baz
// Index 1 equals foo

By default iterators start from index zero, however, the Seek() method can be used to jump to a given index.

Efficiently building lists

If you are building large lists, it is significantly more efficient to use the ListBuilder. It uses nearly the same API as List except that it updates a list in-place until you are ready to use it. This can improve bulk list building by 10x or more.

b := immutable.NewListBuilder[string]()
b.Append("foo")
b.Append("bar")
b.Set(2, "baz")

l := b.List()
fmt.Println(l.Get(0)) // "foo"
fmt.Println(l.Get(1)) // "baz"

Builders are invalid after the call to List().

Map

The Map represents an associative array that maps unique keys to values. It is implemented to act similarly to the built-in Go map type. It is implemented as a Hash-Array Mapped Trie.

Maps require a Hasher to hash keys and check for equality. There are built-in hasher implementations for most primitive types such as int, uint, and string keys. You may pass in a nil hasher to NewMap() if you are using one of these key types.

Setting map key/value pairs

You can add a key/value pair to the map by using the Set() method. It will add the key if it does not exist or it will overwrite the value for the key if it does exist.

Values may be fetched for a key using the Get() method. This method returns the value as well as a flag indicating if the key existed. The flag is useful to check if a nil value was set for a key versus a key did not exist.

m := immutable.NewMap[string,int](nil)
m = m.Set("jane", 100)
m = m.Set("susy", 200)
m = m.Set("jane", 300) // overwrite

fmt.Println(m.Len())   // 2

v, ok := m.Get("jane")
fmt.Println(v, ok)     // 300 true

v, ok = m.Get("susy")
fmt.Println(v, ok)     // 200, true

v, ok = m.Get("john")
fmt.Println(v, ok)     // nil, false

Removing map keys

Keys may be removed from the map by using the Delete() method. If the key does not exist then the original map is returned instead of a new one.

m := immutable.NewMap[string,int](nil)
m = m.Set("jane", 100)
m = m.Delete("jane")

fmt.Println(m.Len())   // 0

v, ok := m.Get("jane")
fmt.Println(v, ok)     // nil false

Iterating maps

Maps are unsorted, however, iterators can be used to loop over all key/value pairs in the collection. Unlike Go maps, iterators are deterministic when iterating over key/value pairs.

m := immutable.NewMap[string,int](nil)
m = m.Set("jane", 100)
m = m.Set("susy", 200)

itr := m.Iterator()
for !itr.Done() {
	k, v := itr.Next()
	fmt.Println(k, v)
}

// susy 200
// jane 100

Note that you should not rely on two maps with the same key/value pairs to iterate in the same order. Ordering can be insertion order dependent when two keys generate the same hash.

Efficiently building maps

If you are executing multiple mutations on a map, it can be much more efficient to use the MapBuilder. It uses nearly the same API as Map except that it updates a map in-place until you are ready to use it.

b := immutable.NewMapBuilder[string,int](nil)
b.Set("foo", 100)
b.Set("bar", 200)
b.Set("foo", 300)

m := b.Map()
fmt.Println(m.Get("foo")) // "300"
fmt.Println(m.Get("bar")) // "200"

Builders are invalid after the call to Map().

Implementing a custom Hasher

If you need to use a key type besides int, uint, or string then you'll need to create a custom Hasher implementation and pass it to NewMap() on creation.

Hashers are fairly simple. They only need to generate hashes for a given key and check equality given two keys.

type Hasher[K any] interface {
	Hash(key K) uint32
	Equal(a, b K) bool
}

Please see the internal intHasher, uintHasher, stringHasher, and byteSliceHasher for examples.

Sorted Map

The SortedMap represents an associative array that maps unique keys to values. Unlike the Map, however, keys can be iterated over in-order. It is implemented as a B+tree.

Sorted maps require a Comparer to sort keys and check for equality. There are built-in comparer implementations for int, uint, and string keys. You may pass a nil comparer to NewSortedMap() if you are using one of these key types.

The API is identical to the Map implementation. The sorted map also has a companion SortedMapBuilder for more efficiently building maps.

Implementing a custom Comparer

If you need to use a key type besides int, uint, or string or derived types, then you'll need to create a custom Comparer implementation and pass it to NewSortedMap() on creation.

Comparers on have one method—Compare(). It works the same as the strings.Compare() function. It returns -1 if a is less than b, returns 1 if a is greater than b, and returns 0 if a is equal to b.

type Comparer[K any] interface {
	Compare(a, b K) int
}

Please see the internal defaultComparer for an example, bearing in mind that it works for several types.

Set

The Set represents a collection of unique values, and it is implemented as a wrapper around a Map[T, struct{}].

Like Maps, Sets require a Hasher to hash keys and check for equality. There are built-in hasher implementations for most primitive types such as int, uint, and string keys. You may pass in a nil hasher to NewSet() if you are using one of these key types.

Sorted Set

The SortedSet represents a sorted collection of unique values. Unlike the Set, however, keys can be iterated over in-order. It is implemented as a B+tree.

Sorted sets require a Comparer to sort values and check for equality. There are built-in comparer implementations for int, uint, and string keys. You may pass a nil comparer to NewSortedSet() if you are using one of these key types.

The API is identical to the Set implementation.

Contributing

The goal of immutable is to provide stable, reasonably performant, immutable collections library for Go that has a simple, idiomatic API. As such, additional features and minor performance improvements will generally not be accepted. If you have a suggestion for a clearer API or substantial performance improvement, please open an issue first to discuss. All pull requests without a related issue will be closed immediately.

Please submit issues relating to bugs & documentation improvements.

immutable's People

Contributors

adrianboyko avatar astef avatar banks avatar barrenszeppelin avatar benbjohnson avatar infogulch avatar laher avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

immutable's Issues

Feature Request: Make datastructures as Interface

Currently, the data structures themself (Map, List) are designed as structs.
Do you think in a future major release it would be possible to change them to interfaces. This would open up the door for different implementations of the structures.

This would be very helpful to define different implementations with their own strengths, like a merkle tree implementation for write heavy scenarios, or an implementation which is almost always just used as append only. We work in a embedded environment and if such implementations would exists, we could choose for every location where we use lists, which implementation would give the best performance.

What do you think?

Hasher for byte array

It would also be useful not only to have hashes for slices, but also for arrays.

Would this be possible?

Generics?

I wonder if this library is willing to adopt Go generics, and whether or not it's aiming to be actively maintained in the future? I'm interested in having immutable data structures for a project I'm working on, and I'm thinking about converting this code to use generics. Shall I contribute this back, if I actually end up doing the fork?

Key constraint might not be ideal

I have a wrapper of immutable that adds sets and some other generic interface stuff. I'm having difficulty mapping it onto v0.4.0 of immutable. I read the PR for the generics, and I think that constraints.Ordered isn't quite right for the key values. I haven't looked into it more, but I suspect it's too strict and that comparable should be possible instead, particularly because the user already has to provide comparer/hasher types in the constructors.

@laher

List - varargs for Append, NewList, Prepend

Would it be desirable to amend the signatures for these List operations?

e.g.

func (l *List[T]) Append(value ...T) *List[T] {

2 benefits:

  • Ergonomics: no need for ListBuilder in many cases
  • Efficiently add multiple to an existing List

Being varargs, it wouldn't break existing callers.

Note that we could implement something similar for maps - maybe a SetAll(map[k]v) but that could be separate.

I'm happy to submit a PR for this

Set/SortedSet types

Would it be OK to add a Set & SortedSet types, wrapping the Map/SortedMap types?

It seems like a natural use case for immutable types, now that we have generics.

Beyond the convenience & readability benefits (vs using immutable.Map[T,struct{}]), I would find it useful for adding Intersection, Union, Complement & Difference methods.

These could potentially be in a subdirectory package, or at least a separate file. I've prototyped something in the same package, it seems nice.

I'd be happy to submit a PR for this, but wanted to check first.

Ta.

SortedSetBuilder Doesn't Have a Mechanism To Return The Set

As far as I can tell, SortedSetBuilder doesn't have a mechanism to allow the caller to retrieve the built Set. I propose adding a method, func(s SortedSetBuilder[T])()SortedSet[T] which is analogous to ListBuilder.List() and MapBuilder.Map() respectively.

Happy to make a PR for this if you think it's useful

Why ImmutableList of size 7 occupies 128 bytes?

List below contains 7 elements, but when I inspect it with debugger it resizes to length of 32, filling the rest with empty strings. I'm curious, what is the reason for that? I understand that resizing is expensive, but it seems like a little bit too much at first glance.

list := immutable.NewList[string](
	"one",
	 "two",
	"three",
	"four",
	"five",
	"six",
	"seven",
)

image

image

Objects stored in maps are not stored as copies

So I was using this as a mock repository for my unit tests.
And I figured it out that after storing them in the immutable map, the element that I stored, if I change it, I will still be able to change values inside it.

So for complete immutability, you would need that value to be copied within.
maybe something like this?

https://go.dev/play/p/J5OJ-i7m0ue

Allow Creating Immutable Builders (Map/SortedSet/List) intialised with initial Values

Right now, `ListBuilder, MapBuilder and SortedSetBuilder) are intialised with empty data structures. This is great if you want to create an object from scratch, but doesn't cover the use case where you have an existing object and want to make a large number of modifications to it.

I propose we add functions that allow a user to create builders from initial data structures. For example:

NewSortedSetBuilderFromExisting[T any](s SortedSet[T]) *SortedSetBuilder[T] 

We would guarantee that the builder itself would mutate a copy so that the initial data structure would remain immutable.

Happy to make a PR if you think this would be useful.

Higher-order procedures

I know that the goal of this project is to provide a clean and efficient minimal API but i think having for example a some high order procedures like Filter will help improve usage and dx.

Consider changing immutable types such as List[T] and Map[T] to use by-value semantics instead of by-reference

I notice that List[T] and Map[T] use by-reference semantics whereas Set[T] uses by-value semantics. I propose that all the main immutable types in this package use by-value semantics like Set[T] already does. By-value semantics both imply and re-enforce immutability. If a method takes a pointer receiver, someone learning the API may wonder if the method somehow changes its receiver. But when a method takes a value receiver, this is a signal that the method doesn't change its receiver. Also by-reference semantics can't guarantee immutability. A client of an API could do something like this with pointers.

var t *SomeImmutableType
t = NewSomeImmutableType(...)
*t = SomeImmutableType{} // Oops, what t points to has unwittingly changed!

By-value semantics prevents this sort of API misuse and re-enforces immutability.

Note that using by-value sematics means that the type must fully support assignment. That is after b := a b gets the value of a at that time. Later changes in a, don't get reflected in b and vice versa. Even with complicated data structures like trees and such, a developer can support full assignment using copy-on-write tricks.

Also it is helpful for immutable by-value types to support zero values. After a declaration like

var empty List[int]

empty would be a fully usable empty list of ints.

deprecate/remove Builders

I don't think there's any need for the builders if/once this is merged: #35 . Adding multiple items at once can be achieved without using them.

So, I don't know for sure, but they seem redundant to me now.

  • We could just delete them from the codebase.
  • Alternatively, just mark them depracted and/or update the Example tests & README guidance

Maybe I'm missing something, IDK

Feature Request: Delete List element

Currently there is no easy way to delete an element from a list.
Only recreating the list can be done what is very annoying.

Something like this would be very helpful:

l = l.Delete(1) // Deletes the element at index 1

Feature Request: Allow to append/prepend multiple values

With go slices it is possible to append multiple values:

s = append(s, anotherSlice...)

This is quite cumbersome currently with immutable, and also would create a lot of unnecessary lists.

Would it be possible to add support for this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.