Thursday, April 21, 2022

Go vs Rust - Functions & Methods & Closures

 Functions

A function is a group of statements that together perform a task. Both Go and Rust program has at least one function, which is main(). You can divide your code into separate functions. Logically, the division should be such that each function performs a specific task.

Go

Multiple results

package main
import "fmt"
func swap(x, y string) (string, string) {
   return y, x
}
func main() {
   a, b := swap("Mahesh", "Kumar")
   fmt.Println(a, b)
}

Named return values

func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}

Functions as Values

package main

import ("fmt" "math")

func main(){
   /* declare a function variable */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }

   /* use the function */
   fmt.Println(getSquareRoot(9))
}

Rust

fn add_one(x: i32) -> i32 {
    x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}
fn main() {
    let answer = do_twice(add_one, 5);
    println!("The answer is: {}", answer);
}

Closures
Sometimes it is useful to wrap up a function and free variables for better clarity and reuse. The free variables that can be used come from the enclosing scope and are ‘closed over’ when used in the function. From this, we get the name ‘closures’ 

Function Closure in Go

Go programming language supports anonymous functions which can acts as function closures. Anonymous functions are used when we want to define a function inline without passing any name to it.

package main
import "fmt"
func getSequence() func() int {
   i:=0
   return func() int {
      i+=1
      return i  
   }
}
func main(){
   /* nextNumber is now a function with i as 0 */
   nextNumber := getSequence()  
   /* invoke nextNumber to increase i by 1 and return the same */
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   
   /* create a new sequence and see the result, i is 0 again*/
   nextNumber1 := getSequence()  
   fmt.Println(nextNumber1())
}

Rust

Rust’s implementation of closures is a bit different than other languages. They are effectively syntax sugar for traits.

let plus_one = |x: i32| x + 1;

We have three separate traits to overload with:

Fn
FnMut
FnOnce

There are a few differences between these traits, but a big one is self: Fn takes &self, FnMut takes &mut self, and FnOnce takes self. This covers all three kinds of self via the usual method call syntax. But we’ve split them up into three traits, rather than having a single one. This gives us a large amount of control over what kind of closures we can take.

Closures and their environment

The environment for a closure can include bindings from its enclosing scope in addition to parameters and local bindings.

let num = 5;
let plus_num = |x: i32| x + num;

Move Closures

We can force our closure to take ownership of its environment with the move keyword

let num = 5;
let owns_num = move |x: i32| x + num;

Taking Closures as Arguments

Now that we know that closures are traits, we already know how to accept and return closures: the same as any other trait!

fn call_with_one<F>(some_closure: F) -> i32
    where F: Fn(i32) -> i32 {
    some_closure(1)
}
let answer = call_with_one(|x| x + 2);

In Rust, we can stack allocate our closure environment, and statically dispatch the call. This happens quite often with iterators and their adapters, which often take closures as arguments.

Of course, if we want dynamic dispatch, we can get that too. A trait object handles this case, as usual:

fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    some_closure(1)
}
let answer = call_with_one(&|x| x + 2);

Now we take a trait object, a &Fn. And we have to make a reference to our closure when we pass it to call_with_one, so we use &||.

Higher-Ranked Trait Bounds

A closure that can borrow its argument only for its own invocation scope, not for the outer function's scope. In order to say that, we can use Higher-Ranked Trait Bounds with the for<...> syntax:

fn call_with_ref<F>(some_closure:F) -> i32
    where F: for<'a> Fn(&'a i32) -> i32 {

    let value = 0;
    some_closure(&value)
}

Returning Closures

Closures are represented by traits, which means you can’t return closures directly. In most cases where you might want to return a trait, you can instead use the concrete type that implements the trait as the return value of the function. But you can’t do that with closures because they don’t have a concrete type that is returnable. Rust doesn’t know how much space it will need to store the closure. We can use box to resolve this problem.

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

Function Pointers and Closures

A function pointer is kind of like a closure that has no environment. As such, you can pass a function pointer to any function expecting a closure argument.

fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    some_closure(1)
}
fn add_one(i: i32) -> i32 {
    i + 1
}
let answer = call_with_one(&add_one);
assert_eq!(2, answer);

Methods

 Go

Go language support methods. Go methods are similar to Go function with one difference, i.e, the method contains a receiver argument in it. With the help of the receiver argument, the method can access the properties of the receiver.

package main
import (
   "fmt" 
   "math" 
)
/* define a circle */
type Circle struct {
   x,y,radius float64
}
/* define a method for circle */
func(circle Circle) area() float64 {
   return math.Pi * circle.radius * circle.radius
}
func main(){
   circle := Circle{x:0, y:0, radius:5}
   fmt.Printf("Circle area: %f", circle.area())
}

Rust 

Some functions are connected to a particular type. These come in two forms: associated functions, and methods. Associated functions are functions that are defined on a type generally, while methods are associated functions that are called on a particular instance of a type.

Associated Functions or Static Methods

The structure_name :: syntax is used to access a static method.

struct Point {
    x: f64,
    y: f64,
}
// Implementation block, all `Point` associated functions & methods go in here
impl Point {
    // This is an "associated function" because this function is associated with
    // a particular type, that is, Point.
    //
    // Associated functions don't need to be called with an instance.
    // These functions are generally used like constructors.
    fn origin() -> Point {
        Point { x: 0.0, y: 0.0 }
    }
    // Another associated function, taking two arguments:
    fn new(x: f64, y: f64) -> Point {
        Point { x: x, y: y }
    }
}

Methods or Instance Method

The first parameter of a method will be always self, which represents the calling instance of the structure. Methods operate on the data members of a structure.

struct Rectangle {
    p1: Point,
    p2: Point,
}
impl Rectangle {
    // This is a method. &self is sugar for `self: &Self`, where `Self` is the type of the
    // caller object. In this case `Self` = `Rectangle`
    fn area(&self) -> f64 {
        // `self` gives access to the struct fields via the dot operator
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;
        // `abs` is a `f64` method that returns the absolute value of the
        // caller
        ((x1 - x2) * (y1 - y2)).abs()
    }
    fn perimeter(&self) -> f64 {
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;
        2.0 * ((x1 - x2).abs() + (y1 - y2).abs())
    }
    // This method requires the caller object to be mutable
    // `&mut self` desugars to `self: &mut Self`
    fn translate(&mut self, x: f64, y: f64) {
        self.p1.x += x;
        self.p2.x += x;
        self.p1.y += y;
        self.p2.y += y;
    }
}

No comments:

Post a Comment