Janet 1.42.0-dev-7fd75f3 Documentation
(Other Versions:
1.42.0
1.41.2
1.41.1
1.40.1
1.40.0
1.39.1
1.38.0
1.37.1
1.36.0
1.35.0
1.34.0
1.31.0
1.29.1
1.28.0
1.27.0
1.26.0
1.25.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.0
1.18.1
1.17.1
1.16.1
1.15.0
1.13.1
1.12.2
1.11.1
1.10.1
1.9.1
1.8.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.0
1.3.1
)
Data Structures
Once you have a handle on functions and the primitive value types, you may be wondering how to work with collections of things. Janet has a small number of core data structure types that are very versatile. Arrays, tuples, tables, structs, buffers, and strings (as well as symbols and keywords) are the main built-in data structure types. These data structures can be arranged in a useful table describing their relationship to each other.
| Interface | Mutable | Immutable |
|---|---|---|
| Indexed | Array | Tuple |
| Dictionary | Table | Struct |
| Bytes | Buffer | String (Symbol, Keyword) |
Indexed types are linear lists of elements than can be accessed in constant time via an integer index. Indexed types are backed by a single chunk of memory for fast access, and are indexed from 0 as in C.
Dictionary types associate keys with values. The difference between dictionaries and indexed types is that dictionaries are not limited to integer keys. They are backed by a hash table and also offer constant time lookup (and insertion for the mutable case).
Finally, the bytes type or byte sequence is a type that contains a sequence of bytes. A bytes value or byteseq associates integer keys (the indices) with integer values between 0 and 255 (the byte values). In this way, they behave much like arrays and tuples. However, one cannot put non-integer values into a byteseq.
The table below summarizes the big-O complexity of various operations and other information of the built-in data structures. All primitive operations on data structures will run in constant time regardless of the number of items in the data structure.
| Data Structure | Access | Insert/Append | Delete | Space Complexity | Mutable |
|---|---|---|---|---|---|
| Array * | O(1) | O(1) | O(1) | O(n) | Yes |
| Tuple | O(1) | - | - | O(n) | No |
| Table | O(1) | O(1) | O(1) | O(n) | Yes |
| Struct | O(1) | - | - | O(n) | No |
| Buffer | O(1) | O(1) | O(1) | O(n) | Yes |
| String/Keyword/Symbol | - | - | - | O(n) | No |
*: Append and delete for an array correspond to array/push and
array/pop. Removing or inserting elements at an arbitrary
index will run in O(n) time where n is the number of
elements in the array.
(def my-tuple [1 2 3])
(def another-tuple (tuple 1 2 3))
(def my-array @[1 2 3])
(def another-array (array 1 2 3))
(def my-struct
{:key "value"
:key2 "another"
1 2
4 3})
(def another-struct (struct :a 1 :b 2))
(def my-table
@{:a :b
:c :d
:A :qwerty})
(def another-table (table 1 2 3 4))
(def my-buffer @"thisismutable")
(def my-buffer2 @``This is also mutable``)To read the values in a data structure, use the get or
in functions. The first parameter is the data structure
itself, and the second parameter is the key. An optional third
parameter can be used to specify a default if the value is not found.
(get @[:a :b :c] 2) # -> :c
(in @[:a :b :c] 2) # -> :c
(get (tuple "a" "b" "c") 1) # -> "b"
(get @{:a 1} :a) # -> 1
(get {:a 1} :a) # -> 1
(in {:a 1} :a) # -> 1
# default values returned as :c was not found
(get {:a :b} :c :d) # -> :d
(in {:a :b} :c :d) # -> :d
(get @"hello, world" 1) # -> 101
(get "hello, world" 1) # -> 101
(in "hello, world" 1) # -> 101The in function (added in v1.5.0) for tables and structs
behaves identically to get. However, for indexed and bytes
types, the in function will throw errors on bad keys, so to
better detect errors, prefer in for these.
# nil returned as the key was not found
(get @[:a :b :c] 3) # -> nil
# errors since the key is out of bounds (> 2)
(in @[:a :b :c] 3)To update a mutable data structure, use the put function. It
takes 3 arguments, the data structure, the key, and the value, and
returns the data structure. The allowed types of keys and values
depend on the data structure passed in.
(put @[] 3 :a) # -> @[nil nil nil :a]
(put @{} :key "value") # -> @{:key "value"}
(put @"" 3 97) # -> @"\0\0\0a"Note that for arrays and buffers, putting an index that is outside the
length of the data structure will extend the data structure and fill
it with nils in the case of an array, or 0s in the case
of a buffer.
The last generic function for all data structures is the length
function. It returns the number of values in a data structure. In
the case of a dictionary type, this is also the same as the number of
keys.
Copying
Copying of indexed and dictionary types involves "shallow" copying by default. This means that any references to contained indexed or dictionary types are copied instead of the content being recursively duplicated. This is both for efficiency reasons and because it's not always obvious or well-defined as to how copying should occur.
One way to copy arrays is via the array/slice function.
(def arr @[:a :b])
(def arr1 @[:x arr])
(def arr2 (array/slice arr1))
# change inner array content
(array/push arr :c)
arr # -> @[:a :b :c]
# arr1 and arr2 both "contain" arr
arr1 # -> @[:x @[:a :b :c]]
arr2 # -> @[:x @[:a :b :c]]Tables may be copied via the table/clone function.
(def arr @[:a :b])
(def tab1 @{:x arr})
(def tab2 (table/clone tab1))
# change array content
(array/push arr :c)
arr # -> @[:a :b :c]
# tab1 and tab2 both "contain" arr
tab1 # -> @{:x @[:a :b :c]}
tab2 # -> @{:x @[:a :c :c]}For simple cases, recursive or "deep" copies can be made by
applying the marshal and unmarshal functions.
(def arr @[:a :b])
(def tab1 @{:x arr})
(def tab2 (unmarshal (marshal tab1)))
# change array content
(array/push arr :c)
arr # -> @[:a :b :c]
# tab1 "contains" arr, tab2 has "replica" of old arr
tab1 # -> @{:x @[:a :b :c]}
tab2 # -> @{:x @[:a :b]}