DEV Community

Cover image for Functional Programming in Rust
Natserract
Natserract

Posted on • Updated on

Functional Programming in Rust

A month ago when I first developed a non-commercial project using ReasonML, I got a new experience, is a functional programming. This language is an alternative syntax from OCaml. OCaml itself is a purely functional language, the features offered are interesting. For example: type inference, strongly type system, algebraic datatypes, and many more. Interesting right?

Now after trying that, I began to interest functional programming. Finally i tried to implement the functional paradigm in a different language, namely Rust.

Introduction

Functional programming (FP) is a programming paradigm which allows us to write expressive, concise, and elegant code. Functional programming also helps developers to manage code so that it doesn't DRY (Don't Repeat Yourself) that's mean doesn't write the same code over and over again. Other functional languages for example like Lisp, Clojure, Erlang, Haskell, R, and many more.

Okay, but why Rust?

The question is, is a Rust functional language? the answer is, no. Although Rust himself was inspired by ML family of language, Rust it's not functional. But fortunately Rust has several features that are similar to other functional languages, such as: algebraic datatypes, expressive types, and others.

Tables of Contents

Primitive Types

In order not to jump right away, it would be nice if we had to know several data types in Rust. This also applies to all programming languages.

Booleans

The most basic datatype is the simple true / false value, in Rust it's called bool

let x = true;
let y: bool = false;
Enter fullscreen mode Exit fullscreen mode

Char

The char data type has a single Unicode value. We can use char data type with a single tick (')

let x = 'x';
let two_hearts = '💕';
Enter fullscreen mode Exit fullscreen mode

Unlike some other languages, char in Rust is not one byte, but four.

Numeric Types

Rust has several numeric type category variants, such as signed(i) and unsigned(u), fixed size (8, 16, 32, 64) and variable(isize, usize) types.

let x = 42; // `x` has type `i32`.
let y = 1.0; // `y` has type `f64`.
Enter fullscreen mode Exit fullscreen mode

Arrays

Like many other programming languages, Rust also has an array data type. By default, arrays in Rust can't be changed. Unless you initialize it with mut

let a = [1, 2, 3]; // a: [i32; 3]
let mut m = [1, 2, 3]; // m: [i32; 3]
Enter fullscreen mode Exit fullscreen mode

Functions

Functions also have data types! For example like:

fn foo(x: i32) -> i32 { x }
let x: fn(i32) -> i32 = foo;
Enter fullscreen mode Exit fullscreen mode

In this case, the foo () function has a return type numeric: i32, and returns the valuex.

For more information, you can check here: primitive types

Closures

Closure is a mechanism by which an inner function will have access to the variables defined in its outer function’s lexical scope even after the outer function has returned. Up to here understand? in short closures is an inner function that has access to retrieve a value throughout the scope both inside and outside.

fn fmt(prev_str: &str) -> String {
    let mut new_str = String::new();

    let closure_annotated = |next_str| -> String {
        new_str.push_str(prev_str);
        new_str.push_str(next_str);
        return new_str;
    };

    closure_annotated("dolor sit amet")
}

let r_txt = "Lorem ipsum ";
assert_eq!("Lorem ipsum dolor sit amet", fmt(r_txt));
Enter fullscreen mode Exit fullscreen mode

In this case, in new_str.push_str () section where closure_annotated accesses thenew_str variable then changes the value.

Currying

Currying is a process in functional programming in which we can transform a function with multiple arguments into a sequence of nesting functions. It returns a new function that expects the next argument inline.

#[derive(Debug)]
struct States<'a> {
    a: &'a i32,
    b: &'a i32,
}

trait Currying {
    type ReturnType: Fn(i32) -> i32;
    fn add(self) -> Self::ReturnType;
}

impl Currying for States<'static>{
    type ReturnType = Box<dyn Fn(i32) -> i32>;

    fn add(self) -> Self::ReturnType {
        Box::new(move|x| {
            x * self.a
        })
    }
}

let r_value: States = States {
    a: &100,
    b: &100
};

let r1 = r_value.add();
let r2 = r1(5);

assert_eq!(500, r2);

Enter fullscreen mode Exit fullscreen mode

There are 2 parameters here, namely a,b where each has a numeric data type, then in trait section is a function interface, a place for initializing functions. These traits are similar to typescript interfaces.

Higher Order Functions(HOF)

Higher order functions are functions that use other functions as parameters or as a result of returns.

fn map<F>(arr: &[i32], func: F) -> Vec<i32> where F: Fn(&i32) -> i32{
    let mut new_array: Vec<i32> = vec![];
    for i in arr.iter() {
        new_array.push(func(i))
    }

    return new_array
}

let lists = vec![1, 4, 9, 16];
let result = map(&lists, |i| *i + 2);

assert_eq!(vec![3, 6, 11, 18], result)
Enter fullscreen mode Exit fullscreen mode

So func andmap are higher order functions, where this function is used to change every contents of an array. The return result is a new array of the same length as the modified originalArray.

Lazy Evaluation

Lazy evaluation or non-strict evaluation is a process of holding the evaluation of an expression/function until the value is needed. The goal is to avoid repeated evaluations.

struct State {
    x: i32,
}

trait Lazy {
    fn add(&self) -> i32;
    fn multiply(&self) -> i32;
    fn add_or_multiply(&self, add: bool) -> i32;
}

impl Lazy for State {
    fn add(&self) -> i32 {
        println!("executing add");
        &self.x + &self.x
    }

