= Persistent-💎: Because Immutable Data Is Forever
:toc:
:toc-placement: macro
:toclevels: 4
:toc-title:
image:https://badge.fury.io/rb/persistent-dmnd.svg["Gem Version", link="https://badge.fury.io/rb/persistent-dmnd"]
Are you tired of calling .freeze
on your data structures (or your colleagues forgetting to do so)? +
Do you wish Ruby had a literal for creating immutable arrays?
Then persistent-💎 aka persistent diamond is for you!
Persistent-💎 gives you a very tidy way of creating immutable...
[source,ruby]
my_array = a💎[1, 2, 3]
[source,ruby]
my_hash = h💎[key1: 'foo', key2: 'bar']
[source,ruby]
my_set = s💎[:sephiroth, :kills, :aeris]
...and it behaves as you expect it to:
- You can compare immutable data structures with regular Ruby instances
[source,ruby]
a💎[1, 2] == [1, 2] && h💎[key1: 'foo'] == {key1: 'foo'} && s💎[:hello] == Set.new([:hello])
=> true
- You can compare immutable hashes with
<
/+++<=+++
/>=
/>
and with regular Ruby hashes:
[source,ruby]
h💎[a: 1] < h💎[a: 1, b: 2] && {a: 1, b: 2} < h💎[a: 1, b: 2, c: 3]
=> true
- You can compare immutable sets with
<
/+++<=+++
/>=
/>
and with regular Ruby sets:
[source,ruby]
s💎[1] < s💎[1, 2] && Set.new([1, 2]) < s💎[1, 2, 3]
=> true
- You can splat (
*
) immutable arrays:
[source,ruby]
def sum(a, b, c)
a + b + c
end
sum(*a💎[1, 2, 39])
=> 42
sum(1, *a💎[2, 39])
=> 42
- You can double-splat (
**
) immmutable hashes:
[source,ruby]
def hello(name:, age:)
"Hello there #{name}, you are #{age} years old!"
end
hello(h💎[name: 'User', age: '50'])
=> "Hello there User, you are 50 years old!"
hello(name: 'Another User', **h💎[age: '50'])
=> "Hello there Another User, you are 50 years old!"
Beyond being immutable, these data structures are thread-safe, and can be efficiently copied: when you "update" them, a new copy gets created that shares most of its structure with the original. Thus, creating new instances from existing structures is both memory-efficient and quite fast!
It also (optionally!) interoperates with the https://github.com/ruby-concurrency/concurrent-ruby[concurrent-ruby gem], for when you need that extra Oomph (or just thread-safe mutability). See <<concurrent-ruby-interoperability,below>> for more details.
Underneath the covers, persistent-💎 mostly builds atop the awesome https://github.com/immutable-ruby/immutable-ruby[immutable-ruby gem].
Big thanks to its equally-awesome authors!
Persistent-💎 is fully supported and tested on Ruby versions 2.4 to 3.3, JRuby 9.2 to 9.4, and TruffleRuby 🎉.
If we don't support your Ruby, it's probably a Python binary instead (or a potato?).
Keep calm and 💎 away!
[discrete]
== Contents
toc::[]
== Installation
Add this line to your application's gems.rb
or Gemfile
:
[source,ruby]
gem 'persistent-dmnd'
And then execute:
[source,bash]
$ bundle install
Or install it yourself as:
[source,bash]
$ gem install persistent-dmnd
This gem is versioned according to http://semver.org/spec/v2.0.0.html[Semantic Versioning].
== Usage
To use persistent-💎, first load it:
[source,ruby]
require 'persistent-💎'
note: you can also use require 'persistent-dmnd'
Persistent-💎 can be added as a module to individual classes (or even to other modules!):
[source,ruby]
class FooController
include Persistent💎
note: you can also use include PersistentDmnd
ARGUMENTS = a💎[:name, :address, :likes_icecream] # Usable inside this class...
def stuff
a💎[:stuff, :more_stuff] # ...and its methods
end
end
Or you can add it to your whole application by just doing
[source,ruby]
require 'persistent_dmnd/everywhere'
a💎[:freeeeeeedooooom] # usable everyhere in your application
As you may have noticed, everywhere there is an 💎
, you can replace it with dmnd
, e.g. PersistentDmnd
instead of Persistent💎
for the gem module and for aDmnd[]
instead of a💎[]
to create an array.
=== Creating new persistent structures
==== Array
Use a💎[]
(or aDmnd[]
) to create a new array:
[source,ruby]
empty_array = a💎[]
=> Persistent💎::Array[]
my_array = a💎[:hello, :world]
=> Persistent💎::Array[:hello, :world]
==== Hash
Use h💎[]
(or hDmnd[]
) to create a new hash:
[source,ruby]
empty_hash = h💎[]
=> Persistent💎::Hash[]
my_hash = h💎['hello' => 'world']
=> Persistent💎::Hash["hello" => "world"]
==== Set
Use s💎[]
(or sDmnd[]
) to create a new set:
[source,ruby]
empty_set = s💎[]
=> Persistent💎::Set[]
2.4.2 :028 > my_set = s💎[:hello, :world]
=> Persistent💎::Set[:hello, :world]
=== Converting from existing structures
You can use 💎ify[]
(or dmndify[]
) to convert any received argument to a persistent structure (without modifying the original).
It is great for getting a protected copy of your input, that you can now store, operate on and share among threads without concern.
It works for all the persistent structures above:
[source,ruby]
my_array = a💎[:hello, :world]
💎ify[my_array]
=> Persistent💎::Array[:hello, :world]
my_hash = h💎['hello' => 'world']
💎ify[my_hash]
=> Persistent💎::Hash["hello" => "world"]
my_set = s💎[:hello, :world]
💎ify[my_set]
=> Persistent💎::Set[:hello, :world]
It works for regular Ruby arrays (and any object that implements to_ary()
):
[source,ruby]
my_array = [:regular, :ruby, :array]
💎ify[my_array]
=> Persistent💎::Array[:regular, :ruby, :array] # Not regular any more! :)
It works for regular Ruby hashes (and any object that implements to_hash()
):
[source,ruby]
my_hash = {regular: :ruby, hash: nil}
💎ify[my_hash]
=> Persistent💎::Hash[:hash => nil, :regular => :ruby]
It works for regular Ruby sets (and any object that implements to_set()
):
[source,ruby]
my_set = Set.new([:regular, :ruby, :set])
💎ify[my_set]
=> Persistent💎::Set[:regular, :ruby, :set]
And it works for https://github.com/hamstergem/hamster[hamster gem] (Hamster::Vector
, Hamster::Hash
, Hamster::Set
), https://github.com/immutable-ruby/immutable-ruby[immutable-ruby gem] (Immutable::Vector
, Immutable::Hash
, Immutable::Set
) and https://github.com/ruby-concurrency/concurrent-ruby[concurrent-ruby gem] (Concurrent::Array
, Concurrent::Tuple
, Concurrent::Hash
, Concurrent::Map
) data structures:
[source,ruby]
my_vector = Immutable::Vector[1, 2, 3]
💎ify[my_vector]
=> Persistent💎::Array[1, 2, 3]
my_array = Concurrent::Array[1, 2, 3]
💎ify[my_array]
=> Persistent💎::Array[1, 2, 3]
my_tuple = Concurrent::Tuple.new(1)
my_tuple.set(0, :hello)
💎ify[my_tuple]
=> Persistent💎::Array[:hello]
my_hash = Immutable::Hash[hello: :world]
💎ify[my_hash]
=> Persistent💎::Hash[:hello => :world]
my_hash = Concurrent::Hash[hello: :world]
💎ify[my_hash]
=> Persistent💎::Hash[:hello => :world]
my_map = Concurrent::Map.new
my_map[:hello] = :world
💎ify[my_map]
=> Persistent💎::Hash[:hello => :world]
my_set = Immutable::Set[:hello, :world]
💎ify[my_set]
=> Persistent💎::Set[:hello, :world]
And you can even implement it on your own classes:
[source,ruby]
class MyList
include Persistent💎
def initialize(item1, item2, item3)
@item1 = item1
@item2 = item2
@item3 = item3
end
def to_💎 # can also be #to_dmnd
a💎[@item1, @item2, @item3]
end
end
my_list = MyList.new(:hello, :there, :readers)
💎ify[my_list]
=> Persistent💎::Array[:hello, :there, :readers]
=== Converting to regular Ruby structures
The usual to_a()
/to_h()
/to_set()
can be used to convert persistent data structures back to their regular Ruby counterparts:
[source,ruby]
a💎[1, 2].to_a
=> [1, 2]
h💎[hello: :world].to_h
=> {:hello=>:world}
s💎[1, 2].to_set
=> #<Set: {1, 2}>
=== Converting between persistent structures
All three persistent structures implement to_a💎()
(or to_aDmnd()
), to_h💎()
(or to_hDmnd()
) and to_s💎()
(or to_sDmnd()
) as persistent counterparts for the usual Ruby to_a()
, to_h()
and to_s()
:
[source,ruby]
a💎[1, 2].to_a💎
=> Persistent💎::Array[1, 2]
a💎[1, 2].to_s💎
=> Persistent💎::Set[1, 2]
a💎[['hello', 'world']].to_h💎
=> Persistent💎::Hash["hello" => "world"]
h💎['hello' => 'world'].to_h💎
=> Persistent💎::Hash["hello" => "world"]
h💎['hello' => 'world'].to_a💎
=> Persistent💎::Array[Persistent💎::Array["hello", "world"]]
h💎['hello' => 'world'].to_s💎
=> Persistent💎::Set[Persistent💎::Array["hello", "world"]]
s💎[1, 2].to_s💎
=> Persistent💎::Set[1, 2]
s💎[1, 2].to_a💎
=> Persistent💎::Array[1, 2]
s💎[['hello', 'world']].to_h💎
=> Persistent💎::Hash["hello" => "world"]
=== Concurrent Ruby interoperability
When you need to go from thread-safe immutable data structures to thread-safe mutable data structures you can use Persistent-💎's optional interoperability with the https://github.com/ruby-concurrency/concurrent-ruby[concurrent-ruby gem].
You'll need to install concurrent-ruby first, see https://github.com/ruby-concurrency/concurrent-ruby#installation for instructions.
NOTE: If you're using TruffleRuby, you'll need to use concurrent-ruby >= 1.1.0, as older versions did not work correctly on TruffleRuby.
After that, you'll be able to:
==== Array
Use to_concurrent()
(or to_concurrent_array()
) to convert your array into a https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Array.html[`Concurrent::Array`]:
[source,ruby]
my_array = a💎[:hello, :world]
my_concurrent_array = my_array.to_concurrent
Use to_concurrent_tuple()
to convert your array into a https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Tuple.html[`Concurrent::Tuple`]:
[source,ruby]
my_array = a💎[:hello, :world]
my_concurrent_tuple = my_array.to_concurrent_tuple
=> #<Concurrent::Tuple @size=2, @tuple=[<#Concurrent::AtomicReference value:hello>, <#Concurrent::AtomicReference value:world>]>
==== Hash
Use to_concurrent()
(or to_concurrent_hash()
) to convert your hash into a https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Hash.html[`Concurrent::Hash`]:
[source,ruby]
my_hash = h💎[hello: :world]
my_concurrent_hash = my_hash.to_concurrent
Use to_concurrent_map()
to convert your hash into a https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Map.html[`Concurrent::Map`]:
[source,ruby]
my_hash = h💎[hello: :world]
my_concurrent_map = my_hash.to_concurrent_map
=> #<Concurrent::Map:0x0055ad9b283ea0 entries=1 default_proc=nil>
=== API documentation for the persistent structures
Because the persistent structures are provided by the awesome https://github.com/immutable-ruby/immutable-ruby[immutable-ruby gem], you can refer back to immutable-ruby's API docs for details on the operations provided by each data structure.
==== Array
Built on top of Immutable::Vector
==== Hash
Built on top of Immutable::Hash
==== Set
Built on top of Immutable::Set
== AAARGH YOU FIEND WHY IS THERE AN EMOJI ON MY CODEBASE?
Every printable ascii character is already in use by Ruby, but I didn't want persistent data structures to clutter my source code. I also did not want to use cryptic single, two-letter or three-letter acronyms. Ruby is supposed to be beautifully readable!
Thus, I kept my Ruby beautiful. With two very clear characters you can create an immutable data structure. No more awkward typing of namespaces. No more .freeze
everywhere. No-one will ever mistake the use of 💎
for another operation.
Alas, you may not agree with me! The good news is that you can avoid having 💎
on your codebase altogether: just use dmnd
as a replacement, as <<usage,documented above>>.
If you're having a hard time typing the emoji, I recommend just adding a quick snippet to your editor or a quick command to search-and-replace aDmnd
/hDmnd
/sDmnd
/dmndify
for a💎
/h💎
/s💎
/💎ify
. That way you get best of both worlds: easy to type, and beautiful to read!
== Usage on Ruby 1.9
Because of our usage of emojis for method names, you'll need to add
[source,ruby]
encoding: UTF-8
as the first (or second) line of any file that uses Persistent-💎. As an alternative, you can also <<usage,use the dmnd
syntax>>.
This setting is the default from Ruby 2.0 on, so users of later versions do not need to worry about this small detail.
== Development
After checking out the repo, run bundle install
to install dependencies. Then, run rake spec
to run the tests.
To open a console with the gem loaded, run bundle console
.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to https://rubygems.org[rubygems.org].
== Feedback and success stories
Your feedback is welcome!
Please leave a comment on https://gitlab.com/ivoanjo/persistent-dmnd/issues/8 and share how persistent-💎 delighted (or disappointed? 😱) you today!
== Contributing
Bug reports and pull requests are welcome on GitLab at https://gitlab.com/ivoanjo/persistent-dmnd.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the http://contributor-covenant.org[Contributor Covenant] code of conduct.
Maintained with 💎❤️ by https://ivoanjo.me/[Ivo Anjo].
== Thanks
Thanks to these amazing people for their contributions!
== License
The gem is available as open source under the terms of the https://opensource.org/licenses/MIT[MIT License].
== Code of Conduct
Everyone interacting in the Persistent-💎 project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the https://gitlab.com/ivoanjo/persistent-dmnd/blob/master/CODE_OF_CONDUCT.adoc[code of conduct].
== Interesting links
Interested in immutable/persistent data structures? Here are some interesting resources for your exploration: