Naomijub's Blog

Welcome to my blog!

This blog is where I share my ideas and processes regarding software engineer, functional programming, Rust, best practices and game development.

Where to find me 📬:


Who am I?

I'm a lead software engineer with experience in web services, project migrations, Rust and cloud. Some notable places I've worked are:

  • Nubank as Lead Software Engineer:
    • I worked on integrating transfers (Pix) and payments to checkins account. The technologies I used were Clojure, Apache Kafka, Datomic, AWS, Flutter.
    • Edn-rs, Clojure's EDN format support for Rust
    • Edn-derive, derive macros for edn-rs
    • Brcode, Pix QR Code parser in Rust with FFI versions in Dart, Clojure, Typescipr, Java.
  • Ubisoft as Team Lead Programmer:
    • I worked on online services for an unannounced production. Rust, AWS (Lambdas, DynamoDB, S3, etc), Terraform, Kubernetes, Prometheus, Grafana, Unreal Engine, Apache Kafka, C++.
    • I worked as lead developer experience for Rainbow Six Mobile, improving content creators and programmers usage on tools and online services. C#, Unity, Prometheus, Grafana, SQLite.
  • Thoughtworks as Tech Lead:
    • Microservices migrations. Notable works are:
      • Java 7 + Spring + MySQL service to Rust + MySQL/Cassandra microservice, which had a huge cost reduction and increased performance.
      • Java 8 + Spring service to Clojure + Pedestal microservice. Fun fact the Clojure service had less lines of code than the java service had classes (Not include tests).
      • Java 7 + Spring service to Java 8 + Spark + Spring Boot microservices using a functional programming approach.
      • Ruby on Rails monolith to Elixir microservices.
    • SRE, developed a state of the art system to monitor the state of the website.
      1. Natural language processing service written in Rust that scrapped social media to detect user comments on problems the werbsite had.
      2. Using sitespeed.io scripts, we actively navigated the web site detecting latency, flow issues, inconsistencies and crashes.
      3. Created a kubernetes clusters that held all the data and had a good developer experience for service developers to integrate and manage.
    • Lead Studio XR Researcher creatring prototypes to showcase in Unity and Vuforia:
      • AR Pokemon inspired card game.
      • Luggage analyzer and trip visualizer.
      • VR Airplane replair mechanical training.

I also wrote a few books for APress and Casa do Codigo:

Stuff I like:

  • Gender equality and LGBTIQ+
  • Card and Board games
  • Databases, I'm really interested in relational-like time serial database. My pet project on this is WooriDB.
  • Rust FFI, did a few cool projects exploring FFI in Rust JVM/Rust FFI.
  • I have a lot of pet projects with Procedural Content Generation and Voxels
  • I have a few pet projects with AR/VR
  • I love to collaborate with ECS/Bevy stuff Space editor and bevy-inspector
  • Working on games with my son.
  • Lastly, I like Genetic Algorithms, Fuzzy Logic and Natural language Processing

Cool Open Source Work:

Function Composition and Partial Functions in Rust

dateDescriptionKeywords
2024-12-20Function Composition in RustFunctional Programming, Rust, Composition

In my latest post, about functional programming I talked about Composition and Higher Order functions, but it was merely a theoretical topic, but today I want to introduce examples of function composition and partial functions in Rust, comparing to other languages.

Introduction

This is something that has been in my mind for a while, but recently a friend of mine asked how my project deals with unit testing functions that have side effects, and my answer was "Higher order functions and function composition". This is something that I have done to the Java/Clojure project we worked together at Thoughtworks some time ago, and I used it as a reminder to that friend. However, more than a reminder to them, I remembered a really old (2019) composition Rust project I did:

fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(A) -> B,
    G: Fn(B) -> C,
{
    move |x| g(f(x))
}

fn main() {
    let add_and_multiply = compose(|x| x * 3f32, |x| x + 3f32);
    let divide_and_subtract = compose(|x| x / 3f32, |x| x - 3f32);

    let composed = compose(add_and_multiply, divide_and_subtract);
    println!("Result is {}", composed(20f32));
}