    fn multiply(&self) -> i32 {
        println!("executing multiply");
        &self.x * &self.x
    }

    fn add_or_multiply(&self, add: bool) -> i32 { 
        match add {
            true => self.add(),
            false =>  self.multiply(),
        }
    }
}

let val: State = State {
    x: 20
};

assert_eq!(40, val.add_or_multiply(true));
assert_eq!(400, val.add_or_multiply(false));
Enter fullscreen mode Exit fullscreen mode

Functional programming (FP) provides many advantages, and its popularity has been increasing as a result. However, each programming paradigm comes with its own unique jargon and FP is no exception. By providing a glossary, i hope to make learning FP easier✌️

Source code: rustfp.github

Discussion (12)

Collapse
aminnairi profile image
Amin • Edited on

Currying. A functional programming technique where we can create a function with many arguments

This is not the definition of currying in computer science.

Currying is (in maths and computer science) a programming technique that transforms a function of N parameters into a chain of functions (N functions) that each takes one parameter (a function of arity 1).

function uncurried(a, b) {
    return a + b;
}

function curried(a) {
    return function(b) {
        return a + b;
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
natserract profile image
Natserract Author • Edited on

Whoa, yes i missconception about currying, thankyou for your comment, i will edit this ;)

Collapse
natserract profile image
Natserract Author

Finally:

#[derive(Debug)]
struct States<'a> {
    a: &'a i32,
    b: &'a i32,
}

trait Currying {
    type ReturnType: Fn(i32) -> i32;
    fn add(self) -> Self::ReturnType;
}

impl Currying for States<'static>{
    type ReturnType = Box<dyn Fn(i32) -> i32>;

    fn add(self) -> Self::ReturnType {
        Box::new(move|x| {
            x * self.a
        })
    }
}

let r_value: States = States {
    a: &100,
    b: &100
};

let r1 = r_value.add();
let r2 = r1(5);

assert_eq!(500, r2);
Collapse
citizen428 profile image
Michael Kohl

OCaml itself is a purely functional language

OCaml isn't a pure functional language. The description on the website is very explicit about that: "OCaml is a general-purpose, multi-paradigm programming language which extends the Caml dialect of ML with object-oriented feature".

The most famous OCaml book, Real World OCaml, has a whole chapter on imperative programming.

dev.realworldocaml.org/imperative-...

Collapse
natserract profile image
Natserract Author • Edited on

I think OCaml is mostly pure but it could be impure in some cases. You can read here: ocaml.org/learn/tutorials/function...

Collapse
citizen428 profile image
Michael Kohl • Edited on

“mostly” is the keyword here though. It’s why IO in OCaml is a lot more straightforward and closer to what you’re used too than it’d be in a pure language. And IMHO the tutorials section of the OCaml website is of very mixed quality.

Not that Wikipedia is always the best source for programming topics, but this paragraph hits the nail on the head:

ML-derived languages are best known for their static type systems and type-inferring compilers. OCaml unifies functional, imperative, and object-oriented programming under an ML-like type system. Thus, programmers need not be highly familiar with the pure functional language paradigm to use OCaml.

That said, "purity" in FP is a totally overrated concept IMHO.

Collapse
ryuheechul profile image
Heechul Ryu

I was wondering how this code work as tail rec function and I discovered that it is not recursive at all.

impl Factor for i64_t {
    fn factorial_tail_rec(val: i64_t) -> Self {
        val
    }

    fn factorial(num: i64_t) -> Self {
        match num {
            0 => 1,
            _ => num * Self::factorial_tail_rec(num - 1)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You could tell by asserting like this:

let result: i64_t = Factor::factorial(4); 
assert_eq!(24, result);

// will give you this kind of error below:

/*
thread 'xxx' panicked at 'assertion failed: `(left == right)`
  left: `24`,
 right: `12`'
*/
Enter fullscreen mode Exit fullscreen mode
Collapse
codewander profile image
codewander

I have been avoiding rust as too low level, but lately I have been wondering if it has a shot at becoming the first widely used backend FP ecosystem, where scala, clojure, and haskell have seemed to plateau and given up on widespread adoption.

Collapse
dystroy profile image
Denys Séguret

An immediate question is "Why Functional programming in Rust?".

Rust's model is a different approach which solves many problems that FP solves. Accidental side effects don't normally happen in Rust and data are immutable by default. You're kind of forced to a modular and maintainable design too and risk-free concurrent execution comes easily.

So, why would you need or want FP in Rust ? Just as a kind of more elegant style ?

Collapse
citizen428 profile image
Michael Kohl

An immediate question is "Why Functional programming in Rust?".

It's a good question too. While bringing functional programming paradigms into other languages has been quite popular in the past few years, it can have its drawbacks. For example, if data structures were not designed with immutability in mind, you may end up with many copy operations because the compiler/interpreter can't make the guarantees necessary for structural sharing.

Two or so years ago I did a bit more Rust and when I benchmarked functional vs more imperative solutions the latter always came out faster, sometimes quite significantly. Granted, that may not matter for a lot of use cases, and the combination of raw performance while still offering high-level APIs is one of Rust's main selling points for me, but it's a tradeoff that needs to be considered.

While it's true that many languages nowadays can be written in many different styles, it's worth looking into how well supported different paradigms are in the language's implementation.

Collapse
meatboy profile image
Meat Boy

Omg, rust code looks so clean! Thanks for this article.

Collapse
phenax profile image
Akshay Nair

How is the last one lazy evaluation? The values are being evaluated eagerly in that example