Cloud-oriented Life

Cloud Native Technology Improves Lives

17. Object Oriented Programming Features of Rust

Many competing definitions describe what OOP is. In this chapter, we’ll explore certain characteristics that are commonly considered object oriented and how those characteristics translate to idiomatic Rust.

Read more »

19. Advanced Features

19.3 Advanced Types

Using the Newtype Pattern for Type Safety and Abstraction

Creating Type Synonyms with Type Aliases

Rust provides the ability to declare a type alias to give an existing type another name. For this we use the type keyword.

The main use case for type Alias(synonyms) is to reduce repetition.

1
2
3
4
5
6
7
8
9
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}

fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
}

A type alias makes this code more manageable by reducing the repetition.

1
2
3
4
5
6
7
8
9
10
11
type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {
// --snip--
}

fn returns_long_type() -> Thunk {
// --snip--
}

Type aliases are also commonly used with the Result<T, E> type for reducing repetition.

The Result<..., Error> is repeated a lot. As such, std::io has this type alias declaration:

1
type Result<T> = std::result::Result<T, std::io::Error>;

Because this declaration is in the std::io module, we can use the fully qualified alias std::io::Result<T>—that is, a Result<T, E>with theEfilled in asstd::io::Error`. The Write trait function signatures end up looking like this:

1
2
3
4
5
6
7
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;

fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

! The Never Type that Never Returns

Rust has a special type named ! that’s known in type theory lingo as the empty type because it has no values. We prefer to call it the never type because it stands in the place of the return type when a function will never return. Here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn bar() -> ! {
// --snip--
}

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue, // Because ! can never have a value, Rust decides that the type of guess is u32.
};

impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}

print!("forever ");

loop {
print!("and ever ");
// break;
} // ! is the value of the expression.

// However, this wouldn’t be true if we included a break, because the loop would terminate when it got to the break.

Dynamically Sized Types and the Sized Trait

Rust needs to know how much memory to allocate for any value of a particular type, and all values of a type must use the same amount of memory.

1
2
3
4
5
// let s1: str = "Hello there!";   // expected `str`, found `&str`
// let s2: str = "How's it going?"; // expected `str`, found `&str`

let s1: &str = "Hello there!";
let s2: &str = "How's it going?";

If Rust allowed us to write this code, these two str values would need to take up the same amount of space. But they have different lengths: s1 needs 12 bytes of storage and s2 needs 15. This is why it’s not possible to create a variable holding a dynamically sized type.

So although a &T is a single value that stores the memory address of where the T is located, a &str is two values: the address of the str and its length. As such, we can know the size of a &str value at compile time: it’s twice the length of a usize. That is, we always know the size of a &str, no matter how long the string it refers to is. In general, this is the way in which dynamically sized types are used in Rust: they have an extra bit of metadata that stores the size of the dynamic information. The golden rule of dynamically sized types is that we must always put values of dynamically sized types behind a pointer of some kind.

References

[1] The Rust Programming Language - The Rust Programming Language - https://doc.rust-lang.org/book/title-page.html

[2] Advanced Features - The Rust Programming Language - https://doc.rust-lang.org/book/ch19-00-advanced-features.html

Basic Example

This is a basic example that sets the value of a variable and exposes it for other contracts to access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: GPL-3.0 // the source code is licensed under the GPL version 3.0
pragma solidity >=0.4.16 <0.9.0; // that the source code is written for Solidity version 0.4.16, or a newer version of the language up to, but not including version 0.9.0.

contract SimpleStorage { // contract name
// declares a state variable called storedData of type uint (unsigned integer of 256 bits)
uint storedData;

// set that can be used to modify the value of the variable
function set(uint x) public {
storedData = x;
}

// get that can be used to retrieve the value of the variable
function get() public view returns (uint) {
return storedData;
}
}

The first line Machine-readable license specifiers are important in a setting where publishing the source code is the default.

pragma - are common instructions for compilers about how to treat the source code (e.g. pragma once - https://en.wikipedia.org/wiki/Pragma_once).

A contract in the sense of Solidity is a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain.

In this example, the contract declares a state variable called storedData of type uint (unsigned integer of 256 bits), and defines the functions set and get that can be used to modify or retrieve the value of the variable.

Anyone could call set again with a different value and overwrite your number, and the number is still stored in the history of the blockchain. Later, you will see how you can impose access restrictions so that only you can alter the number.


All identifiers (contract names, function names and variable names) are restricted to the ASCII character set. It is possible to store UTF-8 encoded data in string variables.


Subcurrency Example

The contract allows only its creator to create new coins (different issuance schemes are possible). Anyone can send coins to each other

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Coin {
// The keyword "public" makes variables
// accessible from other contracts
address public minter;
mapping (address => uint) public balances;

// Events allow clients to react to specific
// contract changes you declare
event Sent(address from, address to, uint amount);

// Constructor code is only run when the contract
// is created
constructor() {
minter = msg.sender;
}

// Sends an amount of newly created coins to an address
// Can only be called by the contract creator
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
require(amount < 1e60);
balances[receiver] += amount;
}

// Sends an amount of existing coins
// from any caller to an address
function send(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "Insufficient balance.");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}

The line address public minter; declares a state variable of type address - https://docs.soliditylang.org/en/v0.8.3/types.html#address. The address type is a 160-bit value that does not allow any arithmetic operations. It is suitable for storing addresses of contracts, or a hash of the public half of a keypair belonging to external accounts - https://docs.soliditylang.org/en/v0.8.3/introduction-to-smart-contracts.html#accounts.

The keyword public automatically generates a function that allows you to access the current value of the state variable from outside of the contract. Without this keyword, other contracts have no way to access the variable. The code of the function generated by the compiler is equivalent to the following

1
function minter() external view returns (address) { return minter; }

Arrays

An array holds a specific number of elements, and it cannot grow or shrink. Different data types can be handled as elements in arrays such as Int, String, Boolean, and others.

An array is a fixed-length sequence of zero or more elements of a particular type. Because of their fixed length, arrays are rarely used directly in Go. Slices, which can grow and shrink, are much more versatile.

Read more »

Maps

Go provides another important data type named map which maps unique keys to values. A key is an object that you use to retrieve a value at a later date. Given a key and a value, you can store the value in a Map object. After the value is stored, you can retrieve it by using its key.

Create a map

1
2
3
4
5
6
7
8
9
10
var m map[string]int                // nil map of string-int pairs

m1 := make(map[string]float64) // Empty map of string-float64 pairs
m2 := make(map[string]float64, 100) // Preallocate room for 100 entries

m3 := map[string]float64{ // Map literal
"e": 2.71828,
"pi": 3.1416,
}
fmt.Println(len(m3)) // Size of map: 2
  • A map (or dictionary) is an unordered collection of key-value pairs, where each key is unique.

  • You create a new map with a make statement or a map literal.

  • The default zero value of a map is nil. A nil map is equivalent to an empty map except that elements can’t be added.

  • The len function returns the size of a map, which is the number of key-value pairs.


Warning: If you try to add an element to an uninitialized map you get the mysterious run-time error Assignment to entry in nil map.


Set, Get, Delete, Length keys/values,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
m := make(map[string]float64)

m["pi"] = 3.14 // Set a new key-value pair
m["pi"] = 3.1416 // Update value
fmt.Println(m) // Print map: "map[pi:3.1416]"

v := m["pi"] // Get value: v == 3.1416
v = m["pie"] // Not found: v == 0 (zero value)

_, found := m["pi"] // found == true
_, found = m["pie"] // found == false

if x, found := m["pi"]; found {
fmt.Println(x)
} // Prints "3.1416"

delete(m, "pi") // Delete a key-value pair
fmt.Println(m) // Print map: "map[]"

When you index a map you get two return values; the second one (which is optional) is a boolean that indicates if the key exists.

If the key doesn’t exist, the first value will be the default zero value.

For-each range loop

1
2
3
4
5
6
7
8
9
m := map[string]float64{
"pi": 3.1416,
"e": 2.71828,
}
fmt.Println(m) // "map[e:2.71828 pi:3.1416]"

for key, value := range m { // Order not specified
fmt.Println(key, value)
}

Iteration order is not specified and may vary from iteration to iteration.

  • If an entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced.

  • If an entry is created during iteration, that entry may or may not be produced during the iteration.

Starting with Go 1.12, the fmt package prints maps in key-sorted order to ease testing.

Performance and implementation

Maps are backed by hash tables.

Set, get and delete operations run in constant expected time. The time complexity for the add operation is amortized.

The comparison operators == and != must be defined for the key type.

References

Maps explained: create, add, get, delete · YourBasic Go - https://yourbasic.org/golang/maps-explained/

Go - Maps - Tutorialspoint - https://www.tutorialspoint.com/go/go_maps.htm

Go map - working with maps in Golang - https://zetcode.com/golang/map/

Slices

References

[1] Go Slices: usage and internals - The Go Blog - https://blog.golang.org/slices-intro

[3] Slice | A Tour of Go - https://tour.golang.org/moretypes/7

[2] Go by Example: Slices - https://gobyexample.com/slices

[1] The anatomy of Slices in Go. Slices are like Arrays but they can… | by Uday Hiwarale | RunGo | Medium - https://medium.com/rungo/the-anatomy-of-slices-in-go-6450e3bb2b94

[] Go - Slices - Tutorialspoint - https://www.tutorialspoint.com/go/go_slice.htm

[] Slices in Golang - GeeksforGeeks - https://www.geeksforgeeks.org/slices-in-golang/

[] Go slice - working with slices in Golang - https://zetcode.com/golang/slice/

[] Learning Go — Array, Slice, Map. In this article, we are going to see… | by Madhavan Nagarajan | Level Up Coding - https://levelup.gitconnected.com/learning-go-array-slice-map-934eed320b1c

SliceTricks · golang/go Wiki - https://github.com/golang/go/wiki/SliceTricks

[] Golang Slices Tutorial with examples - golangprograms.com - https://www.golangprograms.com/go-language/slices-in-golang-programming.html

[] Slices/arrays explained: create, index, slice, iterate · YourBasic Go - https://yourbasic.org/golang/slices-explained/

4. Composite Types

Composite types, the molecules created by combining the basic types in various ways. We’ll talk about four such types—arrays, slices, maps, and structs.

Arrays and structs are aggregate types; their values are concatenations of other values in memory. Arrays are homogeneous—their elements all have the same type—whereas structs are heterogeneous. Both arrays and structs are fixed size. In contrast, slices and maps are dynamic data structures that grow as values are added.

Read more »

7. Interfaces

Interface types express generalizations or abstractions about the behaviors of other types. By generalizing, interfaces let us write functions that are more flexible and adaptable because they are not tied to the details of one particular implementation.

Many object-oriented languages have some notion of interfaces, but what makes Go’s interfaces so distinctive is that they are satisfied implicitly. In other words, there’s no need to
declare all the interfaces that a given concrete type satisfies; simply possessing the necessary
methods is enough. This design lets you create new interfaces that are satisfied by existing
concrete types without changing the existing types, which is partic ularly useful for types
defined in packages that you don’t control.

Read more »

amazing_print or awesome_print

AmazingPrint is a Ruby library that pretty prints Ruby objects in full color exposing their internal structure with proper indentation. Rails ActiveRecord objects and usage within Rails templates are supported via included mixins.

Read more »
0%