But now I want to take my time to explain this code and how it would look now.

What is Function Composition

Function composition is a powerful concept in programming that allows developers to build complex logic by combining functions. It is core to the functional programming paradigm. It allows developers to combine 2 or more functions producing a new function, using functions as input arguments and return types to other functions. mathematically it means that giver the functions f(x) and g(x), their composition, h(x) can be expressed as:

h(x) = g(f(x))

The main purposes of function composition are:

  • Reusability: Reuse smaller, tested functions to build more complex logic.
  • Readability: Make code more declarative by abstracting low-level details, giving more appropriate namings to blocks.
  • Testability: Test smaller components independently, and manipulate complex function creating functions that act like mocks for their specific case.

Implementing Function Composition in Rust

Rust is not necessarily the easiest language to do function composition. However, it is quite expressive in its robustness to do so. Meaning that we can achieve it through closures and Fn traits. Let's first go over a simpler case, given the functions:

#![allow(unused)]
fn main() {
fn duplicate(x: i32) -> i32 {
    x * 2
}

fn square(x: i32) -> i32 {
    x * x
}
}

We want to compose them in a way that we square x after duplicating it. It could easily be done by calling square(duplicate(3)), but this is not actually composition, and makes our life a bit harder to read over a long pipe, so we want to be able to compose it as follows compose(duplicate, square), meaning that we will first duplicate and then square it. Which means that now we have to understand how the compose function works:

#![allow(unused)]
fn main() {
fn compose(f: impl Fn(i32) -> i32, g: impl Fn(i32) -> i32) -> impl Fn(i32) -> i32 {
    move |x| g(f(x))
}
}

Compose receives as argument two function implementations of trait Fn, f and g, both of them receive receive as argument a type i32 and return a type i32, then we define h(x) as g(f(x)), which can be transformed into a function by applying move the closure |x| g(f(x)).

In the first versions of Rust, this was possible by using the Box pointer. If you read Portuguese, you can learn a bit more about this in my book Programação Funcional e Concorrente em Rust.

Now that we have defined the compose function, we can use it to compose the two functions we created (Rust Playground):

fn main() {
    let double_then_square = compose(duplicate, square);
    println!("Anonymous composition: {}", compose(duplicate, square)(3)); // Output: 36
    println!("Named composition:     {}", double_then_square(3));         // Output: 36
}

There are two ways of using the result from compose:

  • Assign it to a variable and use it as a named function, in our case double_then_square.
  • Call it anonymously with the desired x value, as compose(duplicate, square)(3).

Some other use cases that are common in Rust ecosystem:

  • Data Transformation Pipelines: Transforming streams of data in a structured manner.
  • Middleware in Web Frameworks: Combining pre-processing and post-processing logic.
  • Chained Operations: Complex mathematical computations or algorithms.

Comparing to C++

To those that know that I complain a lot about functional programming in C++, I know it possible achieve it using function pointers, lambda expressions, and higher-order functions from libraries like <functional> and it looks a lot like what Rust looks like, we just have to consider that impl Fn(i32) -> i32 becomes the function pointer std::function<int(int)>.

#include <iostream>
#include <functional>

int duplicate(int x) {
    return x * 2;
}

int square(int x) {
    return x * x;
}

std::function<int(int)> compose(std::function<int(int)> f, std::function<int(int)> g) {
    return [f, g](int x) { return g(f(x)); };
}

int main() {
    auto double_then_square = compose(duplicate, square);
    std::cout << "Anonymous composition: " << compose(duplicate, square)(3) << std::endl; // Output: 36
    std::cout << "Named composition: "     << double_then_square(3)         << std::endl; // Output: 36
    return 0;
}

While both Rust and C++ allow function composition, Rust’s type system and functional idioms make the process more expressive and safer. The lack of null pointers and the Option type help avoid runtime errors. Also, we can easily make the compose function more generic with generics, which is not as easy in C++:

