From f3c6addb3d5c4d695cf120f23870c053b2b1b803 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:15:31 +0100 Subject: [PATCH 01/10] feat: Updated `/book/maps` page * All of the available stuff! * And mention of issues with large maps and how to avoid them --- pages/book/contracts.mdx | 2 +- pages/book/maps.mdx | 251 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 238 insertions(+), 15 deletions(-) diff --git a/pages/book/contracts.mdx b/pages/book/contracts.mdx index 02ffb4e7..d196a0f0 100644 --- a/pages/book/contracts.mdx +++ b/pages/book/contracts.mdx @@ -46,7 +46,7 @@ contract Example { } ``` -State variables must have a default value or initialized in [`init(){:tact}`](#init-function) function, that runs on deployment of the contract. +State variables must have a default value or initialized in [`init(){:tact}`](#init-function) function, that runs on deployment of the contract. The only exception is persistent state variables of type [`map{:tact}`](/book/maps) since they are initialized empty by default. diff --git a/pages/book/maps.mdx b/pages/book/maps.mdx index 7b018564..693d3d54 100644 --- a/pages/book/maps.mdx +++ b/pages/book/maps.mdx @@ -2,42 +2,265 @@ import { Callout } from 'nextra/components' -The type `map{:tact}` is used as a way to associate keys of type `k` with corresponding values of type `v`. +The [composite type](/book/types#composite-types) `map{:tact}` is used as a way to associate keys of type `k` with corresponding values of type `v`. -Possible key types: +For example, `map{:tact}` uses [`Int{:tact}`][int] type for its keys and values: -* [`Int{:tact}`][ints] -* `Address{:tact}` +```tact +struct IntToInt { + counters: map; +} +``` + +## Allowed types -Possible value types: +Allowed key types: -* [`Int{:tact}`][ints] +* [`Int{:tact}`][int] +* [`Address{:tact}`][p] + +Allowed value types: + +* [`Int{:tact}`][int] * [`Bool{:tact}`](/book/types#booleans) -* `Cell{:tact}` -* `Address{:tact}` +* [`Cell{:tact}`][p] +* [`Address{:tact}`][p] * [Struct](/book/structs-and-messages#structs) * [Message](/book/structs-and-messages#messages) -For example, `map{:tact}` uses [`Int{:tact}`][ints] type for its keys and values: +## Operations + +### Declare + +As a [local variable](/book/statements#let), using `emptyMap(){:tact}` function of standard library: ```tact -struct IntToInt { - counters: map; +let fizz: map = emptyMap(); +let fizz: map = null; // identical to the previous line, but less descriptive +``` + +As a [persistent state variables](/book/contracts#variables): + +```tact +contract Example { + fizz: map; // Int keys to Int values + init() { + self.fizz = emptyMap(); // redundant and can be removed! + } +} +``` + +Note, that [persistent state variables](/book/contracts#variables) of type `map{:tact}` are initialized empty by default and don't need default values or an initialization in the [`init(){:tact}` function](/book/contracts#init-function). + +### Set values, `.set()` [#set] + +To set or replace the value under a key call the `.set(){:tact}` [method](/book/functions#extension-function), which is accessible for all maps. + +```tact +// Empty map +let fizz: map = emptyMap(); + +// Setting a couple of values under different keys +fizz.set(7, "nice"); +fizz.set(42, "the answer"); + +// Overriding one of the existing key-value pairs +fizz.set(7, "42 over 6"); // key 7 now points to value "42 over 6" +``` + +### Get values, `.get()` [#get] + +To check if a key is found in the map by calling the `.get(){:tact}` [method](/book/functions#extension-function), which is accessible for all maps. This will return `null{:tact}` if the key is missing, or the value if the key is found. + +```tact +// Empty map +let fizz: map = emptyMap(); + +// Setting a value +fizz.set(68, "almost nice"); + +// Getting the value by its key +let gotButUnsure: String? = fizz.get(68); // returns String or null, therefore the type is String? +let mustHaveGotOrErrored: String = fizz.get(68)!!; // explicitly asserting that the value must not be null, + // which may crush at runtime if the value is, in fact, null + +// Alternatively, we can check for the key in the if statement +if (gotButUnsure != null) { + // Hooray, let's use !! without fear now and cast String? to String + let definitelyGotIt: String = fizz.get(68)!!; +} else { + // Do something else... +} +``` + +### Delete entries + +To delete a single key-value pair (single entry), simply assign the `null{:tact}` value to the key when using the [`.set(){:tact}`](#set) [method](/book/functions#extension-function). + +```tact +// Empty map +let fizz: map = emptyMap(); + +// Setting a couple of values under different keys +fizz.set(7, "nice"); +fizz.set(42, "the answer"); + +// Deleting one of the keys +fizz.set(7, null); // the entry under key 7 is now deleted +``` + +To delete all the entries from the map, re-assign the map using the `emptyMap(){:tact}` function: + +```tact +// Empty map +let fizz: map = emptyMap(); + +// Setting a couple of values under different keys +fizz.set(7, "nice"); +fizz.set(42, "the answer"); + +// Deleting all of the entries at once +fizz = emptyMap(); +fizz = null; // identical to the previous line, but less descriptive +``` + +### Convert to a `Cell`, `.asCell()` + +Use `.asCell(){:tact}` [method](/book/functions#extension-function) on maps to convert all their values to a [`Cell{:tact}`][p] type. Be mindful, that [`Cell{:tact}`][p] type is able to store up to 1023 bits, so converting larger maps to the Cell will result in error. + +As an example, this method is useful for sending small maps directly in the body of the reply: + +```tact +contract Example { + // Persistent state variables + fizz: map; // our map + + // Constructor (initialization) function of the contract + init() { + // Setting a bunch of values + self.fizz.set(0, 3); + self.fizz.set(1, 14); + self.fizz.set(2, 15); + self.fizz.set(3, 926); + self.fizz.set(4, 5_358_979_323_846); + } + + // Internal message receiver, which responds to empty messages + receive() { + // Here we're converting the map to a Cell and making a reply with it + self.reply(self.fizz.asCell()); + } +} +``` + +### Traverse over entries + +At the moment Tact doesn't have a special syntax for iterating over maps. However, it's possible to use maps as a simple arrays if you define a `map{:tact}` with an [`Int{:tact}`][int] type for the keys and keep track of the number of items in the separate variable: + +```tact +contract MapAsArray { + // Persistent state variables + arr: map; // «array» of String values as a map + arrLength: Int = 0; // length of the «array», defaults to 0 + + // Constructor (initialization) function of the contract + init() {} + + // Internal function for pushing an item to the end of the «array» + fun arrPush(item: String) { + self.arr.set(self.arrLength, item); + self.arrLength += 1; + } + + // Internal message receiver, which responds to a String message "item" + receive("item") { + // add a new item + increase the counter + self.arrPush("item"); + } + + // Internal message receiver, which responds to a String message "get_last" + receive("get_last") { + // Reply back with the latest item in the map if it exists + if (self.arrLength > 0) { + // Note, that we use !! operator as we know for sure that the value would be there + self.reply((self.arr.get(self.arrLength - 1)!!).asComment()); + } + } + + // Getter function for obtaining the «array» + get fun map(): map { + return self.arr; + } + + // Getter function for obtaining the current length of the «array» + get fun length(): Int { + return self.arrLength; + } } ``` -Additionally, maps allow [integer serialization](/book/integers#serialization-types) of their keys, values or both to [preserve space and reduce storage costs](/book/integers#serialization): +Additionally, it's useful to set an upper-bound restriction on such maps, so that you [don't hit the limits](#limits-and-drawbacks). + +## Serialization + +It's possible to do [integer serialization](/book/integers#serialization-types) of map keys, values or both to [preserve space and reduce storage costs](/book/integers#serialization): ```tact struct SerializedMapInside { + // Both keys and values here would be serialized as 8-bit unsigned integers, + // thus preserving the space and reducing storage costs: countersButCompact: map; } ``` - Read about other serialization options: [Compatibility with FunC](/book/func#convert-serialization) + Read about other serialization options: [Compatibility with FunC](/book/func#convert-serialization). -[ints]: /book/integers +## Limits and drawbacks + +While maps can be convenient to work with on a small scale, they cause a number of issues if the number of items is unbounded and map can significantly grow in size: + +* As the upper bound of the smart contract state size is around $65\,000$ items of type [`Cell{:tact}`][p], it constrains the storage limit of maps to be about $30\,000$ key-value pairs for the whole contract. + +* The more entries you have in a map, the bigger [compute fees](https://docs.ton.org/develop/howto/fees-low-level#computation-fees) you'll get. Thus, working with large maps makes compute fees tough to predict and manage. + +* Using a large map in a single contract doesn't allow to distribute its workload. Hence, it can make the overall performance much worse compared to using a smaller map and a bunch of interacting smart contracts. + +To resolve such issues you can set an upper-bound restriction on a map as a constant and check against it every time you're setting a new value to the map: + +```tact +contract Example { + // Declare a compile-time constant upper-bound for our map + const MaxMapSize: Int = 42; + + // Persistent state variables + arr: map; // "array" of String values as a map + arrLength: Int = 0; // length of the "array", defaults to 0 + + // Constructor (initialization) function of the contract + init() {} + + // Internal function for pushing an item to the end of the "array" + fun arrPush(item: String) { + if (self.arrLength >= self.MaxMapSize) { + // Do something, stop the operation, for example + } else { + // Proceed with adding new item + self.arr.set(self.arrLength, item); + self.arrLength += 1; + } + } +} +``` + +If you still need a large map or an unbound (infinitely large) map, it's better to architect your smart contracts according to the [asynchronous and actor-based model of TON blockchain](https://docs.ton.org/learn/overviews/ton-blockchain). That is, to use contract sharding and essentially make the whole blockchain a part of your map(s). + +{/* + TODO: Add reference to sharding page as per: https://github.com/tact-lang/tact-docs/issues/155 +*/} + +[int]: /book/integers +[p]: /book/types#primitive-types From 7db1a9306663db559ca0241066a88699ceba9172 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:15:50 +0100 Subject: [PATCH 02/10] feat: Additional examples for maps in Cookbook --- pages/book/maps.mdx | 8 +++ pages/cookbook/index.mdx | 129 ++++++++++++++++++++++++++++++++++----- 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/pages/book/maps.mdx b/pages/book/maps.mdx index 693d3d54..ddc259a6 100644 --- a/pages/book/maps.mdx +++ b/pages/book/maps.mdx @@ -201,6 +201,14 @@ contract MapAsArray { Additionally, it's useful to set an upper-bound restriction on such maps, so that you [don't hit the limits](#limits-and-drawbacks). + + + See other examples of traversing maps in the Cookbook:\ + [How to iterate over map entries](/cookbook#how-to-iterate-over-map-entries)\ + [How to make a cyclic array with a map](/cookbook#how-to-make-a-cyclic-array-with-a-map) + + + ## Serialization It's possible to do [integer serialization](/book/integers#serialization-types) of map keys, values or both to [preserve space and reduce storage costs](/book/integers#serialization): diff --git a/pages/cookbook/index.mdx b/pages/cookbook/index.mdx index b27b083d..6e7f8d0c 100644 --- a/pages/cookbook/index.mdx +++ b/pages/cookbook/index.mdx @@ -114,30 +114,129 @@ dump("The loop is over!"); ## Map +For description and basic usage see [`map{:tact}` type in the Book](/book/maps). + +### How to iterate over map entries + ```tact -// Create an empty map -let m: map = emptyMap(); +import "@stdlib/deploy"; // for Deployable trait -// Add the key/value pair to the map -m.set(1, "a"); +contract Iteration with Deployable { + // Persistent state variables + counter: Int as uint32; // counter of map entries, serialized as a 32-bit unsigned + record: map; // Int to Address map -// Get the value by the key from the map -let first: String? = m.get(1); + // Constructor (initialization) function of the contract + init() { + self.counter = 0; // Setting the self.counter to 0 + } -// Check if the key exists -if (first == null) { - // do something... -} else { - // Cast value if the key exists - let firstStr: String = first!!; - // do something... + // Internal message receiver, which responds to a String message "Add" + receive("Add") { + // Get the Context Struct + let ctx: Context = context(); + // Set the entry: counter Int as a key, ctx.sender Address as a value + self.record.set(self.counter, ctx.sender); + // Increase the counter + self.counter = self.counter + 1; + } + + // Internal message receiver, which responds to a String message "Send" + receive("Send") { + // Loop until the value of self.counter (over all the self.record entries) + let i: Int = 0; // declare usual i for loop iterations + while (i < self.counter) { + send(SendParameters{ + bounce: false, // do not bounce back this message + to: self.record.get(i)!!, // set the sender address, knowing that key i exists in the map + value: ton("0.0000001"), // 100 nanoToncoins (nano-tons) + mode: SendIgnoreErrors, // send ignoring errors in transaction, if any + body: "SENDING".asComment() // String "SENDING" converted to a Cell as a message body + }); + i += 1; // don't forget to increase the i + } + } + + // Getter function for obtaining the value of self.record + get fun map(): map { + return self.record; + } + + // Getter function for obtaining the value of self.counter + get fun counter(): Int { + return self.counter; + } } ``` - **Useful links:**\ - [`map{:tact}` type in the Book](/book/maps) + This example is adapted from [howardpen9/while-example-tact](https://github.com/howardpen9/while-example-tact/blob/de5807fcd20dba5f6a3748d112511477fb22bfcc/contracts/awesome.tact#L19C10-L19C10). + + + +### How to make a cyclic array with a map + +```tact +import "@stdlib/deploy"; // for Deployable trait + +// This contract records the last 5 timestamps of when "timer" message was received +contract Arrays with Deployable { + // Declare a compile-time constant upper-bound for our map + const MaxMapSize: Int = 42; + + // Persistent state variables + arr: map; // this is our array implemented with a map + arrLength: Int as uint8 = 0; + arrStart: Int as uint8 = 0; // our array is cyclic + + // Constructor (initialization) function of the contract + init() {} + + // Internal function for pushing an item to the end of the array + fun arrPush(item: Int) { + if (self.arrLength < self.MaxSize) { + self.arr.set(self.arrLength, item); + self.arrLength = self.arrLength + 1; + } else { + self.arr.set(self.arrStart, item); + self.arrStart = (self.arrStart + 1) % self.MaxSize; + } + } + + // Iterate over all items in the array and dump them + fun arrPrint() { + let i: Int = self.arrStart; + repeat (self.arrLength) { + dump(self.arr.get(i)!!); // !! tells the compiler this can't be null + i = (i + 1) % self.MaxSize; + } + } + + // Internal message receiver, which responds to a String message "timer" + // and records the timestamp when it receives such message + receive("timer") { + let timestamp: Int = now(); + self.arrPush(timestamp); + } + + receive("dump") { + self.arrPrint(); + } + + get fun length(): Int { + return self.arrLength; + } + + get fun map(): map { + return self.arr; + } +} +``` + + + + This example is adapted from [Arrays page in Tact-By-Example](https://tact-by-example.org/04-arrays). From 31ed2235c7166942ad7e470e07c28265978afa3d Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:28:46 +0100 Subject: [PATCH 03/10] Apply suggestions from code review Co-authored-by: Anton Trunov --- pages/cookbook/index.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pages/cookbook/index.mdx b/pages/cookbook/index.mdx index 6e7f8d0c..9e63b230 100644 --- a/pages/cookbook/index.mdx +++ b/pages/cookbook/index.mdx @@ -138,7 +138,7 @@ contract Iteration with Deployable { // Set the entry: counter Int as a key, ctx.sender Address as a value self.record.set(self.counter, ctx.sender); // Increase the counter - self.counter = self.counter + 1; + self.counter += 1; } // Internal message receiver, which responds to a String message "Send" @@ -182,19 +182,19 @@ import "@stdlib/deploy"; // for Deployable trait // This contract records the last 5 timestamps of when "timer" message was received contract Arrays with Deployable { - // Declare a compile-time constant upper-bound for our map - const MaxMapSize: Int = 42; + // Declare a compile-time constant upper bound for our map representing a buffer + const MaxBufferSize: Int = 42; // Persistent state variables - arr: map; // this is our array implemented with a map - arrLength: Int as uint8 = 0; - arrStart: Int as uint8 = 0; // our array is cyclic + buffer: map; // this is our buffer implemented with a map + bufferLength: Int as uint8 = 0; + bufferStart: Int as uint8 = 0; // current index into the buffer // Constructor (initialization) function of the contract init() {} - // Internal function for pushing an item to the end of the array - fun arrPush(item: Int) { + // Internal function for adding a new item into the buffer + fun bufferPush(item: Int) { if (self.arrLength < self.MaxSize) { self.arr.set(self.arrLength, item); self.arrLength = self.arrLength + 1; From cbcc9d8893eaa0d6e7460b935e3f38a6b3b6d774 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:05:26 +0100 Subject: [PATCH 04/10] Apply suggestions from code review --- pages/book/maps.mdx | 49 ++++++++++++++++++++++++---------------- pages/cookbook/index.mdx | 12 +++++----- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/pages/book/maps.mdx b/pages/book/maps.mdx index ddc259a6..460a5f42 100644 --- a/pages/book/maps.mdx +++ b/pages/book/maps.mdx @@ -35,8 +35,8 @@ Allowed value types: As a [local variable](/book/statements#let), using `emptyMap(){:tact}` function of standard library: ```tact -let fizz: map = emptyMap(); -let fizz: map = null; // identical to the previous line, but less descriptive +let fizz: map = emptyMap(); +let fizz: map = null; // identical to the previous line, but less descriptive ``` As a [persistent state variables](/book/contracts#variables): @@ -58,14 +58,14 @@ To set or replace the value under a key call the `.set(){:tact}` [method](/book/ ```tact // Empty map -let fizz: map = emptyMap(); +let fizz: map = emptyMap(); // Setting a couple of values under different keys -fizz.set(7, "nice"); -fizz.set(42, "the answer"); +fizz.set(7, 7); +fizz.set(42, 42); // Overriding one of the existing key-value pairs -fizz.set(7, "42 over 6"); // key 7 now points to value "42 over 6" +fizz.set(7, 68); // key 7 now points to value 68 ``` ### Get values, `.get()` [#get] @@ -74,10 +74,10 @@ To check if a key is found in the map by calling the `.get(){:tact}` [method](/b ```tact // Empty map -let fizz: map = emptyMap(); +let fizz: map = emptyMap(); // Setting a value -fizz.set(68, "almost nice"); +fizz.set(68, 0); // Getting the value by its key let gotButUnsure: String? = fizz.get(68); // returns String or null, therefore the type is String? @@ -99,11 +99,11 @@ To delete a single key-value pair (single entry), simply assign the `null{:tact} ```tact // Empty map -let fizz: map = emptyMap(); +let fizz: map = emptyMap(); // Setting a couple of values under different keys -fizz.set(7, "nice"); -fizz.set(42, "the answer"); +fizz.set(7, 123); +fizz.set(42, 321); // Deleting one of the keys fizz.set(7, null); // the entry under key 7 is now deleted @@ -113,11 +113,11 @@ To delete all the entries from the map, re-assign the map using the `emptyMap(){ ```tact // Empty map -let fizz: map = emptyMap(); +let fizz: map = emptyMap(); // Setting a couple of values under different keys -fizz.set(7, "nice"); -fizz.set(42, "the answer"); +fizz.set(7, 123); +fizz.set(42, 321); // Deleting all of the entries at once fizz = emptyMap(); @@ -160,22 +160,31 @@ At the moment Tact doesn't have a special syntax for iterating over maps. Howeve ```tact contract MapAsArray { // Persistent state variables - arr: map; // «array» of String values as a map - arrLength: Int = 0; // length of the «array», defaults to 0 + arr: map; // «array» of Int values as a map + arrLength: Int = 0; // length of the «array», defaults to 0 // Constructor (initialization) function of the contract init() {} // Internal function for pushing an item to the end of the «array» - fun arrPush(item: String) { + fun arrPush(item: Int) { self.arr.set(self.arrLength, item); self.arrLength += 1; } + // Internal function for popping the last item of the «array» and returning its value + fun arrPop(): Int { + let lastItem: Int = self.arr.get(self.arrLength - 1)!!; + self.arr.set(self.arrLength - 1, null); + self.arrLength -= 1; + + return lastItem; + } + // Internal message receiver, which responds to a String message "item" receive("item") { // add a new item + increase the counter - self.arrPush("item"); + self.arrPush(42); } // Internal message receiver, which responds to a String message "get_last" @@ -188,7 +197,7 @@ contract MapAsArray { } // Getter function for obtaining the «array» - get fun map(): map { + get fun map(): map { return self.arr; } @@ -245,7 +254,7 @@ contract Example { const MaxMapSize: Int = 42; // Persistent state variables - arr: map; // "array" of String values as a map + arr: map; // "array" of String values as a map arrLength: Int = 0; // length of the "array", defaults to 0 // Constructor (initialization) function of the contract diff --git a/pages/cookbook/index.mdx b/pages/cookbook/index.mdx index 9e63b230..4d511acc 100644 --- a/pages/cookbook/index.mdx +++ b/pages/cookbook/index.mdx @@ -175,7 +175,7 @@ contract Iteration with Deployable { -### How to make a cyclic array with a map +### How to make a circular buffer with a map ```tact import "@stdlib/deploy"; // for Deployable trait @@ -183,7 +183,7 @@ import "@stdlib/deploy"; // for Deployable trait // This contract records the last 5 timestamps of when "timer" message was received contract Arrays with Deployable { // Declare a compile-time constant upper bound for our map representing a buffer - const MaxBufferSize: Int = 42; + const MaxBufferSize: Int = 5; // Persistent state variables buffer: map; // this is our buffer implemented with a map @@ -195,12 +195,12 @@ contract Arrays with Deployable { // Internal function for adding a new item into the buffer fun bufferPush(item: Int) { - if (self.arrLength < self.MaxSize) { + if (self.arrLength < self.MaxBufferSize) { self.arr.set(self.arrLength, item); - self.arrLength = self.arrLength + 1; + self.arrLength += 1; } else { self.arr.set(self.arrStart, item); - self.arrStart = (self.arrStart + 1) % self.MaxSize; + self.arrStart = (self.arrStart + 1) % self.MaxBufferSize; } } @@ -209,7 +209,7 @@ contract Arrays with Deployable { let i: Int = self.arrStart; repeat (self.arrLength) { dump(self.arr.get(i)!!); // !! tells the compiler this can't be null - i = (i + 1) % self.MaxSize; + i = (i + 1) % self.MaxBufferSize; } } From 95bdfe8b6d0f03342ce1c486b255593d6198a1f8 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:48:19 +0200 Subject: [PATCH 05/10] feat: New example to the Cookbook --- pages/book/maps.mdx | 33 ++++++++---- pages/cookbook/index.mdx | 108 +++++++++++++++++++++++++++++++++------ 2 files changed, 116 insertions(+), 25 deletions(-) diff --git a/pages/book/maps.mdx b/pages/book/maps.mdx index 460a5f42..668bbe6e 100644 --- a/pages/book/maps.mdx +++ b/pages/book/maps.mdx @@ -93,7 +93,7 @@ if (gotButUnsure != null) { } ``` -### Delete entries +### Delete entries [#del] To delete a single key-value pair (single entry), simply assign the `null{:tact}` value to the key when using the [`.set(){:tact}`](#set) [method](/book/functions#extension-function). @@ -124,7 +124,7 @@ fizz = emptyMap(); fizz = null; // identical to the previous line, but less descriptive ``` -### Convert to a `Cell`, `.asCell()` +### Convert to a `Cell`, `.asCell()` [#convert] Use `.asCell(){:tact}` [method](/book/functions#extension-function) on maps to convert all their values to a [`Cell{:tact}`][p] type. Be mindful, that [`Cell{:tact}`][p] type is able to store up to 1023 bits, so converting larger maps to the Cell will result in error. @@ -153,7 +153,7 @@ contract Example { } ``` -### Traverse over entries +### Traverse over entries [#traverse] At the moment Tact doesn't have a special syntax for iterating over maps. However, it's possible to use maps as a simple arrays if you define a `map{:tact}` with an [`Int{:tact}`][int] type for the keys and keep track of the number of items in the separate variable: @@ -174,6 +174,8 @@ contract MapAsArray { // Internal function for popping the last item of the «array» and returning its value fun arrPop(): Int { + require(self.arrLength > 0, "No items in the array to delete!"); + let lastItem: Int = self.arr.get(self.arrLength - 1)!!; self.arr.set(self.arrLength - 1, null); self.arrLength -= 1; @@ -181,18 +183,25 @@ contract MapAsArray { return lastItem; } - // Internal message receiver, which responds to a String message "item" - receive("item") { - // add a new item + increase the counter + // Internal message receiver, which responds to a String message "push" + receive("push") { + // Add a new item and increase the counter self.arrPush(42); } + // Internal message receiver, which responds to a String message "pop" + receive("pop") { + // Remove the last item and decrease the counter + self.arrPop(); + } + // Internal message receiver, which responds to a String message "get_last" receive("get_last") { // Reply back with the latest item in the map if it exists if (self.arrLength > 0) { // Note, that we use !! operator as we know for sure that the value would be there - self.reply((self.arr.get(self.arrLength - 1)!!).asComment()); + let last: Int = self.arr.get(self.arrLength - 1)!!; + self.reply(last.toCoinsString().asComment()); } } @@ -208,12 +217,18 @@ contract MapAsArray { } ``` -Additionally, it's useful to set an upper-bound restriction on such maps, so that you [don't hit the limits](#limits-and-drawbacks). +It's often useful to set an upper-bound restriction on such maps, so that you [don't hit the limits](#limits-and-drawbacks). + + + + Note, that manually keeping track of number of items or checking the length of such map is very error-prone and generally discouraged. Instead, try to wrap your map into the [Struct](/book/structs-and-messages#structs) and define [extension functions](/book/functions#extension-function) on it. See example in the Cookbook: [How to traverse a map wrapped in a Struct](/cookbook#how-to-traverse-a-map-wrapped-in-a-struct). + + See other examples of traversing maps in the Cookbook:\ - [How to iterate over map entries](/cookbook#how-to-iterate-over-map-entries)\ + [How to emulate an array using a map and iterate over its entries](/cookbook#how-to-emulate-an-array-using-a-map-and-iterate-over-its-entries)\ [How to make a cyclic array with a map](/cookbook#how-to-make-a-cyclic-array-with-a-map) diff --git a/pages/cookbook/index.mdx b/pages/cookbook/index.mdx index 4d511acc..cf38b2c9 100644 --- a/pages/cookbook/index.mdx +++ b/pages/cookbook/index.mdx @@ -116,7 +116,7 @@ dump("The loop is over!"); For description and basic usage see [`map{:tact}` type in the Book](/book/maps). -### How to iterate over map entries +### How to emulate an array using a map and iterate over its entries ```tact import "@stdlib/deploy"; // for Deployable trait @@ -175,13 +175,88 @@ contract Iteration with Deployable { +### How to traverse a map wrapped in a Struct + +```tact +import "@stdlib/deploy"; + +struct Array { + map: map; // «array» of Int values as a map of Ints to Ints + length: Int = 0; // length of the «array», defaults to 0 +} + +// Extension mutation function for adding new entries +extends mutates fun push(self: Array, item: Int) { + self.map.set(self.length, item); // set the entry (key-value pair) + self.length = self.length + 1; // increase the length field +} + +// Extension mutation function for deleting the last entry and returning its value +extends mutates fun pop(self: Array): Int { + require(self.length > 0, "No items in the array to delete!"); + + // Note, that we use !! operator as we know for sure that the value would be there + let lastItem: Int = self.map.get(self.length - 1)!!; + self.map.set(self.length - 1, null); // delete the entry + self.length = self.length - 1; // decrease the length field + + return lastItem; +} + +// Extension function for returning the last value +extends fun getLast(self: Array): Int { + require(self.length > 0, "No items in the array!"); + + // Note, that we use !! operator as we know for sure that the value would be there + return self.map.get(self.length - 1)!!; +} + +contract MapsAsArrays with Deployable { + // Persistent state variables + array: Array; // our «array», which uses the map + + // Constructor (initialization) function of the contract + init() { + self.array = Array{map: emptyMap()}; // length defaults to 0 + } + + // Internal message receiver, which responds to a String message "push" + receive("push") { + // Add a new item and increase the counter + self.array.push(42); + } + + // Internal message receiver, which responds to a String message "pop" + receive("pop") { + // Remove the last item and decrease the counter + self.array.pop(); + } + + // Internal message receiver, which responds to a String message "get_last" + receive("get_last") { + // Reply back with the latest item in the map if it exists, or raise an error + self.reply(self.array.getLast().toCoinsString().asComment()); + } + + // Getter function for obtaining the «array» + get fun map(): map { + return self.array.map; + } + + // Getter function for obtaining the current length of the «array» + get fun length(): Int { + return self.array.length; + } +} +``` + ### How to make a circular buffer with a map ```tact import "@stdlib/deploy"; // for Deployable trait // This contract records the last 5 timestamps of when "timer" message was received -contract Arrays with Deployable { +contract CircularBuffer with Deployable { // Declare a compile-time constant upper bound for our map representing a buffer const MaxBufferSize: Int = 5; @@ -195,20 +270,20 @@ contract Arrays with Deployable { // Internal function for adding a new item into the buffer fun bufferPush(item: Int) { - if (self.arrLength < self.MaxBufferSize) { - self.arr.set(self.arrLength, item); - self.arrLength += 1; + if (self.bufferLength < self.MaxBufferSize) { + self.buffer.set(self.bufferLength, item); + self.bufferLength += 1; } else { - self.arr.set(self.arrStart, item); - self.arrStart = (self.arrStart + 1) % self.MaxBufferSize; + self.buffer.set(self.bufferStart, item); + self.bufferStart = (self.bufferStart + 1) % self.MaxBufferSize; } } - // Iterate over all items in the array and dump them - fun arrPrint() { - let i: Int = self.arrStart; - repeat (self.arrLength) { - dump(self.arr.get(i)!!); // !! tells the compiler this can't be null + // Iterate over all items in the buffer and dump them + fun bufferPrint() { + let i: Int = self.bufferStart; + repeat (self.bufferLength) { + dump(self.buffer.get(i)!!); // !! tells the compiler this can't be null i = (i + 1) % self.MaxBufferSize; } } @@ -217,19 +292,20 @@ contract Arrays with Deployable { // and records the timestamp when it receives such message receive("timer") { let timestamp: Int = now(); - self.arrPush(timestamp); + self.bufferPush(timestamp); } + // Internal message receiver, which responds to a String message "dump" receive("dump") { - self.arrPrint(); + self.bufferPrint(); } get fun length(): Int { - return self.arrLength; + return self.bufferLength; } get fun map(): map { - return self.arr; + return self.buffer; } } ``` From ebe4f2ce1232c6465a962578aba2bce9018d9e3e Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:46:14 +0200 Subject: [PATCH 06/10] fix: Swap examples for clarity --- pages/book/maps.mdx | 81 +++++++++++++++++--------------------- pages/cookbook/index.mdx | 85 +++++++++++++++++++++++----------------- 2 files changed, 84 insertions(+), 82 deletions(-) diff --git a/pages/book/maps.mdx b/pages/book/maps.mdx index 668bbe6e..592d2bc3 100644 --- a/pages/book/maps.mdx +++ b/pages/book/maps.mdx @@ -158,61 +158,50 @@ contract Example { At the moment Tact doesn't have a special syntax for iterating over maps. However, it's possible to use maps as a simple arrays if you define a `map{:tact}` with an [`Int{:tact}`][int] type for the keys and keep track of the number of items in the separate variable: ```tact -contract MapAsArray { +contract Iteration { // Persistent state variables - arr: map; // «array» of Int values as a map - arrLength: Int = 0; // length of the «array», defaults to 0 + counter: Int as uint32; // counter of map entries, serialized as a 32-bit unsigned + record: map; // Int to Address map // Constructor (initialization) function of the contract - init() {} - - // Internal function for pushing an item to the end of the «array» - fun arrPush(item: Int) { - self.arr.set(self.arrLength, item); - self.arrLength += 1; - } - - // Internal function for popping the last item of the «array» and returning its value - fun arrPop(): Int { - require(self.arrLength > 0, "No items in the array to delete!"); - - let lastItem: Int = self.arr.get(self.arrLength - 1)!!; - self.arr.set(self.arrLength - 1, null); - self.arrLength -= 1; - - return lastItem; - } - - // Internal message receiver, which responds to a String message "push" - receive("push") { - // Add a new item and increase the counter - self.arrPush(42); + init() { + self.counter = 0; // Setting the self.counter to 0 } - // Internal message receiver, which responds to a String message "pop" - receive("pop") { - // Remove the last item and decrease the counter - self.arrPop(); + // Internal message receiver, which responds to a String message "Add" + receive("Add") { + // Get the Context Struct + let ctx: Context = context(); + // Set the entry: counter Int as a key, ctx.sender Address as a value + self.record.set(self.counter, ctx.sender); + // Increase the counter + self.counter += 1; } - // Internal message receiver, which responds to a String message "get_last" - receive("get_last") { - // Reply back with the latest item in the map if it exists - if (self.arrLength > 0) { - // Note, that we use !! operator as we know for sure that the value would be there - let last: Int = self.arr.get(self.arrLength - 1)!!; - self.reply(last.toCoinsString().asComment()); + // Internal message receiver, which responds to a String message "Send" + receive("Send") { + // Loop until the value of self.counter (over all the self.record entries) + let i: Int = 0; // declare usual i for loop iterations + while (i < self.counter) { + send(SendParameters{ + bounce: false, // do not bounce back this message + to: self.record.get(i)!!, // set the sender address, knowing that key i exists in the map + value: ton("0.0000001"), // 100 nanoToncoins (nano-tons) + mode: SendIgnoreErrors, // send ignoring errors in transaction, if any + body: "SENDING".asComment() // String "SENDING" converted to a Cell as a message body + }); + i += 1; // don't forget to increase the i } } - // Getter function for obtaining the «array» - get fun map(): map { - return self.arr; + // Getter function for obtaining the value of self.record + get fun map(): map { + return self.record; } - // Getter function for obtaining the current length of the «array» - get fun length(): Int { - return self.arrLength; + // Getter function for obtaining the value of self.counter + get fun counter(): Int { + return self.counter; } } ``` @@ -221,14 +210,16 @@ It's often useful to set an upper-bound restriction on such maps, so that you [d - Note, that manually keeping track of number of items or checking the length of such map is very error-prone and generally discouraged. Instead, try to wrap your map into the [Struct](/book/structs-and-messages#structs) and define [extension functions](/book/functions#extension-function) on it. See example in the Cookbook: [How to traverse a map wrapped in a Struct](/cookbook#how-to-traverse-a-map-wrapped-in-a-struct). + Note, that manually keeping track of number of items or checking the length of such map is very error-prone and generally discouraged. Instead, try to wrap your map into the [Struct](/book/structs-and-messages#structs) and define [extension functions](/book/functions#extension-function) on it. See example in the Cookbook: [How to emulate an array using a map wrapped in a Struct](/cookbook#how-to-emulate-an-array-using-a-map-wrapped-in-a-struct). + This example was adapted from [howardpen9/while-example-tact](https://github.com/howardpen9/while-example-tact/blob/de5807fcd20dba5f6a3748d112511477fb22bfcc/contracts/awesome.tact#L19C10-L19C10). + See other examples of traversing maps in the Cookbook:\ - [How to emulate an array using a map and iterate over its entries](/cookbook#how-to-emulate-an-array-using-a-map-and-iterate-over-its-entries)\ + [How to emulate an array using a map](/cookbook#how_to_emulate_an_array_using_a_map)\ [How to make a cyclic array with a map](/cookbook#how-to-make-a-cyclic-array-with-a-map) diff --git a/pages/cookbook/index.mdx b/pages/cookbook/index.mdx index 2bc5472a..8c906fd8 100644 --- a/pages/cookbook/index.mdx +++ b/pages/cookbook/index.mdx @@ -116,69 +116,80 @@ dump("The loop is over!"); For description and basic usage see [`map{:tact}` type in the Book](/book/maps). -### How to emulate an array using a map and iterate over its entries +### How to emulate an array using a map ```tact import "@stdlib/deploy"; // for Deployable trait -contract Iteration with Deployable { +contract MapAsArray with Deployable { // Persistent state variables - counter: Int as uint32; // counter of map entries, serialized as a 32-bit unsigned - record: map; // Int to Address map + arr: map; // «array» of Int values as a map + arrLength: Int = 0; // length of the «array», defaults to 0 // Constructor (initialization) function of the contract - init() { - self.counter = 0; // Setting the self.counter to 0 + init() {} + + // Internal function for pushing an item to the end of the «array» + fun arrPush(item: Int) { + self.arr.set(self.arrLength, item); + self.arrLength += 1; + } + + // Internal function for popping the last item of the «array» and returning its value + fun arrPop(): Int { + require(self.arrLength > 0, "No items in the array to delete!"); + + let lastItem: Int = self.arr.get(self.arrLength - 1)!!; + self.arr.set(self.arrLength - 1, null); + self.arrLength -= 1; + + return lastItem; + } + + // Internal message receiver, which responds to a String message "push" + receive("push") { + // Add a new item and increase the counter + self.arrPush(42); } - // Internal message receiver, which responds to a String message "Add" - receive("Add") { - // Get the Context Struct - let ctx: Context = context(); - // Set the entry: counter Int as a key, ctx.sender Address as a value - self.record.set(self.counter, ctx.sender); - // Increase the counter - self.counter += 1; + // Internal message receiver, which responds to a String message "pop" + receive("pop") { + // Remove the last item and decrease the counter + self.arrPop(); } - // Internal message receiver, which responds to a String message "Send" - receive("Send") { - // Loop until the value of self.counter (over all the self.record entries) - let i: Int = 0; // declare usual i for loop iterations - while (i < self.counter) { - send(SendParameters{ - bounce: false, // do not bounce back this message - to: self.record.get(i)!!, // set the sender address, knowing that key i exists in the map - value: ton("0.0000001"), // 100 nanoToncoins (nano-tons) - mode: SendIgnoreErrors, // send ignoring errors in transaction, if any - body: "SENDING".asComment() // String "SENDING" converted to a Cell as a message body - }); - i += 1; // don't forget to increase the i + // Internal message receiver, which responds to a String message "get_last" + receive("get_last") { + // Reply back with the latest item in the map if it exists + if (self.arrLength > 0) { + // Note, that we use !! operator as we know for sure that the value would be there + let last: Int = self.arr.get(self.arrLength - 1)!!; + self.reply(last.toCoinsString().asComment()); } } - // Getter function for obtaining the value of self.record - get fun map(): map { - return self.record; + // Getter function for obtaining the «array» + get fun map(): map { + return self.arr; } - // Getter function for obtaining the value of self.counter - get fun counter(): Int { - return self.counter; + // Getter function for obtaining the current length of the «array» + get fun length(): Int { + return self.arrLength; } } ``` - + - This example is adapted from [howardpen9/while-example-tact](https://github.com/howardpen9/while-example-tact/blob/de5807fcd20dba5f6a3748d112511477fb22bfcc/contracts/awesome.tact#L19C10-L19C10). + Note, that manually keeping track of number of items or checking the length of such map is very error-prone and generally discouraged. Instead, try to wrap your map into the [Struct](/book/structs-and-messages#structs) and define [extension functions](/book/functions#extension-function) on it. See the [next example showcasing such approach](#how-to-emulate-an-array-using-a-map-wrapped-in-a-struct). -### How to traverse a map wrapped in a Struct +### How to emulate an array using a map wrapped in a Struct ```tact -import "@stdlib/deploy"; +import "@stdlib/deploy"; // for Deployable trait struct Array { map: map; // «array» of Int values as a map of Ints to Ints From 3bd16c40cec51d2b24ab9b6719d682d9908f7313 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:46:38 +0200 Subject: [PATCH 07/10] chore: Suggestion of corepack to pin down version of the package manager --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 90989567..69e89225 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,6 @@ "@types/react": "18.2.61", "typescript": "^4.9.5" }, - "license": "CC-BY-4.0" -} \ No newline at end of file + "license": "CC-BY-4.0", + "packageManager": "yarn@1.22.22" +} From cbf94160d81816b85a742c1b72ecce0c3670a26a Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:10:46 +0200 Subject: [PATCH 08/10] chore: modernize syntax (+-, -=) --- pages/book/maps.mdx | 2 +- pages/cookbook/index.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/book/maps.mdx b/pages/book/maps.mdx index 592d2bc3..791053c8 100644 --- a/pages/book/maps.mdx +++ b/pages/book/maps.mdx @@ -218,7 +218,7 @@ It's often useful to set an upper-bound restriction on such maps, so that you [d This example was adapted from [howardpen9/while-example-tact](https://github.com/howardpen9/while-example-tact/blob/de5807fcd20dba5f6a3748d112511477fb22bfcc/contracts/awesome.tact#L19C10-L19C10). - See other examples of traversing maps in the Cookbook:\ + See other examples of map usage in the Cookbook:\ [How to emulate an array using a map](/cookbook#how_to_emulate_an_array_using_a_map)\ [How to make a cyclic array with a map](/cookbook#how-to-make-a-cyclic-array-with-a-map) diff --git a/pages/cookbook/index.mdx b/pages/cookbook/index.mdx index 8c906fd8..20379324 100644 --- a/pages/cookbook/index.mdx +++ b/pages/cookbook/index.mdx @@ -199,7 +199,7 @@ struct Array { // Extension mutation function for adding new entries extends mutates fun push(self: Array, item: Int) { self.map.set(self.length, item); // set the entry (key-value pair) - self.length = self.length + 1; // increase the length field + self.length += 1; // increase the length field } // Extension mutation function for deleting the last entry and returning its value @@ -209,7 +209,7 @@ extends mutates fun pop(self: Array): Int { // Note, that we use !! operator as we know for sure that the value would be there let lastItem: Int = self.map.get(self.length - 1)!!; self.map.set(self.length - 1, null); // delete the entry - self.length = self.length - 1; // decrease the length field + self.length -= 1; // decrease the length field return lastItem; } From b6383dc0abfc31d7ef5d97ff4e10b510f0afafcb Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:12:22 +0200 Subject: [PATCH 09/10] fix: Add info about no penalty for using emptyMap() --- pages/book/maps.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pages/book/maps.mdx b/pages/book/maps.mdx index 791053c8..2f32e3cd 100644 --- a/pages/book/maps.mdx +++ b/pages/book/maps.mdx @@ -124,6 +124,8 @@ fizz = emptyMap(); fizz = null; // identical to the previous line, but less descriptive ``` +With this approach all previous entries of the map are completely discarded from the contract even if the map was declared as its persistent state variable. As a result, assigning maps to `emptyMap(){:tact}` **does not** inflict any hidden or sudden [storage fees](https://docs.ton.org/develop/smart-contracts/fees#storage-fee). + ### Convert to a `Cell`, `.asCell()` [#convert] Use `.asCell(){:tact}` [method](/book/functions#extension-function) on maps to convert all their values to a [`Cell{:tact}`][p] type. Be mindful, that [`Cell{:tact}`][p] type is able to store up to 1023 bits, so converting larger maps to the Cell will result in error. @@ -219,8 +221,8 @@ It's often useful to set an upper-bound restriction on such maps, so that you [d This example was adapted from [howardpen9/while-example-tact](https://github.com/howardpen9/while-example-tact/blob/de5807fcd20dba5f6a3748d112511477fb22bfcc/contracts/awesome.tact#L19C10-L19C10). See other examples of map usage in the Cookbook:\ - [How to emulate an array using a map](/cookbook#how_to_emulate_an_array_using_a_map)\ - [How to make a cyclic array with a map](/cookbook#how-to-make-a-cyclic-array-with-a-map) + [How to emulate a stack using a map wrapped in a Struct](/cookbook#how-to-emulate-a-circular-buffer-using-a-map-wrapped-in-a-struct)\ + [How to emulate a circular buffer using a map wrapped in a Struct](/cookbook#how-to-emulate-a-circular-buffer-using-a-map-wrapped-in-a-struct) From 6400e27d94cdcf143eefd74bc5c1dc5d52fde567 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:12:51 +0200 Subject: [PATCH 10/10] feat: Maps as Array, Stack, CircularBuffer :rocket: --- pages/cookbook/index.mdx | 335 +++++++++++++++++++++++++++------------ 1 file changed, 234 insertions(+), 101 deletions(-) diff --git a/pages/cookbook/index.mdx b/pages/cookbook/index.mdx index 20379324..205dfda5 100644 --- a/pages/cookbook/index.mdx +++ b/pages/cookbook/index.mdx @@ -116,95 +116,175 @@ dump("The loop is over!"); For description and basic usage see [`map{:tact}` type in the Book](/book/maps). -### How to emulate an array using a map +### How to emulate an array using a map wrapped in a Struct ```tact import "@stdlib/deploy"; // for Deployable trait -contract MapAsArray with Deployable { - // Persistent state variables - arr: map; // «array» of Int values as a map - arrLength: Int = 0; // length of the «array», defaults to 0 +struct Array { + map: map; // array of Int values as a map of Ints to Ints + length: Int = 0; // length of the array, defaults to 0 +} - // Constructor (initialization) function of the contract - init() {} +// Compile-time constant upper bound for our map representing an array. +const MaxArraySize: Int = 5_000; // 5,000 entries max, to stay reasonably far from limits - // Internal function for pushing an item to the end of the «array» - fun arrPush(item: Int) { - self.arr.set(self.arrLength, item); - self.arrLength += 1; +// Extension mutation function for adding new entries to the end of the array +extends mutates fun append(self: Array, item: Int) { + require(self.length + 1 < MaxArraySize, "No space in the array left for new items!"); + + self.map.set(self.length, item); // set the entry (key-value pair) + self.length += 1; // increase the length field +} + +// Extension mutation function for inserting new entries at the given index +extends mutates fun insert(self: Array, item: Int, idx: Int) { + require(self.length + 1 < MaxArraySize, "No space in the array left for new items!"); + require(idx >= 0, "Index of the item cannot be negative!"); + require(idx < self.length, "Index is out of array bounds!"); + + // Move all items from idx to the right + let i: Int = self.length; // not a typo, as we need to start from the non-existing place + while (i > idx) { + // Note, that we use !! operator as we know for sure that the value would be there + self.map.set(i, self.map.get(i - 1)!!); + i -= 1; } - // Internal function for popping the last item of the «array» and returning its value - fun arrPop(): Int { - require(self.arrLength > 0, "No items in the array to delete!"); + // And put the new item in + self.map.set(idx, item); // set the entry (key-value pair) + self.length += 1; // increase the length field +} + +// Extension function for getting the value at the given index +extends fun getIdx(self: Array, idx: Int): Int { + require(self.length > 0, "No items in the array!"); + require(idx >= 0, "Index of the item cannot be negative!"); + require(idx < self.length, "Index is out of array bounds!"); - let lastItem: Int = self.arr.get(self.arrLength - 1)!!; - self.arr.set(self.arrLength - 1, null); - self.arrLength -= 1; + // Note, that we use !! operator as we know for sure that the value would be there + return self.map.get(idx)!!; +} - return lastItem; +// Extension function for returning the last value +extends fun getLast(self: Array): Int { + require(self.length > 0, "No items in the array!"); + + // Note, that we use !! operator as we know for sure that the value would be there + return self.map.get(self.length - 1)!!; +} + +// Extension mutation function for deleting and entry at the given index and returning its value +extends mutates fun deleteIdx(self: Array, idx: Int): Int { + require(self.length > 0, "No items in the array to delete!"); + require(idx >= 0, "Index of the item cannot be negative!"); + require(idx < self.length, "Index is out of array bounds!"); + + // Remember the value, which is going to be deleted + let memorized: Int = self.map.get(idx)!!; + + // Move all items from idx and including to the left + let i: Int = idx; + while (i + 1 < self.length) { + // Note, that we use !! operator as we know for sure that the value would be there + self.map.set(i, self.map.get(i + 1)!!); + i += 1; } - // Internal message receiver, which responds to a String message "push" - receive("push") { - // Add a new item and increase the counter - self.arrPush(42); + self.map.set(self.length - 1, null); // delete the last entry + self.length -= 1; // decrease the length field + + return memorized; +} + +// Extension mutation function for deleting the last entry and returning its value +extends fun deleteLast(self: Array): Int { + require(self.length > 0, "No items in the array!"); + + // Note, that we use !! operator as we know for sure that the value would be there + let lastItem: Int = self.map.get(self.length - 1)!!; + self.map.set(self.length - 1, null); // delete the entry + self.length -= 1; // decrease the length field + + return lastItem; +} + +// Extension function for deleting all items in the Array +extends mutates fun deleteAll(self: Array) { + self.map = emptyMap(); + self.length = 0; +} + +// Global static function for creating an empty Array +fun emptyArray(): Array { + return Array{map: emptyMap()}; // length defaults to 0 +} + +// Contract, with emulating an Array using the map +contract MapAsArray with Deployable { + // Persistent state variables + array: Array; + + // Constructor (initialization) function of the contract + init() { + self.array = emptyArray(); } - // Internal message receiver, which responds to a String message "pop" - receive("pop") { - // Remove the last item and decrease the counter - self.arrPop(); + // Internal message receiver, which responds to a String message "append" + receive("append") { + // Add a new item + self.array.append(42); } - // Internal message receiver, which responds to a String message "get_last" - receive("get_last") { - // Reply back with the latest item in the map if it exists - if (self.arrLength > 0) { - // Note, that we use !! operator as we know for sure that the value would be there - let last: Int = self.arr.get(self.arrLength - 1)!!; - self.reply(last.toCoinsString().asComment()); - } + // Internal message receiver, which responds to a String message "delete_5h" + receive("delete_5th") { + // Remove the 5th item if it exists and reply back with its value, or raise an erroor + self.reply(self.array.deleteIdx(4).toCoinsString().asComment()); // index offset 0 + 4 gives the 5th item } - // Getter function for obtaining the «array» - get fun map(): map { - return self.arr; + // Internal message receiver, which responds to a String message "del_last" + receive("del_last") { + // Remove the last item and reply back with its value, or raise an error + self.reply(self.array.deleteLast().toCoinsString().asComment()); } - // Getter function for obtaining the current length of the «array» - get fun length(): Int { - return self.arrLength; + // Internal message receiver, which responds to a String message "get_last" + receive("get_last") { + // Reply back with the latest item in the array if it exists, or raise an error + self.reply(self.array.getLast().toCoinsString().asComment()); + } + + // Internal message receiver, which responds to a String message "delete_all" + receive("delete_all") { + self.array.deleteAll(); } } ``` - +### How to emulate a stack using a map wrapped in a Struct - Note, that manually keeping track of number of items or checking the length of such map is very error-prone and generally discouraged. Instead, try to wrap your map into the [Struct](/book/structs-and-messages#structs) and define [extension functions](/book/functions#extension-function) on it. See the [next example showcasing such approach](#how-to-emulate-an-array-using-a-map-wrapped-in-a-struct). +A [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) is a collection of elements with two main operations: - - -### How to emulate an array using a map wrapped in a Struct +* push, which adds an element to the end of the collection +* pop, which removes the most recently added element ```tact import "@stdlib/deploy"; // for Deployable trait -struct Array { - map: map; // «array» of Int values as a map of Ints to Ints - length: Int = 0; // length of the «array», defaults to 0 +struct Stack { + map: map; // stack of Int values as a map of Ints to Ints + length: Int = 0; // length of the stack, defaults to 0 } -// Extension mutation function for adding new entries -extends mutates fun push(self: Array, item: Int) { +// Extension mutation function for adding new entries to the end of the stack +extends mutates fun push(self: Stack, item: Int) { self.map.set(self.length, item); // set the entry (key-value pair) self.length += 1; // increase the length field } // Extension mutation function for deleting the last entry and returning its value -extends mutates fun pop(self: Array): Int { - require(self.length > 0, "No items in the array to delete!"); +extends mutates fun pop(self: Stack): Int { + require(self.length > 0, "No items in the stack to delete!"); // Note, that we use !! operator as we know for sure that the value would be there let lastItem: Int = self.map.get(self.length - 1)!!; @@ -215,108 +295,161 @@ extends mutates fun pop(self: Array): Int { } // Extension function for returning the last value -extends fun getLast(self: Array): Int { - require(self.length > 0, "No items in the array!"); +extends fun getLast(self: Stack): Int { + require(self.length > 0, "No items in the stack!"); // Note, that we use !! operator as we know for sure that the value would be there return self.map.get(self.length - 1)!!; } -contract MapsAsArrays with Deployable { +// Extension function for deleting all items in the Stack +extends mutates fun deleteAll(self: Stack) { + self.map = emptyMap(); + self.length = 0; +} + +// Global static function for creating an empty Stack +fun emptyStack(): Stack { + return Stack{map: emptyMap()}; // length defaults to 0 +} + +contract MapAsStack with Deployable { // Persistent state variables - array: Array; // our «array», which uses the map + stack: Stack; // our stack, which uses the map // Constructor (initialization) function of the contract init() { - self.array = Array{map: emptyMap()}; // length defaults to 0 + self.stack = emptyStack(); } // Internal message receiver, which responds to a String message "push" receive("push") { - // Add a new item and increase the counter - self.array.push(42); + // Add a new item + self.stack.push(42); } // Internal message receiver, which responds to a String message "pop" receive("pop") { - // Remove the last item and decrease the counter - self.array.pop(); + // Remove the last item and reply with it + self.reply(self.stack.pop().toCoinsString().asComment()); } // Internal message receiver, which responds to a String message "get_last" receive("get_last") { // Reply back with the latest item in the map if it exists, or raise an error - self.reply(self.array.getLast().toCoinsString().asComment()); + self.reply(self.stack.getLast().toCoinsString().asComment()); + } + + // Internal message receiver, which responds to a String message "delete_all" + receive("delete_all") { + self.stack.deleteAll(); } - // Getter function for obtaining the «array» + // Getter function for obtaining the stack get fun map(): map { - return self.array.map; + return self.stack.map; } - // Getter function for obtaining the current length of the «array» + // Getter function for obtaining the current length of the stack get fun length(): Int { - return self.array.length; + return self.stack.length; } } ``` -### How to make a circular buffer with a map +### How to emulate a circular buffer using a map wrapped in a Struct + +A [circular buffer](https://en.wikipedia.org/wiki/Circular_buffer) (circular queue, cyclic buffer or ring buffer) is a data structure, which uses a single, fixed-size [buffer](https://en.wikipedia.org/wiki/Data_buffer) as it were connected end-to-end. ```tact import "@stdlib/deploy"; // for Deployable trait -// This contract records the last 5 timestamps of when "timer" message was received -contract CircularBuffer with Deployable { - // Declare a compile-time constant upper bound for our map representing a buffer - const MaxBufferSize: Int = 5; +struct CircularBuffer { + map: map; // circular buffer of Int values as a map of Ints to Ints + length: Int = 0; // length of the circular buffer, defaults to 0 + start: Int = 0; // current index into the circular buffer, defaults to 0 +} - // Persistent state variables - buffer: map; // this is our buffer implemented with a map - bufferLength: Int as uint8 = 0; - bufferStart: Int as uint8 = 0; // current index into the buffer +// Compile-time constant upper bound for our map representing a circular buffer. +const MaxCircularBufferSize: Int = 5; + +// Extension mutation function for putting new items to the circular buffer +extends mutates fun put(self: CircularBuffer, item: Int) { + if (self.length < MaxCircularBufferSize) { + self.map.set(self.length, item); // store the item + self.length += 1; // increase the length field + } else { + self.map.set(self.start, item); // store the item, overriding previous entry + self.start = (self.start + 1) % MaxCircularBufferSize; // update starting position + } +} - // Constructor (initialization) function of the contract - init() {} +// Extension mutation function for getting an item from the circular buffer +extends mutates fun getIdx(self: CircularBuffer, idx: Int): Int { + require(self.length > 0, "No items in the circular buffer!"); + require(idx >= 0, "Index of the item cannot be negative!"); - // Internal function for adding a new item into the buffer - fun bufferPush(item: Int) { - if (self.bufferLength < self.MaxBufferSize) { - self.buffer.set(self.bufferLength, item); - self.bufferLength += 1; - } else { - self.buffer.set(self.bufferStart, item); - self.bufferStart = (self.bufferStart + 1) % self.MaxBufferSize; - } + if (self.length < MaxCircularBufferSize) { + // Note, that we use !! operator as we know for sure that the value would be there + return self.map.get(idx % self.length)!!; } - // Iterate over all items in the buffer and dump them - fun bufferPrint() { - let i: Int = self.bufferStart; - repeat (self.bufferLength) { - dump(self.buffer.get(i)!!); // !! tells the compiler this can't be null - i = (i + 1) % self.MaxBufferSize; - } + // Return the value rotating around the circular buffer, also guaranteed to be there + return self.map.get((self.start + idx) % MaxCircularBufferSize)!!; +} + +// Extension function for iterating over all items in the circular buffer and dumping them to the console +extends fun printAll(self: CircularBuffer) { + let i: Int = self.start; + repeat (self.length) { + dump(self.map.get(i)!!); // !! tells the compiler this can't be null + i = (i + 1) % MaxCircularBufferSize; + } +} + +// Extension function for deleting all items in the CircularBuffer +extends mutates fun deleteAll(self: CircularBuffer) { + self.map = emptyMap(); + self.length = 0; + self.start = 0; +} + +// Global static function for creating an empty CircularBuffer +fun emptyCircularBuffer(): CircularBuffer { + return CircularBuffer{map: emptyMap()}; // length and start default to 0 +} + +// This contract records the last 5 timestamps of when "timer" message was received +contract MapAsCircularBuffer with Deployable { + // Persistent state variables + cBuf: CircularBuffer; // our circular buffer, which uses a map + + // Constructor (initialization) function of the contract + init() { + self.cBuf = emptyCircularBuffer(); } // Internal message receiver, which responds to a String message "timer" // and records the timestamp when it receives such message receive("timer") { let timestamp: Int = now(); - self.bufferPush(timestamp); + self.cBuf.put(timestamp); } - // Internal message receiver, which responds to a String message "dump" - receive("dump") { - self.bufferPrint(); + // Internal message receiver, which responds to a String message "get_first" + // and replies with the first item of the circular buffer + receive("get_first") { + self.reply(self.cBuf.getIdx(0).toCoinsString().asComment()); } - get fun length(): Int { - return self.bufferLength; + // Internal message receiver, which responds to a String message "print_all" + receive("print_all") { + self.cBuf.printAll(); } - get fun map(): map { - return self.buffer; + // Internal message receiver, which responds to a String message "delete_all" + receive("delete_all") { + self.cBuf.deleteAll(); } } ```