evcxr_repl is A Rust REPL (Read-Eval-Print loop) built to provide evaluation context for Interactive programming.
Interactive programming, in which expressions are entered and evaluated in real-time, can be a powerful tool for exploring a language and problem solving. This capability is most often associated with dynamically evaluated languages such as JavaScript and Python. Compiled languages such as Java and C++ can also be used interactively, but tooling tends to lag by many years. Rust is a newer compiled language for which interactive programming has recently emerged. This article discusses interactive programming with Rust courtesy of the google/evcxr - https://github.com/google/evcxr crate.
Generics are abstract stand-ins for concrete types or other properties. When we’re writing code, we can express the behavior of generics or how they relate to other generics without knowing what will be in their place when compiling and running the code.
Then you’ll learn how to use traits to define behavior in a generic way. You can combine traits with generic types to constrain a generic type to only those types that have a particular behavior, as opposed to just any type.
Finally, we’ll discuss lifetimes, a variety of generics that give the compiler information about how references relate to each other. Lifetimes allow us to borrow values in many situations while still enabling the compiler to check that the references are valid.
The second trait important to the smart pointer pattern is Drop, which lets you customize what happens when a value is about to go out of scope. You can provide an implementation for the Drop` trait on any type, and the code you specify can be used to release resources like files or network connections.
Specify the code to run when a value goes out of scope by implementing the Drop trait. The Drop trait requires you to implement one method named drop that takes a mutable reference to self. To see when Rust calls drop, let’s implement drop with println! statements for now.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
structCustomSmartPointer { data: String, }
implDropforCustomSmartPointer { fndrop(&mutself) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } }
When we run this program, we’ll see the following output:
1 2 3 4
$ cargo run CustomSmartPointers created. Dropping CustomSmartPointer with data `other stuff`! Dropping CustomSmartPointer with data `my stuff`!
Rust automatically called drop for us when our instances went out of scope, calling the code we specified. Variables are dropped in the reverse order of their creation, so d was dropped before c.
Dropping a Value Early with std::mem::drop
You might want to force the drop method that releases the lock so that other code in the same scope can acquire the lock. Rust doesn’t let you call the Drop trait’s drop method manually; instead you have to call the std::mem::drop function provided by the standard library if you want to force a value to be dropped before the end of its scope.
Rust doesn’t let us call drop of Drop trait explicitly because Rust would still automatically call drop on the value at the end of main. This would be a double free error because Rust would be trying to clean up the same value twice.
The std::mem::drop function is different from the drop method in the Drop trait. We call it by passing the value we want to force to be dropped early as an argument.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
structCustomSmartPointer { data: String, }
implDropforCustomSmartPointer { fndrop(&mutself) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } }
fnmain() { letc = CustomSmartPointer { data: String::from("some data"), }; println!("CustomSmartPointer created."); drop(c); println!("CustomSmartPointer dropped before the end of main."); }
Running this code will print the following:
1 2 3 4
$ cargo run CustomSmartPointer created. Dropping CustomSmartPointer with data `some data`! CustomSmartPointer dropped before the end of main.
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.
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:
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:
letguess: 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> { pubfnunwrap(self) -> T { matchself { 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`
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.
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.
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
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 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; }
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.
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