#![allow(unused)]
fn main() {
fn compose<T>(f: impl Fn(T) -> T, g: impl Fn(T) -> T) -> impl Fn(T) -> T {
    move |x| g(f(x))
}
}

Partial Functions in Rust

Partial functions are somewhat similar to function composition, but we define a function to a subset of the possible values that it can receive, as if it was using a constant value instead of a function. In this case, I want to extrapolate partial functions from Clojure's partial function.

Clojure's partial is a core language utility that allows to pre-fill arguments to a function, creating a new function with fewer parameters. Could say that it is quite common for testing Dal like structures that require a lot of configuration. So let's consider the following example:

(defn multiply [a b]
  (* a b))

(def multiply-by-three (partial multiply 3))

(println (multiply-by-three 4)) ; Output: 12

We have the function multiply, but we only care about the case where multiply has the first element set to 3, binding it as multiply-by-three from (partial multiply 3), then we can just call it (multiply-by-three 4). The caveat from Clojure side is that partial can only be applied to the first n arguments, while Rust gives you a bit more control over that.

The following Rust code is a translation of the previous Clojure code with a twist of having the partial applied to the second argument, demonstrating how Rust enables a bit more control over this:

fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

fn partial_multiply(b: i32) -> impl Fn(i32) -> i32 {
    move |a| multiply(a, b)
}

fn main() {
    let multiply_by_four = partial_multiply(4);
    println!("Result: {}", multiply_by_four(3)); // Output: 12
}

multiply function is exactly the same. However, the partial application of multiply, shifts which argument we are applying the partiality, as we create multiply_by_four applying partial_multiply to b, instead of a. Note that we can apply multiply_by_four multiple times and always having the guarantee that we will multiply by 4.

Use Cases of Partial Functions

  • Pre-binding Arguments: Useful when testing functions that require a lot of configuration, or event-driven architectures where some functions take repetitive arguments over and over.
  • Currying Simulation: Breaking down multi-argument functions into simpler, single-argument functions.
  • Code Reusability: Creating specific versions of generic functions, useful for simplifying test cases as well.

My Coding Problems and Code Examples

Best Time to Buy and Sell Stock in Rust

dateDescriptionKeywords
2024-12-14Leetcode - problems 121 and 122, Best Time to Buy and Sell Stock in RustLeetcode, 121, 122

Leetcode has two best time to sell problems that I have solved, one easy and one medium. Here we will take a look at both, starting from the easy then going to the medium.

Best Time to Buy and Sell Stock (Easy/121):

Leetcode gives us the following statement:

"You are given an array prices where prices[i] is the price of a given stock on the ith day.

You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.

Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0."

Example cases:

  • Example 1:
