When learning a new programming language, you can sometimes feel confused about new paradigms. For me it happened with cairo and its lack of for loops.
Indeed, as the memory is immutable in cairo, the usage of iterators is not possible. Only recursion remains.
To illustrate this article, let’s assume we have a grid structure (with a width and height) and that each cell can be accessed using its (x,y) coordinates.
We will use the OOP-like pattern described here
Let’s see how it looks:
# grid.cairo
%lang starknet
from starkware.cairo.common.alloc import alloc
struct Grid:
member width : felt
member height : felt
member cells : felt*
end
# External functions
namespace external:
func create(width : felt, height : felt) -> (grid : Grid):
let (cells) = alloc
return (grid=Grid(width, height, cells))
end
func get_cell_at{grid : Grid}(x : felt, y : felt) -> (cell : felt):
let (index) = internal.index_of(x, y)
return (cell=grid.cells[index])
end
func set_cell_at{grid : Grid}(x : felt, y : felt, cell : felt):
let (index) = internal.index_of(x, y)
assert grid.cells[index] = cell
return (cell=grid.cells[index])
end
end
# Internal functions
namespace internal:
func index_of{grid : Grid}(x : felt, y : felt) -> (index : felt):
return (index=grid)
end
end
Letting aside the grid filling for the moment, let’s focus on how to iterate over the grid in an external contract. In a naive implementation, it would look like this:
# main.cairo
%lang starknet
from src.grid import Grid, external as grid_access
func new_filled_grid(width : felt, height : felt, value : felt) -> (grid : Grid):
alloc_locals
let (local grid) = grid_access.create(width, height)
fill_grid_loop(grid, 0, 0, value)
return (grid=grid)
end
func fill_grid_loop(grid : Grid, x : felt, y : felt, value : felt):
if y == grid.height:
return ()
end
if x == grid.width:
return fill_grid_loop(grid, 0, y + 1, value)
end
with grid:
grid_access.set_cell_at(x, y, value)
end
return fill_grid_loop(grid, x + 1, y, value)
end
There are several drawbacks with this approach, but the main one is that the main contract knows too much about the internal structure of the grid.
The grid should be responsible of its own structure. It should know how to iterate through it. But there is no foreach -like function in cairo. So what can we do ?
Well, we can implement the Iterator design pattern.
This pattern requires to define:
An iterator structure to hold the progress
A start() function to return the first cell of the grid
A next() function to return the next cell of the grid
A done function to return true when the loop is over
Here is how it looks:
# grid.cairo
%lang starknet
from starkware.cairo.common.alloc import alloc
struct Grid:
member width : felt
member height : felt
member cells : felt*
end
struct Iterator:
member x : felt
member y : felt
end
# External functions
namespace external:
func create(width : felt, height : felt) -> (grid : Grid):
let (cells) = alloc()
return (grid=Grid(width, height, cells))
end
func get_current_cell{grid : Grid, iterator : Iterator}() -> (cell : felt):
let (index) = internal.index_of(iterator.x, iterator.y)
return (cell=grid.cells[index])
end
func set_current_cell{grid : Grid, iterator : Iterator}(cell : felt):
let (index) = internal.index_of(iterator.x, iterator.y)
assert grid.cells[index] = cell
return ()
end
func start{grid : Grid}() -> (iterator : Iterator):
return (iterator=Iterator(0, 0))
end
func next{grid : Grid, iterator : Iterator}() -> ():
if iterator.x == grid.width - 1:
let iterator = Iterator(0, iterator.y + 1)
return ()
end
let iterator = Iterator(iterator.x + 1, iterator.y)
return ()
end
func done{grid : Grid, iterator : Iterator}() -> (is_done : felt):
if iterator.y == grid.height:
return (is_done=1)
end
return (is_done=0)
end
end
# Internal functions
namespace internal:
func index_of{grid : Grid}(x : felt, y : felt) -> (index : felt):
return (index=y * grid.width + x)
end
end
And here is how to use it:
# main.cairo
%lang starknet
from src.grid import Grid, Iterator, external as grid_access
func new_filled_grid(width : felt, height : felt, value : felt) -> (grid : Grid):
alloc_locals
let (local grid) = grid_access.create(width, height)
with grid:
let (iterator) = grid_access.start()
with iterator:
fill_grid_loop(value)
end
end
return (grid=grid)
end
func fill_grid_loop{grid : Grid, iterator : Iterator}(value : felt):
let (is_done) = grid_access.done()
if is_done == 1:
return ()
end
grid_access.set_current_cell(value)
grid_access.next()
return fill_grid_loop(value)
end
Note: It is possible to improve this code by modifying the iterator struct to be only a
felt, as themaincontract does not need to know the structure of the iterator anymore !
Using this pattern, we could also implement easily other kind of operators like the reverse iterator. But I will let this as an exercise :-)
When learning a new programming language, you can sometimes feel confused about new paradigms. For me it happened with cairo and its lack of for loops.
Indeed, as the memory is immutable in cairo, the usage of iterators is not possible. Only recursion remains.
To illustrate this article, let’s assume we have a grid structure (with a width and height) and that each cell can be accessed using its (x,y) coordinates.
We will use the OOP-like pattern described here
Let’s see how it looks:
# grid.cairo
%lang starknet
from starkware.cairo.common.alloc import alloc
struct Grid:
member width : felt
member height : felt
member cells : felt*
end
# External functions
namespace external:
func create(width : felt, height : felt) -> (grid : Grid):
let (cells) = alloc
return (grid=Grid(width, height, cells))
end
func get_cell_at{grid : Grid}(x : felt, y : felt) -> (cell : felt):
let (index) = internal.index_of(x, y)
return (cell=grid.cells[index])
end
func set_cell_at{grid : Grid}(x : felt, y : felt, cell : felt):
let (index) = internal.index_of(x, y)
assert grid.cells[index] = cell
return (cell=grid.cells[index])
end
end
# Internal functions
namespace internal:
func index_of{grid : Grid}(x : felt, y : felt) -> (index : felt):
return (index=grid)
end
end
Letting aside the grid filling for the moment, let’s focus on how to iterate over the grid in an external contract. In a naive implementation, it would look like this:
# main.cairo
%lang starknet
from src.grid import Grid, external as grid_access
func new_filled_grid(width : felt, height : felt, value : felt) -> (grid : Grid):
alloc_locals
let (local grid) = grid_access.create(width, height)
fill_grid_loop(grid, 0, 0, value)
return (grid=grid)
end
func fill_grid_loop(grid : Grid, x : felt, y : felt, value : felt):
if y == grid.height:
return ()
end
if x == grid.width:
return fill_grid_loop(grid, 0, y + 1, value)
end
with grid:
grid_access.set_cell_at(x, y, value)
end
return fill_grid_loop(grid, x + 1, y, value)
end
There are several drawbacks with this approach, but the main one is that the main contract knows too much about the internal structure of the grid.
The grid should be responsible of its own structure. It should know how to iterate through it. But there is no foreach -like function in cairo. So what can we do ?
Well, we can implement the Iterator design pattern.
This pattern requires to define:
An iterator structure to hold the progress
A start() function to return the first cell of the grid
A next() function to return the next cell of the grid
A done function to return true when the loop is over
Here is how it looks:
# grid.cairo
%lang starknet
from starkware.cairo.common.alloc import alloc
struct Grid:
member width : felt
member height : felt
member cells : felt*
end
struct Iterator:
member x : felt
member y : felt
end
# External functions
namespace external:
func create(width : felt, height : felt) -> (grid : Grid):
let (cells) = alloc()
return (grid=Grid(width, height, cells))
end
func get_current_cell{grid : Grid, iterator : Iterator}() -> (cell : felt):
let (index) = internal.index_of(iterator.x, iterator.y)
return (cell=grid.cells[index])
end
func set_current_cell{grid : Grid, iterator : Iterator}(cell : felt):
let (index) = internal.index_of(iterator.x, iterator.y)
assert grid.cells[index] = cell
return ()
end
func start{grid : Grid}() -> (iterator : Iterator):
return (iterator=Iterator(0, 0))
end
func next{grid : Grid, iterator : Iterator}() -> ():
if iterator.x == grid.width - 1:
let iterator = Iterator(0, iterator.y + 1)
return ()
end
let iterator = Iterator(iterator.x + 1, iterator.y)
return ()
end
func done{grid : Grid, iterator : Iterator}() -> (is_done : felt):
if iterator.y == grid.height:
return (is_done=1)
end
return (is_done=0)
end
end
# Internal functions
namespace internal:
func index_of{grid : Grid}(x : felt, y : felt) -> (index : felt):
return (index=y * grid.width + x)
end
end
And here is how to use it:
# main.cairo
%lang starknet
from src.grid import Grid, Iterator, external as grid_access
func new_filled_grid(width : felt, height : felt, value : felt) -> (grid : Grid):
alloc_locals
let (local grid) = grid_access.create(width, height)
with grid:
let (iterator) = grid_access.start()
with iterator:
fill_grid_loop(value)
end
end
return (grid=grid)
end
func fill_grid_loop{grid : Grid, iterator : Iterator}(value : felt):
let (is_done) = grid_access.done()
if is_done == 1:
return ()
end
grid_access.set_current_cell(value)
grid_access.next()
return fill_grid_loop(value)
end
Note: It is possible to improve this code by modifying the iterator struct to be only a
felt, as themaincontract does not need to know the structure of the iterator anymore !
Using this pattern, we could also implement easily other kind of operators like the reverse iterator. But I will let this as an exercise :-)
A standard on Github labels for contribution
How to make your need for external contribution explicit and widely visible?At Only Dust we believe than in a close future most developers will get paid in crypto to contribute to open source projects, not as employees but as contributors.To help it happen there is a lot to do: create communities, involve companies, create some kind of on-chain platform for missions to be posted on. That’s a lot to do, too much for one sprint or even one year. So we decided to start small, to find quick wins ...
Demystifying StarkNet accounts
IntroductionWhen trying StarkNet for the first time, it’s easy to go through the account setup without paying much attention to it. The documentation just says:Unlike Ethereum, which distinguishes between Externally Owned Accounts (EOA) and contracts, StarkNet doesn’t have this distinction. Instead, an account is represented by a deployed contract that defines the account’s logic — most notably the signature scheme that controls who can issue transactions from it.Then, you are invited to chos...
SOLID cairo - OOP-like pattern
When I learn a new language, my first focus is to build SOLID components while leveraging the feature of this new language. In this article, I would like to present a new convention we at OnlyDust are adopting with regards to cairo development. This new kind of architecture feels like object-oriented programming in a way and leverage several cairo features like:implicit parametersstructsnamespacesFollowing OpenZeppelin’s recommendations about extensibility, we aims at building nice modules th...
A standard on Github labels for contribution
How to make your need for external contribution explicit and widely visible?At Only Dust we believe than in a close future most developers will get paid in crypto to contribute to open source projects, not as employees but as contributors.To help it happen there is a lot to do: create communities, involve companies, create some kind of on-chain platform for missions to be posted on. That’s a lot to do, too much for one sprint or even one year. So we decided to start small, to find quick wins ...
Demystifying StarkNet accounts
IntroductionWhen trying StarkNet for the first time, it’s easy to go through the account setup without paying much attention to it. The documentation just says:Unlike Ethereum, which distinguishes between Externally Owned Accounts (EOA) and contracts, StarkNet doesn’t have this distinction. Instead, an account is represented by a deployed contract that defines the account’s logic — most notably the signature scheme that controls who can issue transactions from it.Then, you are invited to chos...
SOLID cairo - OOP-like pattern
When I learn a new language, my first focus is to build SOLID components while leveraging the feature of this new language. In this article, I would like to present a new convention we at OnlyDust are adopting with regards to cairo development. This new kind of architecture feels like object-oriented programming in a way and leverage several cairo features like:implicit parametersstructsnamespacesFollowing OpenZeppelin’s recommendations about extensibility, we aims at building nice modules th...
We onboard devs to the Starknet ecosystem. Our discord : https://discord.gg/onlydust To create Stars, you need Only Dust.
We onboard devs to the Starknet ecosystem. Our discord : https://discord.gg/onlydust To create Stars, you need Only Dust.
Share Dialog
Share Dialog
Subscribe to Only Dust
Subscribe to Only Dust
<100 subscribers
<100 subscribers
No activity yet