Rust for Javascript and Golang Developers
1. Introduction
- Aim of Rustkid is to explain rust concepts to javascript and golang developers.
- And also documenting my understanding of rust.
2. Hello World
rust
fn main() {
let k = "World!";
println!("Hello, {}", k);
println!("Hello, {k}");
}
/* Output
Hello, World!
Hello, World!
*/
javascript
let k = "World!";
console.log("Hello", k);
console.log(`Hello ${k}`);
/* Output
Hello World!
Hello World!
*/
golang
package main
import (
"fmt"
)
func main() {
k := "World!"
println("Hello", k)
fmt.Printf("Hello %s\n", k)
}
/* Output
Hello World!
Hello World!
*/
println!
- println! is a macro. It is not a normal fuction.
- Macro is identified by suffix '!' at the end of its name.
- It will be converted to a normal function during compilation.
If you want to know, how does the converted function look like? expand this line.
rust
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
let k = "World!";
{
::std::io::_print(::core::fmt::Arguments::new_v1(&["Hello, ", "\n"],
&[::core::fmt::ArgumentV1::new_display(&k)]));
};
{
::std::io::_print(::core::fmt::Arguments::new_v1(&["Hello, ", "\n"],
&[::core::fmt::ArgumentV1::new_display(&k)]));
};
}
/*
# To see above code, you need to install rust nightly
rustup toolchain install nightly
rustup default nightly
# In Makefile
expand:
cargo rustc --profile=check -- -Zunpretty=expanded
# Later in terminal
make expand
# then you should see the above rust code.
*/
3. Simple logging with dbg!
rust
fn main() {
let k = "Hello World!";
dbg!(k);
dbg!(k);
}
/* Output
[src/main.rs:3] k = "Hello World!"
[src/main.rs:4] k = "Hello World!"
*/
javascript
let k = "Hello World";
debugger;
console.log(k);
// debugger sets the breakpoint,
// variable values can be seen live in code editors
// as js is a dynamic language!
golang
package main
import (
"log"
"os"
)
var logger = log.New(os.Stdout, "app: ", log.Ldate+log.Ltime+log.Lshortfile)
func main() {
k := "Helllo World"
logger.Println(k)
logger.Println(k)
}
/* Output
app: 2022/05/11 00:28:19 dbg.go:12: Helllo World
app: 2022/05/11 00:28:19 dbg.go:13: Helllo World
*/
4. Memory
To understand rust we must understand what is memory? and what is memory location, what is a pointer? and what is reference?
- Memory is array of virtual addresses given by OS process to executable.
- These virtual addresses are called memory locations.
- When you run out of these addresses, you run out of memory.
- Memory locations can contains either data or addresses of other locations.
- When one memory location contains address of other memory location it is called a pointer.
- When a pointer can be accessed safely with compile time guarantee it is called referece in rustlang.
4.1. Memory Regions
- Meomory has been broadly divided into three regions
- Static Memory - this is part of executable binary. Immutable string literals reside here
- Stack Memory - contains data whose size is known at compile time
- Heap Memory - contains data whose size is known only at runtime.
5. Data types based on how assignment operator (=) works
- Assignment operator is used for binding values to variables
- And also for implicitly copying data, moving ownership and borrwing data.
- Based on this functionality of assignment operator, data types are classified into 3 types:
- Copy type - Implicit copying of data using assignment operator (=) - uses only Stack Memory
- Move type - Implicit moving of ownership using assignment operator (=) - uses Stack and Heap Memory
- Borrow type - Implicit borrowing of data using assignment operator (=) - uses Stack and Static Memory
rust
fn main() {
// copy-type
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b: [i32; 5] = a; // Implicitly copied data from a to b
let c: [i32; 5] = a.clone(); // Explicitly copied data from a to c
dbg!(a, b, c); // a is still in scope as we only copied data
// As the data can be copied implicitly using assingment operator
// these variables are called copy-type
// move-type
let v1: Vec<i32> = vec![1, 2, 3, 4, 5];
let v2: Vec<i32> = v1; // Implicitly moved ownership from v1 to v2 and v1 is dropped
// dbg!(v1); // throws error as v1 is no more.
dbg!(v2);
// Todo the same explicitly
let v1: Vec<i32> = vec![1, 2, 3, 4, 5];
let v2: Vec<i32> = v1.clone();
drop(v1);
// borrow-type
let s: &'static str = "Hello World"; // Implicitly borrowed data from Static Memory
dbg!(s);
// Compiler embeds string literals into executable in a location called static memory
// and gives a reference to it during compilation
// we can't borrow data from stack memory and heap memory implicitly using assignment operator(=).
// we have to always use reference (&) to borrow from stack and heap memories.
// static memory lives for the entire program duraion unlike heap memory which gets cleared once owner goes out of scope.
// to convert borrow-type to own type
let s1: String = s.to_owned();
let s2: &String = &s1; // explicitly borrowed data from s1
dbg!(s2);
}
/* Output
[src/main.rs:7] a = [1,2,3,4,5]
[src/main.rs:7] b = [1,2,3,4,5]
[src/main.rs:7] c = [1,2,3,4,5]
[src/main.rs:16] v2 = [1,2,3,4,5]
[src/main.rs:25] s = "Hello World"
[src/main.rs:37] s2 = "Hello World"
*/
Golang and Javascript
- Golang and Javascript has only copy-type and borrow-type. They don't have move-type.
- Borrow-type meaning references of data copied implicitly using assignment operator =
- Eg. In javascript, arrays are borrow-type, references are copied implicitly
- Eg. In golang, dynamic arrays (i.e. slices) are borrow-type, references are copied implicitly
5.1. Copy-type
- All data types are move-type by default in rust. They are converted as copy-type by implimenting copy trait.
- Copy trait can be implimented only if the data size is known at compile time.
- So copy-type data types always uses Stack Memory.
- Primitives impliments copy-trait. So that they can be easily copied by using assignment operator.
- Primitives:
- Scalar values - Integers, Floating-point, Char, Boolean
- Composite values - Arrays and Tuples with Scalar values
rust
fn main() {
// primitives - always uses stack memory
// as their size is known at compile time.
// scalar types - copy types
let signed_int: i32 = -100;
let unsigned_int: u32 = 100;
let float: f64 = 10.0;
let is_playing: bool = true;
let special: char = '🍋';
dbg!(signed_int, unsigned_int, float, is_playing, special);
// These scalar types are still in scope
// even after passing to dbg! as they are copy-type.
dbg!(signed_int, unsigned_int, float, is_playing, special);
// composite types with scalar values
let tupple: (i32, bool, char) = (100, true, '🍋');
dbg!(tupple);
dbg!(tupple.1);
// fixed-size array
let array:[i32; 5] = [1,2,3,4,5];
dbg!(array);
// Now contents of array are copied into a new variable myarray
let myarray = array; // = operator is used as implicit copy
// contents of array are available even after assignment as it is copy-type
dbg!(array);
// if you want to copy contents of array explicitily use clone method
let iarray = myarray.clone();
dbg!(iarray);
}
/* Output
[src/main.rs:11] signed_int = -100
[src/main.rs:11] unsigned_int = 100
[src/main.rs:11] float = 10.0
[src/main.rs:11] is_playing = true
[src/main.rs:11] special = '🍋'
[src/main.rs:14] signed_int = -100
[src/main.rs:14] unsigned_int = 100
[src/main.rs:14] float = 10.0
[src/main.rs:14] is_playing = true
[src/main.rs:14] special = '🍋'
[src/main.rs:18] tupple = (100,true,'🍋')
[src/main.rs:19] tupple.1 = true
[src/main.rs:23] array = [1,2,3,4,5]
[src/main.rs:28] array = [1,2,3,4,5]
[src/main.rs:32] iarray = [1,2,3,4,5]
*/
javascript
function main() {
// Javascript has only two types of data based on assignment operator
// 1. Copy-type
// 2. Borrow-type i.e. reference type
let signed_int = -100;
let unsigned_int = 100;
let float = 10.0;
let is_playing = true;
let special = '🍋';
console.log(signed_int, unsigned_int, float, float, is_playing, special);
console.log(signed_int, unsigned_int, float, float, is_playing, special);
let tuple = [100, true, '🍋'];
console.log(tuple);
// javascript has only dynamic arrays and they are borrow-type
let array = [1,2,3,4,5];
console.log(array);
let myarray = array; // Assignment operator borrows data implicitly.
// change in myarray will effect array as well
myarray[0] = 222;
console.log(array);
console.log(myarray);
// if you want to copy contents of array use spread operator
let iarray = [...myarray];
iarray[0] = 333;
console.log(iarray);
console.log(myarray);
}
main()
/* Output
-100 100 10 10 true 🍋
-100 100 10 10 true 🍋
[ 100, true, '🍋' ]
[ 1, 2, 3, 4, 5 ]
[ 222, 2, 3, 4, 5 ]
[ 222, 2, 3, 4, 5 ]
[ 333, 2, 3, 4, 5 ]
[ 222, 2, 3, 4, 5 ]
*/
golang
package main
import (
"log"
"os"
)
var logger = log.New(os.Stdout, "app: ", log.Ldate+log.Ltime+log.Lshortfile)
func main() {
var signed_int int = -100
var unsigned_int uint = 100
var float float32 = 10.0
var is_playing bool = true
var special string = "🍋"
logger.Println(signed_int, unsigned_int, float, is_playing, special)
logger.Println(signed_int, unsigned_int, float, is_playing, special)
myint, mybool, mystring := 100, true, '🍋'
logger.Println(myint, mybool, mystring)
// fixed-size array - copy type
var array = [5]int{1, 2, 3, 4, 5}
// Now contents of array are copied into a new variable myarray
var myarray = array // = operator is used as implicit copy
// change in myarray will not effect array
myarray[0] = 222
logger.Println(myarray)
logger.Println(array)
// if you want to copy contents of array explicitily, same like implicit
var iarray = myarray
iarray[0] = 333
logger.Println(iarray)
logger.Println(myarray)
}
/* Output
app: 2022/06/02 17:02:50 copy.go:16: -100 100 10 true 🍋
app: 2022/06/02 17:02:50 copy.go:17: -100 100 10 true 🍋
app: 2022/06/02 17:02:50 copy.go:20: 100 true 127819
app: 2022/06/02 17:02:50 copy.go:29: [222 2 3 4 5]
app: 2022/06/02 17:02:50 copy.go:30: [1 2 3 4 5]
app: 2022/06/02 17:02:50 copy.go:35: [333 2 3 4 5]
app: 2022/06/02 17:02:50 copy.go:36: [222 2 3 4 5]
*/
5.2. Move-type
- All types which doesn't impliment copy triat are move-type.
- Uses stack memory, if data size is known at comple time
- Uses heap memory, if data size is known only at runtime
- It is not necessary for types to impliment copy trait even if their size is known at compile time.
- For instance, mutable references won't impliment copy trait even if their size is known at compile time.
- Only immutable references impliment copy trait so that they can be easily copied everywhere.
- Difference between copy-type and move-type is
- When you assign one variable to another variable,
- first variable is available even after assignment in case of copy-type
- in case of move-type first variable is NOT available, it will be dropped.
- Data types
- Types which doesn't impliment copy trait.
- Vector
- String
- Arrays and Tupples containg move-type data
Vector
- Vector is a dual-leg variable. Contains dynamic size data.
- one-leg is in stack memory and one-leg is in heap memory
- Stack contains fat pointer(struct fields) and Heap contains actual data.
- Vecotr is a struct with 3 fields - ptr, len and cap
- ptr - pointer - contains address of actual data in heap memory
- len - length of actual data
- cap - capacity - length of memory allocated in heap by allocator
- When we assign vector from one variable to another
- Stack data(fat pointer) is moved from first variable to second variable
- and first variable is dropped as rust follows single owner principle.
- Heap data remains as it is. i.e. no movement in heap data
rust
fn main(){
// dynamic array
let vector1: Vec<i32> = vec![1,2,3]; // move-type
// ownership moved from vector1 to vector2. And vector1 is dropped here.
let vector2 = vector1;
dbg!(vector2);
dbg!(vector1); // throws an error
}
fn main(){
let vector1: Vec<i32> = vec![1,2,3]; // move-type
let vector2 = &vector1; // reference to vector1
dbg!(vector2); // vector2 is a borrower
dbg!(vector1); // vector1 is a owner
}
/* Output
[src/main.rs:4] vector2 = [1,2,3]
[src/main.rs:5] vector1 = [1,2,3]
*/
javascript
function main(){
// Javascript has only dynamic arrays
let vector1 = [1,2,3]; //borrow-type
let vector2 = vector1; // borrows data from vector1 to vector2 implicitly
vector2[0] = 100; // // it effects vector1 also
console.log(vector1);
console.log(vector2);
}
main();
/* Output
[100, 2, 3]
[100, 2, 3]
*/
golang
package main
import (
"log"
"os"
)
var logger = log.New(os.Stdout, "app: ", log.Ldate+log.Ltime+log.Lshortfile)
func main() {
// This is slice of array as we are not specifying length of array
// This is called dynamic-array
// This is borrow-type data
vector1 := []int{1, 2, 3}
vector2 := vector1 // borrows data from vector1 to vector2 implicitly
vector2[0] = 100 // it effects vector1 also
logger.Println(vector1)
logger.Println(vector2)
}
/* output
app: 2022/06/01 16:28:04 move.go:16: [100 2 3]
app: 2022/06/01 16:28:04 move.go:17: [100 2 3]
*/
String
- String is Vector of utf-8 encoded bytes
- Whatever that applies to vector is also applies to String
5.3. Borrow-type
- Data size is known at compile time
- Data is stored in Static memory and reference is stored in Stack memory
- Used for immutable string literals
- String literals are always stored in Static Memory
- We only gets referece to that data implicitly
- Thats why it is always denoted with &str and called borrow-type
6. String vs. &str
- String is owned type. Memory is allocated in Heap. It has dynamic size. Stack data contains: ptr, len and cap.
- &str is borrowed type. Stored in Static memory. No allocation is needed. Stack data contains: ptr and len only.
7. dbg! Vs. println!
rust
fn main() {
// type annotaion
let k: &str = "World!";
let kid: String = format!("Hello {k}");
println!("{}", kid);
println!("{}", kid);
}
/* Output
Hello World!
Hello World!
*/
rust
fn main() {
let k: &str = "World!";
let kid: String = format!("Hello {k}");
dbg!(kid);
dbg!(kid);
}
/* Output
error[E0382]: use of moved value: `kid`
--> src/main.rs:5:10
|
3 | let kid = format!("Hello {k}");
| --- move occurs because `kid` has type `String`, which does not implement the `Copy` trait
4 | dbg!(kid);
| --------- value moved here
5 | dbg!(kid);
| ^^^ value used here after move
*/
Why println! is compiling? and why dbg! is not compiling?
- println! & format! macros always captures variables by its reference.
- i.e. println!("{}", kid) is converted to -> println!("{}", &kid)
- dbg! captures variables based on how you passed arguments. No magic.
- As a learner we should always use dbg!
- String is a move-type. Once you pass ownership of String to dbg!. It is not available for reuse.
- If you want to reuse, use reference explicitly. i.e. dbg!(&kid)
8. Lifetime of a variable
- In all other languages lifetime of a variable depends on functin scope and block scope.
- Where as in rust, it also depends on shadowing, ownership and mutable reference.
8.1. Function scope and Block scope
rust
fn scope(){
let fn_scope = "I have function scope";
{
// This can't be accessed outside this block
let block_scope = "I have block scope";
dbg!(block_scope);
dbg!(block_scope);
// this is dropped at the end of this block
}
dbg!(fn_scope); // works
dbg!(fn_scope); // works
//fn_scope is dropped here
}
javascript
function scope(){
let fn_scope = "I have function scope";
{
// This can't be accessed outside this block
let block_scope = "I have block scope";
console.log(block_scope);
console.log(block_scope);
// this is dropped at the end of this block
}
console.log(fn_scope); // works
console.log(fn_scope); // works
//fn_scope is dropped here
}
golang
package main
func main() {
fn_scope := "I have function scope"
{
// This can't be accessed outside this block
block_scope := "I have block scope"
println(block_scope)
println(block_scope)
// this is dropped at the end of this block
}
println(fn_scope) // works
println(fn_scope) // works
//fn_scope is dropped here
}
8.2. Variable Shadowing
rust
fn main(){
let hello = "Hello";
let hello = "Hello World"; // works
dbg!(hello);
}
/* Output
[src/main.rs:4] hello = "Hello World"
*/
javascript
function main(){
let hello = "Hello";
let hello = "Hello World"; // error
console.log(hello)
}
main()
/* Output
Uncaught SyntaxError: Identifier 'hello' has already been declared
*/
golang
package main
func main() {
hello := "Hello"
hello := "Hello World" // error
println(hello)
}
/* Output
snippets/shadowing.go:5:8: no new variables on left side of :=
*/
8.3. Ownership
- Every value in rust has owner
- Owner will be dropped if the data type is move-type immidiately after assignemnt to
- another variable or passing to a function
8.4. Non-Lexical Lifetime
- Explained in Mutable vs. Immutable
9. Type Annotations
- Rust compiler can inferer type of a variable based on context, most of the times.
- Some times compiler asks us to specify type
- And sometimes, we ourself specify the type of variable to meet our requirements
rust
fn main(){
let a: f64 = 20.5; // specified explicitly as f64
let b: f32 = 20.5; // specified explicitly as f32
let c = a + b as f64; // b casted as f64
dbg!(c); // 41.0
infer_types_as_f64();
infer_types_as_i32();
infer_types_as_i64();
mixed_types();
text();
tuples();
array();
vectors();
}
// default type for floating-point types is f64
fn infer_types_as_f64(){
let a = 20.5;
let b = 20.5;
let c = a + b;
dbg!(c); // 41.0
}
// default type for integers is i32
fn infer_types_as_i32(){
let a = 10;
let b = -10;
let c = a + b;
dbg!(c); // 0
}
// If you specify type for one variable in the equation, all other variables in the equation will also get same type.
fn infer_types_as_i64(){
let a = 10;
let b: i64 = 10; // type specified as i64
let c = a + b;
dbg!(c); // 20
}
fn mixed_types() {
let a = 10.9;
let b = 10;
let c = a as i32 + b; // here compiler asks us to specify type. As a and b are different types.
dbg!(c); // prints 20 . Ignores 0.9
dbg!(a); // a = 10.9
}
fn text(){
let a: &str = "hello";
let b: String = "world".to_string();
let c: String = format!("{} {}", a, b);
dbg!(c); // "hello world"
let d: char = 'A';
let e: char = 'B';
let f:String = format!("{}{}", d, e);
dbg!(f); // "AB"
}
fn tuples(){
let t = (100, 300, "Hai"); // tupples can have different types
let t: (i32, i32, &str) = (100, 300, "Hai"); // with annotations
dbg!(t.1); // 300
}
fn array() {
let a = [0,1,2,3,4]; // array can have only same type
let a: [i32;5] = [0,1,2,3,4]; // with annotation
let b = ['h', 'e', 'l', 'l', 'o'];
let b: [char; 5] = ['h', 'e', 'l', 'l', 'o']; // with annotation
let c = [3;5]; // [3, 3, 3, 3, 3]
let c: [i32; 5] = [3; 5]; // with annotation
let d = ["hai", "hello"];
let d: [&str; 2] = ["hai", "hello"]; // with annotation
dbg!(d.get(1)); // Some("hello")
dbg!(d[1]); // "hello"
}
fn vectors(){
let v = vec![1,2,3]; //vectors can have only same type
let v: Vec<i32> = vec![1,2,3]; //with annotation
dbg!(v[1]); // 2
dbg!(v.get(1)); // Some(2)
}
10. Creating new types and Type aliases
10.1. Creating new types
- struct is a keyword for creating product types.
- enum is a keyword for creating sum types.
- New types created by struct or enum are by default move-type.
- To convert them as copy-type we need to impl copy trait either by specifying attributes or by implimenting manually.
- To impliment copy trait, size of new type must be known at complie time. Otherwise we can only impliment clone but not copy.
rust
fn main(){
let stock: Stock = Stock{
qty: 10,
rate: 100
};
dbg!(stock);
// this will throw an error if copy attribute is not there at struct definition
dbg!(stock);
let w = Weekend::Saturday;
dbg!(w);
// this will throw an error if copy attribute is not there at enum definition
dbg!(w);
}
#[derive(Debug, Clone, Copy)]
struct Stock {
qty: i32,
rate: i32,
}
#[derive(Debug, Clone, Copy)]
enum Weekend {
Saturday,
Sunday,
}
/*
[src/main.rs:6] stock = Stock {
qty: 10,
rate: 100,
}
[src/main.rs:9] stock = Stock {
qty: 10,
rate: 100,
}
[src/main.rs:12] w = Saturday
[src/main.rs:13] w = Saturday
*/
10.2. Type aliases
- The main use case for type aliases is to reduce repetition.
- type is a keyword for creating type aliases.
- For example, we might have a lengthy type like this: Result<String, dyn Err>
- type K = Result<String, dyn Err>
- Now we can use K in place of Result<String, dyn Err>
11. Mutable vs Immutable
- All values and references are immutable by default in rust.
- To make them mutable we need to use keyword mut.
- Immutable references are copy-type
- Mutable references are move-type, copy trait is not implimented.
Book Analogy
- Lets say you have written a book and you want your friends to review it before publishing.
- And book is hosted in github
- As book is not yet published, you can say book is mutable.
- You can give some friends read-only access i.e. immutable reference.
- And as you don't want to deal with merge conflicts,
- you want to give write access to only one friend at a time i.e. mutable reference.
- Now the book is published. No more changes. Now book is immutable.
- For read-only book, you can give only read access i.e immutable reference.
- Conclusion
- Book can be either mutable or immutable
- If it is mutable, you can have either one mutable reference or any number of immutable references.
- If it is immutable, you can have only immutable references.
- Immutable references created prior to mutable reference are not available for use after mutable reference. This is called Non-lexical lifetime (NLL)
rust
fn main() {
// values
let v1 = 100; // immutable value
// v1 = 50; throws error
dbg!(v1);
let mut v2 = 100; // mutable value
v2 = 200; // works
dbg!(v2);
// references
let v1 = 100; // immutable variable
let r1 = &v1; // immutable reference
dbg!(r1);
dbg!(r1);
let mut v2 = 100; // mutable variable
let r2 = &v2; // immutable reference
dbg!(r2);
dbg!(r2);
let r3 = &mut v2; // mutable reference
dbg!(r3); // works
// dbg!(r3); throws an error as mutable reference is move-type
// dbg!(r2); throws error - Non-lexical lifetime. r2 can't be used after mutable reference
let r2 = &v2; // create a reference again as v2 is still in scope
dbg!(r2);
dbg!(r2);
}
fn main2() {
let mut girl = "hi";
let friend1 = &girl;
let friend2 = &girl;
let husband = &mut girl; // all friends are dropped here.
dbg!(friend1); // throws error. friend1 already dropped.
}
fn main3() {
let mut girl = "hi";
let husband1 = &mut girl;
let husband2 = &mut girl; // husband1 dropped here.
dbg!(husband1); // throws error. husband1 already dropped.
}
12. Functions and Closures
12.1 Functions
rust
fn main() {
let k = add(2, 3);
dbg!(k);
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
javascript
function main() {
let k = add(2, 3);
console.log(k);
}
function add(a, b) {
return a + b;
}
golang
package main
import "fmt"
func main(){
k := add(2, 3)
fmt.Println(k)
}
func add(a,b int) int {
return a + b
}
12.2 Function Pointer
- Function signature without function name is called function pointer
- Passing functions with function pointers will allow us to use functions as arguments to other functions.
rust
fn add(a: i32, b: i32) -> i32 {
a + b
}
// function pointer aliased to a type
type Binop = fn(i32, i32) -> i32;
fn main() {
let k = add(1, 2);
dbg!(k);
// function pointer
let kadd: fn(i32, i32) -> i32 = add;
let k = kadd(1, 2);
dbg!(k);
// function pointer with type alias
let bo: Binop = add;
let k = bo(1, 2);
dbg!(k);
// passing function as argument to another function with function pointer
let k = add_two(add);
dbg!(k);
// closure - no need to specify types. compiler can infer.
let cadd = |a, b| a + b;
let k = cadd(1, 2);
dbg!(k);
}
// passing function as argument to another function with function pointer
fn add_two(f: Binop) -> i32 {
f(2, 3) + 2
}
/*
[src/main.rs:10] k = 3
[src/main.rs:15] k = 3
[src/main.rs:20] k = 3
[src/main.rs:24] k = 7
[src/main.rs:29] k = 3
*/
12.3 Closures
- Closure is an anonymous function that can capture its environment.
- Closure can be coerced as a function pointer if it doesn't capture its environment.
- Closure can infere its arguments types based on its environment
rust
fn main() {
let add = |a, b| a + b;
let k = add(1, 2);
dbg!(k);
let k = add(2.5, 3.5); // throws error. Already infered as integers.
dbg!(k);
}
13. Methods and Associated Functions
- Methods are like functions but they are defined within the context of types (struct, enum and trait objects) with impl block
- Methods receive self as their first argument indicating they are instance methods
- Functions that don't receive self as their first argument are called Associated functions
- Methods (Instance Methods) are called on instance of type e.g mystr.len()
- Associated Functions (Static Methods) are called on type e.g String::from("Hello")
- Whether method is reading, mutating or consuming the instance depends on its receiver.
-
Types of recivers:
- &self for reading
- &mut self for mutating
- self for consuming
- Automatic referencing and dereferencing happens while calling these methods.
rust
#[derive(Debug)]
struct Circle {
radious: f64,
}
impl Circle {
fn area(self: &Self) -> f64 {
std::f64::consts::PI * self.radious * self.radious
}
// &self is short hand notaion for self: &Self
fn circumference(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radious
}
fn update_radious(&mut self, radious: f64) -> &Self {
self.radious = radious;
self
}
// Static Method. It doesn't take self as first argument
fn new(radious: f64) -> Self {
Circle { radious: radious }
}
}
fn main() {
let mut c = Circle { radious: 5.0 };
dbg!(&c);
dbg!(c.area());
dbg!(c.circumference());
dbg!(c.update_radious(10.0));
dbg!(c.area());
let c = Circle::new(3.0);
dbg!(&c);
dbg!(c.area());
}
/*
[src/main.rs:29] &c = Circle {
radious: 5.0,
}
[src/main.rs:30] c.area() = 78.53981633974483
[src/main.rs:31] c.circumference() = 31.41592653589793
[src/main.rs:32] c.update_radious(10.0) = Circle {
radious: 10.0,
}
[src/main.rs:33] c.area() = 314.1592653589793
[src/main.rs:35] &c = Circle {
radious: 3.0,
}
[src/main.rs:36] c.area() = 28.274333882308138
*/
14. Generics
- Generics is a tool for eliminating repetetive code for each type
rust
// A function that takes generic type
fn hai<T>(a: T) -> T {
a
}
// A struct that takes generic type
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
// A struct that takes generic tupple
#[derive(Debug)]
struct Circle<T>(T);
// A function that takes generic Circle
fn hello<T>(c: Circle<T>) -> Circle<T> {
c
}
fn main() {
dbg!(hai(123));
dbg!(hai(Point { x: 1, y: 1 }));
dbg!(hai(Circle(1)));
dbg!(hello(Circle(100)));
}
/*
[src/main.rs:23] hai(123) = 123
[src/main.rs:24] hai(Point { x: 1, y: 1 }) = Point {
x: 1,
y: 1,
}
[src/main.rs:25] hai(Circle(1)) = Circle(
1,
)
[src/main.rs:26] hello(Circle(100)) = Circle(
100,
)
*/
rust
#[derive(Debug)]
struct Square<T> {
side: T,
}
// Generic Type with Trait Bounds
impl<T: std::ops::Mul<Output = T> + Copy + Into<f64>> Square<T> {
fn area(&self) -> T {
self.side * self.side
}
fn circumference(&self) -> f64 {
4.0 * self.side.into()
}
}
fn main() {
let s = Square { side: 2 };
dbg!(&s);
dbg!(s.area());
dbg!(s.circumference());
let s = Square { side: 3.0 };
dbg!(&s);
dbg!(s.area());
dbg!(s.circumference());
}
/*
[src/main.rs:17] &s = Square {
side: 2,
}
[src/main.rs:18] s.area() = 4
[src/main.rs:19] s.circumference() = 8.0
[src/main.rs:22] &s = Square {
side: 3.0,
}
[src/main.rs:23] s.area() = 9.0
[src/main.rs:24] s.circumference() = 12.0
*/
15. Traits
- Traits - Defines common behaviour among types
- Also used as constrains on generic types
- Trait objects can also take generic types in their definition
rust
use std::fmt::Debug;
trait Shape {
fn area(&self) -> f64;
fn circumference(&self) -> f64;
// Default implementation for whoami method
fn whoami(&self) -> String {
"I am default implementation for Shape Trait whoami method".to_owned()
}
}
#[derive(Debug)]
struct Circle {
radious: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radious * self.radious
}
fn circumference(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radious
}
}
#[derive(Debug)]
struct Square {
side: f64,
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
fn circumference(&self) -> f64 {
4.0 * self.side
}
fn whoami(&self) -> String {
"I am Square".to_owned()
}
}
// This function accepts any type that implements Shape Triat
fn print(s: impl Shape + Debug) {
dbg!(&s);
dbg!(s.area());
dbg!(s.circumference());
dbg!(s.whoami());
}
// Above function can also be written using generic notation
fn kprint<T: Shape + Debug>(s: T) {
dbg!(&s);
dbg!(s.area());
dbg!(s.circumference());
dbg!(s.whoami());
}
fn main() {
print(Circle { radious: 10.0 });
kprint(Square { side: 5.0 });
}
/*
[src/main.rs:46] &s = Circle {
radious: 10.0,
}
[src/main.rs:47] s.area() = 314.1592653589793
[src/main.rs:48] s.circumference() = 62.83185307179586
[src/main.rs:49] s.whoami() = "I am default implementation for Shape Trait"
[src/main.rs:54] &s = Square {
side: 5.0,
}
[src/main.rs:55] s.area() = 25.0
[src/main.rs:56] s.circumference() = 20.0
[src/main.rs:57] s.whoami() = "I am Square"
*/
16. Generic Lifetime Annotations
- Lifetime of a variable
- Lifetimes in Functions
- Lifetimes in Data Structures
rust
17. Dangling Pointer
rust
18. Closures in Detail
rust
19. Iterators
rust
20. Smart Pointers
rust
21. Concurrency
rust