Input: prices = [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.
  • Example 2:
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transactions are done and the max profit = 0.

Constraints

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104

My Solution:

This is a pretty straight forward problem, you have a vector of elements, where all elements are larger or equal to zero and you need to identify the maximum and the minimum of this vector. You can achieve this by iterating over all elements and preserving the minimum and the maximum difference for each element. In a procedural approach, this would mean:

#![allow(unused)]
fn main() {
pub fn max_profit(prices: Vec<i32>) -> i32 {
    let mut min = i32::MAX;
    let mut max_profit = 0;

    for value in prices {
        min = value.min(min);
        max_profit = max_profit.max(value - min);
    }

    max_profit
}
}

So, we create two variables to control min, which starts as anything above 104 (second constraint, but i32::MAX works beyond that constraint) and max_profit, that should start as zero in case we do not find any maximum value. Then we loop over all elements and reassign min and max_profit for the current value. Definitvely works, and is quite uncomplex solution with constant value allocation and a simple loop over n (prices.len()). Now we need to transition this to a functional solution, which in this case is have an iterator consumer and immutable data:

#![allow(unused)]
fn main() {
pub fn max_profit(prices: Vec<i32>) -> i32 {
    prices
        .iter()
        .fold((i32::MAX, 0), |(min, max_profit), &value| {
            (value.min(min), max_profit.max(value - min))
        })
        .1
}
}

To begin, we create an iterator, prices.iter(), then we consume this iterator with a fold, where the accumulator is a tuple consisting of the minimal value, same as before, and a maximum profit starting as 0. Then, in the fold loop, we already return a tuple containing the new minimum, value.min(min), as the first element, and the new maximum profit max_profit.max(value - min), as the second element. After we have consumed all the iterator, just return the second element of the resulting tuple.

Benchmark wise both solutions were equivalent in execution time and memory using criterion and memory-stats crates.

Best Time to Buy and Sell Stock (Medium/122):

"You are given an integer array prices where prices[i] is the price of a given stock on the ith day.

On each day, you may decide to buy and/or sell the stock. You can only hold at most one share of the stock at any time. However, you can buy it then immediately sell it on the same day.

Find and return the maximum profit you can achieve."

Example cases:

  • Example 1:
Input: prices = [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.
Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.
Total profit is 4 + 3 = 7.
  • Example 2:
Input: prices = [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
Total profit is 4.
  • Example 3:
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: There is no way to make a positive profit, so we never buy the stock to achieve the maximum profit of 0.

Constraints

  • 1 <= prices.length <= 3 * 104
  • 0 <= prices[i] <= 104

My Solution:

This problem is a bit more flexible as we can buy and sell in the same day, which means we can use a method that traverses the vector two by two, windows. This method allows us to traverse the following array [1, 2, 3, 4], in the following manner [[1, 2], [2, 3], [3,4]]. This means that for the example [7,1,5,3,6,4], we get the following results [[7,1], [1,5], [5,3], [3,6], [6,4]], if we filter out the results where the second element is smaller than the first we get [[1,5], [3,6]], which means a maximum profit of (5 - 1) + (6 - 3). This works well even with se quentially increasing number like [1, 2, 3, 4, 5, 6, 7]. Considering this, the trivial answer would be:

#![allow(unused)]
fn main() {
pub fn max_profit(prices: Vec<i32>) -> i32 {
    prices.windows(2)
        .filter(|price| price[0] < price[1])
        .map(|price| price[1] - price[0])
        .sum()
}
}

Where we iterate over prices two by two, filtering out all pairs where the second element is smaller than the first, so we map their difference and sum all the values. Note that sum starts at zero. Even if this is a good solution, I figured there is an even better approach to this, which results in one less line of code:

#![allow(unused)]
fn main() {
pub fn max_profit(prices: Vec<i32>) -> i32 {
    prices.windows(2)
        .map(|price| 0.max(price[1] - price[0]))
        .sum()
}
}

In this new solution, we replace the filter with a map operation that gets the maximum value between zero and the difference between prices (price[1] - price[0]). This means that if price[0] >= price[1], the map will return 0, allowing us to remove the filter line.

Merge Sorted Arrays in Rust

dateDescriptionKeywords
2024-12-11Leetcode - problem 88, merge sorted arrays in RustLeetcode, 88

I wanted to start with some exercises that allow us to discuss functional problem solving exercises from platforms like Leetcode, Codility and Exercism.org. Among all this platforms, I would say leetcode is the most well known, but exercism is my favorite due to the very complete and transparent test cases. However, recently, I have been asked to live code in a mob session in Rust, Clojure and Go of leetcode problems, so this series is a representation of those sessions.

Merge Sorted Arrays:

Leetcode gives us the following statement:

"You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively.

Merge nums1 and nums2 into a single array sorted in non-decreasing order.

The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n."

Example cases:

  • Example 1:
Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
Output: [1,2,2,3,5,6]
Explanation: The arrays we are merging are [1,2,3] and [2,5,6].
The result of the merge is [1,2,2,3,5,6] with the underlined 
elements coming from nums1.
  • Example 2:
Input: nums1 = [1], m = 1, nums2 = [], n = 0
Output: [1]
Explanation: The arrays we are merging are [1] and [].
The result of the merge is [1].
  • Example 3:
Input: nums1 = [0], m = 0, nums2 = [1], n = 1
Output: [1]
Explanation: The arrays we are merging are [] and [1].
The result of the merge is [1].
Note that because m = 0, there are no elements in nums1. The 0 is only
there to ensure the merge result can fit in nums1.

Constraints

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • -109 <= nums1[i], nums2[j] <= 109

Naive Solution:

we can start by doing a simple for loop that iterates over the range of elements (0..n), which means, all elements in nums2:

#![allow(unused)]
fn main() {
pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut Vec<i32>, n: i32) {
    for nums2_index in (0..n) {
        // ...
    }
}
}

Now, we know that we need to add each element of nums2 to a position in nums1, starting in m:

#![allow(unused)]
fn main() {
pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut Vec<i32>, n: i32) {
    for nums2_index in (0..n) {
        nums1[(m + nums2_index) as usize] = nums2[nums2_index as usize];
    }
}
}

With this, our array contains 2 sorted sections of arrays in nums1, so we could just call a simple rust sort there:

#![allow(unused)]
fn main() {
pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut Vec<i32>, n: i32) {
    for nums2_index in (0..n) {
        nums1[(m + nums2_index) as usize] = nums2[nums2_index as usize];
    }
    nums1.sort();
}
}

