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
'sEDN
format support forRust
- Edn-derive, derive macros for edn-rs
- Brcode, Pix QR Code parser in
Rust
with FFI versions inDart
,Clojure
,Typescipr
,Java
.
- I worked on integrating transfers (Pix) and payments to checkins account. The technologies I used were
- 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
.
- I worked on online services for an unannounced production.
- Thoughtworks as Tech Lead:
- Microservices migrations. Notable works are:
Java 7
+Spring
+MySQL
service toRust
+MySQL/Cassandra
microservice, which had a huge cost reduction and increased performance.Java 8
+Spring
service toClojure
+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 toJava 8
+Spark
+Spring Boot
microservices using a functional programming approach.Ruby on Rails
monolith toElixir
microservices.
- SRE, developed a state of the art system to monitor the state of the website.
- Natural language processing service written in
Rust
that scrapped social media to detect user comments on problems the werbsite had. - Using
sitespeed.io
scripts, we actively navigated the web site detecting latency, flow issues, inconsistencies and crashes. - Created a
kubernetes
clusters that held all the data and had a good developer experience for service developers to integrate and manage.
- Natural language processing service written in
- Lead Studio XR Researcher creatring prototypes to showcase in
Unity
andVuforia
:- AR Pokemon inspired card game.
- Luggage analyzer and trip visualizer.
- VR Airplane replair mechanical training.
- Microservices migrations. Notable works are:
I also wrote a few books for APress and Casa do Codigo:
- 📖 Functional and Concurrent Programming in Rust - Casa do Código
- 📖 Lean Game Development - Edition 2 - English - Apress
- 📖 Lean Game Development - Edition 1 - English - Apress
- 📖 Lean Game Development - Portuguese - Casa do Código
- 📖 TDD For Games - Casa do Código
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:
- WooriDB - Time Serial Database
- Brcode - PIX QR Code parser
- Space Editor - Bevy Engine Game Editor
- tokio_retry2 - Extensible, asynchronous retry behaviours for tokio
- Observable Trees - Fully tokio async channeled trees, no extra deps
- Bevy Chess Game - 0.14
- edn-rs - Rust EDN (de)serializer
- edn-derive - Rust EDN macro (de)serializer
- Transistor - Rust CruxDB Client
- Translixir - Elixir CruxDB Client
- Elixir EDN libs: Eden and exdn
- Hiccup - html templating macro in Rust
- Composing functions with Rust
- Exploring FFI between JVM and Rust
- DiammondSeek - Simple game in Java
- Rust Exercism Solutions
- TDD on Unity - Random Game
- Kotlin Exercism Solutions
- Mario Kart Multiplayer Demo in Unity 5
Function Composition and Partial Functions in Rust
date | Description | Keywords |
---|---|---|
2024-12-20 | Function Composition in Rust | Functional 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, ascompose(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
date | Description | Keywords |
---|---|---|
2024-12-14 | Leetcode - problems 121 and 122, Best Time to Buy and Sell Stock in Rust | Leetcode, 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
date | Description | Keywords |
---|---|---|
2024-12-11 | Leetcode - problem 88, merge sorted arrays in Rust | Leetcode, 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:
- Check if the first
m
elements are0
, if they are, just replace them withnums2
. - Create an iterator for
nums2
. - Loop over
nums1
, and if thenext
element innums2
iterator is less of equal to the current element innums1
, insert it in the list and pop the last element. - 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
date | Description | Keywords |
---|---|---|
2024-12-20 | Function Composition in Rust | Functional 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, ascompose(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
date | Description | Keywords |
---|---|---|
2024-12-15 | Insights on Functional Programming | Functional 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
date | Description | Keywords |
---|---|---|
2024-12-10 | What is tech debt and how to handle it | Tech 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.
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.