A final style touch to this code is that we don't need to pass a mutable reference to a Vec<i32>, we can just pass &mut &[i32]. Also, style wise, the correct way to express a range in a for loop is without (), so we should rewrite the for loop as for num2_index in 0..n:

#![allow(unused)]
fn main() {
pub fn merge_naive(nums1: &mut [i32], m: i32, nums2: &mut [i32], n: i32) {
    for nums2_index in 0..n {
        nums1[(m + nums2_index) as usize] = nums2[nums2_index as usize];
    }
    nums1.sort();
}
}

According to benchmarks using criterion and memory-stats, for only the first example the execution time is around 7.8 ns and 40 KB of memory (all relative to my machine). For the largest example I have it has an execution time of 1 ms and a Memory consumption of 2.19 MB, not a bad start.

Less procedural

Rust has a great function to solve this problem and to solve my problem with for loops, it is called splice. Splice receives a mutable reference to the vector, a range of points to splice (replace_with) the vector, and an IntoIterator to loop. The following code represents the solving of this problem with this function:

#![allow(unused)]
fn main() {
pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut Vec<i32>, n: i32) {
    let m = m as usize;
    let n = n as usize;
    nums1.splice(m..m+n, nums2.to_owned());
    nums1.sort();
}
}

One thing I don't like in this exact code is m+n range ending, as we could consider for this problem n+m == nums1.len(), so the whole function would look like:

#![allow(unused)]
fn main() {
pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut Vec<i32>, n: i32) {
    let m = m as usize;
    let _ = nums1.splice(m.., nums2.to_owned());
    nums1.sort();
}
}

Another take on this, would be to use the n variable with a take function, as well as simplify the use case of the & mut Vec<i32> where Vec is not needed:

#![allow(unused)]
fn main() {
pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut [i32], n: i32) {
    let m = m as usize;
    nums1.splice(m.., nums2.iter().copied().take(n as usize));
    nums1.sort();   
}
}

This is about 10% more memory efficient and 10% less performatic than the for loop, that consumed around 40 KB for the first example, while this approach consumes only around 36 KB, for the largest test case. Performance wise, this approach being about 10% less performatic with 8.5 ns. Another interesting addition of using splice is that if we collect::<Vec<i32>>() the splice, we can return the exactly elements that were replaced by paying the allocation cost of a new Vec<i32>.

"But Julia! you are using std functions to solve this problem!". Yes! young padawan, why recreate the wheel? But if you really want, here is a guide:

  1. Check if the first m elements are 0, if they are, just replace them with nums2.
  2. Create an iterator for nums2.
  3. Loop over nums1, and if the next element in nums2 iterator is less of equal to the current element in nums1, insert it in the list and pop the last element.
  4. Get the next element in nums2 iterator.

A pure functional approach in this problem is hard as we are dealing with mutability in nums1, but we could easily create a new vector from the originals ones and return it. This would be terrible memory wise as we are allocating a new vector full of new elements.

For the previous guide, a more functional approach can be using fold and collecting the elements in a new vector. A pure functional way would require returning that list and avoiding mutability. However, that would fail leetcode test cases, so it will need to be assigned to nums1. I would risk saying that m and n are useless in a more functional Rust.

If you need help, you can open an issue in the repo

Benchmark dependencies:

[dev-dependencies]
criterion = "0.4"
memory-stats = "1"

My Thoughts on Software Engineering and Programming

Function Composition and Partial Functions in Rust

dateDescriptionKeywords
2024-12-20Function Composition in RustFunctional Programming, Rust, Composition

In my latest post, about functional programming I talked about Composition and Higher Order functions, but it was merely a theoretical topic, but today I want to introduce examples of function composition and partial functions in Rust, comparing to other languages.

Introduction

This is something that has been in my mind for a while, but recently a friend of mine asked how my project deals with unit testing functions that have side effects, and my answer was "Higher order functions and function composition". This is something that I have done to the Java/Clojure project we worked together at Thoughtworks some time ago, and I used it as a reminder to that friend. However, more than a reminder to them, I remembered a really old (2019) composition Rust project I did:

fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(A) -> B,
    G: Fn(B) -> C,
{
    move |x| g(f(x))
}

fn main() {
    let add_and_multiply = compose(|x| x * 3f32, |x| x + 3f32);
    let divide_and_subtract = compose(|x| x / 3f32, |x| x - 3f32);

    let composed = compose(add_and_multiply, divide_and_subtract);
    println!("Result is {}", composed(20f32));
}

But now I want to take my time to explain this code and how it would look now.

What is Function Composition

Function composition is a powerful concept in programming that allows developers to build complex logic by combining functions. It is core to the functional programming paradigm. It allows developers to combine 2 or more functions producing a new function, using functions as input arguments and return types to other functions. mathematically it means that giver the functions f(x) and g(x), their composition, h(x) can be expressed as:

h(x) = g(f(x))

The main purposes of function composition are:

  • Reusability: Reuse smaller, tested functions to build more complex logic.
  • Readability: Make code more declarative by abstracting low-level details, giving more appropriate namings to blocks.
  • Testability: Test smaller components independently, and manipulate complex function creating functions that act like mocks for their specific case.

Implementing Function Composition in Rust

Rust is not necessarily the easiest language to do function composition. However, it is quite expressive in its robustness to do so. Meaning that we can achieve it through closures and Fn traits. Let's first go over a simpler case, given the functions:

#![allow(unused)]
fn main() {
fn duplicate(x: i32) -> i32 {
    x * 2
}

fn square(x: i32) -> i32 {
    x * x
}
}

We want to compose them in a way that we square x after duplicating it. It could easily be done by calling square(duplicate(3)), but this is not actually composition, and makes our life a bit harder to read over a long pipe, so we want to be able to compose it as follows compose(duplicate, square), meaning that we will first duplicate and then square it. Which means that now we have to understand how the compose function works:

#![allow(unused)]
fn main() {
fn compose(f: impl Fn(i32) -> i32, g: impl Fn(i32) -> i32) -> impl Fn(i32) -> i32 {
    move |x| g(f(x))
}
}

Compose receives as argument two function implementations of trait Fn, f and g, both of them receive receive as argument a type i32 and return a type i32, then we define h(x) as g(f(x)), which can be transformed into a function by applying move the closure |x| g(f(x)).

In the first versions of Rust, this was possible by using the Box pointer. If you read Portuguese, you can learn a bit more about this in my book Programação Funcional e Concorrente em Rust.

Now that we have defined the compose function, we can use it to compose the two functions we created (Rust Playground):

fn main() {
    let double_then_square = compose(duplicate, square);
    println!("Anonymous composition: {}", compose(duplicate, square)(3)); // Output: 36
    println!("Named composition:     {}", double_then_square(3));         // Output: 36
}

There are two ways of using the result from compose:

  • Assign it to a variable and use it as a named function, in our case double_then_square.
  • Call it anonymously with the desired x value, as compose(duplicate, square)(3).

Some other use cases that are common in Rust ecosystem:

  • Data Transformation Pipelines: Transforming streams of data in a structured manner.
  • Middleware in Web Frameworks: Combining pre-processing and post-processing logic.
  • Chained Operations: Complex mathematical computations or algorithms.

Comparing to C++

To those that know that I complain a lot about functional programming in C++, I know it possible achieve it using function pointers, lambda expressions, and higher-order functions from libraries like <functional> and it looks a lot like what Rust looks like, we just have to consider that impl Fn(i32) -> i32 becomes the function pointer std::function<int(int)>.

#include <iostream>
#include <functional>

int duplicate(int x) {
    return x * 2;
}

int square(int x) {
    return x * x;
}

std::function<int(int)> compose(std::function<int(int)> f, std::function<int(int)> g) {
    return [f, g](int x) { return g(f(x)); };
}

int main() {
    auto double_then_square = compose(duplicate, square);
    std::cout << "Anonymous composition: " << compose(duplicate, square)(3) << std::endl; // Output: 36
    std::cout << "Named composition: "     << double_then_square(3)         << std::endl; // Output: 36
    return 0;
}

While both Rust and C++ allow function composition, Rust’s type system and functional idioms make the process more expressive and safer. The lack of null pointers and the Option type help avoid runtime errors. Also, we can easily make the compose function more generic with generics, which is not as easy in C++:

#![allow(unused)]
fn main() {
fn compose<T>(f: impl Fn(T) -> T, g: impl Fn(T) -> T) -> impl Fn(T) -> T {
    move |x| g(f(x))
}
}

Partial Functions in Rust

Partial functions are somewhat similar to function composition, but we define a function to a subset of the possible values that it can receive, as if it was using a constant value instead of a function. In this case, I want to extrapolate partial functions from Clojure's partial function.

Clojure's partial is a core language utility that allows to pre-fill arguments to a function, creating a new function with fewer parameters. Could say that it is quite common for testing Dal like structures that require a lot of configuration. So let's consider the following example:

(defn multiply [a b]
  (* a b))

(def multiply-by-three (partial multiply 3))

(println (multiply-by-three 4)) ; Output: 12

We have the function multiply, but we only care about the case where multiply has the first element set to 3, binding it as multiply-by-three from (partial multiply 3), then we can just call it (multiply-by-three 4). The caveat from Clojure side is that partial can only be applied to the first n arguments, while Rust gives you a bit more control over that.

The following Rust code is a translation of the previous Clojure code with a twist of having the partial applied to the second argument, demonstrating how Rust enables a bit more control over this:

fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

fn partial_multiply(b: i32) -> impl Fn(i32) -> i32 {
    move |a| multiply(a, b)
}

fn main() {
    let multiply_by_four = partial_multiply(4);
    println!("Result: {}", multiply_by_four(3)); // Output: 12
}

multiply function is exactly the same. However, the partial application of multiply, shifts which argument we are applying the partiality, as we create multiply_by_four applying partial_multiply to b, instead of a. Note that we can apply multiply_by_four multiple times and always having the guarantee that we will multiply by 4.

Use Cases of Partial Functions

  • Pre-binding Arguments: Useful when testing functions that require a lot of configuration, or event-driven architectures where some functions take repetitive arguments over and over.
  • Currying Simulation: Breaking down multi-argument functions into simpler, single-argument functions.
  • Code Reusability: Creating specific versions of generic functions, useful for simplifying test cases as well.

Functional Programming

dateDescriptionKeywords
2024-12-15Insights on Functional ProgrammingFunctional Programming, Insights, Guide

How can we start a functional programming-centred blog without talking about functional programming? Although I started programming with C++ version 98, I'm far from a fan of object-oriented programming. Back then, my code was mostly imperative and procedural. Same goes with Python, the second language I learned. Things started changing when I was doing my bachelor's in applied mathematics and my dislike for MatLab, which made me search for classes that were using anything but MatLab, which meant Haskell and Lisp. Lisp had a very interesting approach for a mathematician, as it used the same computational logic as my graphic HP calculator, so I would always aim for classes that were in Lisp (or, for some weird reason, Pascal).

Fast forward over a decade, and everyone that has personally talked to me about programming knows I am passionate about functional programming, and more so, I have experience making non-fp code bases become functional, as well as writing a lot of content about functional programming, especially in Rust, Java, C# and Clojure. So the passion is evident, but what are the benefits of FP?

  • Referential Transparency, with the use of pure functions your objects don't necessary own data and mutate their own state, you start having Instances/Namespaces/Objects that act on data passed to them, helping testability and providing a safer and more predictable way to understand the state of your data.

  • Immutability. At one point, I read in a Clojure book about the fact that if you have mutability, you know nothing about the state of your application, and so, you cannot make any predictions about its behaviour. This is not the case when you deal with immutable code, at any moment in time, when you take a snapshot of your data, you can clearly predict its next state based on x, y, z functions.

  • Composition and higher order functions are another key aspect and has a lot to do with a simpler way to deal with functions that contain side effects. By using composition, we can always guarantee that our code is predictable, a very interesting feature for testing. Let's use the general example of random data. I have a function that needs to define the initial direction something is moving, but that direction cannot be always the same, so we need random data. However, it makes it very hard to test, as we now have lost prediction of results. However, by passing a function that handles randomize data, we mock that function in test cases to return a desired value.

  • Concurrency. Off-course concurrency is not a functional programming restricted topic, but the previous attributes that we discussed, make it so much clearer on how to deal with it.

A few books that I can recommend for learning how to become functional thinking or how expand your fp insights are:

  • Clojure Applied by Ben Vandgrift and Alex Miller
  • Domain Modeling Made Functional by Scott Wlaschin
  • Adopting Elixir by Ben Marx, Bruce Tate and José Valim
  • Becoming Functional by Joshua Backfield
  • Functional Thinking by Neal Ford

Tech Debt

dateDescriptionKeywords
2024-12-10What is tech debt and how to handle itTech Debt Recognition, Tech Debt

Between "Do it quick" and "Do it right", I will always go with "Do it right", which doesn't mean over engineering. It means we should avoid deliberate technical debt, as Martin Fowler defines in the tech debt quadrants between reckless/prudent and deliberate/inadvertent (figure 1). This is different from inadvertent technical debt, which usually means a tech debt that we realized we had later.

Tech debt quadrants: Reckless and Prudent vs. Deliberate Inadvertent

Tests, in fact untested code, should also be considered a tech debt. I have, multiple times, in my career decided to deliver a "working software" to showcase the product before a well tested working software. However, once the proof of concept evolved, my team paid this recklessness cost later, and this was a larger cost than just delivering tested code from the start. So, my position is that untested code is deliberate reckless tech debt as we should have planned the test cases beforehand and implement the code as we test it.

Untested code is very common in game development, where people like to prototype untested games and evolve from there without adding tests. Also, people usually avoid at all costs automated test for gameplay, which 99% of the time is the cause of recurrent bugs. At some point, when people decide to add tests, it is usually just too late or too hard, meaning you a set of useless tests.

In short: Tech Debt is like a bad loan, the quicker we pay it, the less we suffer from it.