Learning Blog [2]

Who needs Haskell? Straight to Rust Hell.

I like rust, everybody does. And there are multiple things to like about it; the type system, memory safety, safe concurrency, fast as f*ck, actually helpful error messages. All of that is great, and I believe almost every programmer would benefit from these features. But I’m no ordinary programmer that values efficiency and productivity, I’m the type of guy that wastes 6 hours coding in APL or Haskell just to see how elegantly I can solve a problem that I could easily solve in 3 minutes using Python.

You are a madman! that's what you are.

Maybe I’m mad. But I also have big dreams and determination. And one of my dreams is to be able to code with the abstraction level and elegance of Haskell, without all the roadblocks and inefficiencies that come with it. And I’m starting to believe that rust is slowly getting there. The engine behind this awesomeness is Rust's Generics. Let me show you some cool experiments I did with Rust in the past year that you might find cool. These examples aren’t revolutionary at all, I’m sure they look trivial to some experience Rustaceans. But I think they are cool, and maybe you will too. There is no overarching theme here, this is more like a small compilation of interesting pieces of code.

Generic function composition

If you have done any functional programming I’m sure you are aware of function composition. As a refresher, the composition of the functions f and g is defined as (gf)(x)g(f(x)). Look at this example, let f(x)=2x and g(x)=x1 then: (gf)(2)=3(fg)(2)=2 Simple enough. Now in Rust !!

rust
fn compose<T, Y, X, F, G>(f: F, g: G) -> impl Fn(X) -> T where F: Fn(X) -> Y, G: Fn(Y) -> T, { move |x| g(f(x)) }

Whaaaat?

This will seems either trivial or absolutely alien depending on your familiarity with Rust's Generics. But is not hard to understand at all it is just weird syntax. Let’s break it down. In the <T, Y, X, F, G> we are defining 3 Generic Types and 2 Generic Function Types. The F is a function type that takes inputs of the type X and outputs types T; then the G type function takes those types T and outputs types Y.

Now to test the generic compose function:

rust
fn f(x: i32) -> i32 { 2 * x } fn g(x: i32) -> i32 { x-1 } fn main() { let f_comp_g = compose(f, g); let g_comp_f = compose(g, f); assert_eq!(f_comp_g(2), 3); assert_eq!(g_comp_f(2), 2); }

This, of course, is an extremely simple example because both fn: f() and fn: g() have the same input and output types of i32, but that isn’t necessary. The only condition is that the output type of f has to be the input type of g().

Pipe function

Composition is at the core of functional programming, but more often than not a more useful representation of the same concept is the pipe function or pipe operator. In some programming languages like elixir or F# this operator is denoted by |>. The idea is very similar to classical function composition, but in some ways, it’s easier to work with, because you can think about passing or “piping” values from the left faction to the rights function. For example:

baz() |> bar() |> foo()

Turns into:

foo(bar(baz()))

Same idea as function composition, but it is easier to read and write. The rust code is very similar to the last one, but at the same time it is very different because it uses a new future of stable rust called Generic Associated Types or GATs that was stabilized this in October of this year with the 1.65 release.

rust
#![feature(type_alias_impl_trait)] trait Pipe<IN, OUT> {     type Output<G: Copy + Fn(OUT)->NEXT, NEXT>;         fn pipe<G, NEXT>(self, g: G) -> Self::Output<G, NEXT>        where         G: Fn(OUT) -> NEXT + Copy; } impl<F, IN, OUT> Pipe<IN, OUT> for F where     F: Fn(IN) -> OUT + Copy, {     type Output<G: Copy + Fn(OUT)->NEXT, NEXT> = impl Fn(IN) -> NEXT + Copy;     fn pipe<G, NEXT>(self, g: G) -> Self::Output<G, NEXT>     where         G: Fn(OUT) -> NEXT + Copy,     {         move |x| g(self(x))     } }

There is a lot to unpack here and this is supposed to be a short blog post, so I’m not going to dig deep. The idea is that instead of using a compose function, we make a pipe trait, and then implement such trait over F . One thing to note #![feature(type_alias_impl_trait)] in the first line, which means this code sadly isn’t stable yet. The feature Permit impl Trait in type aliases for now requires nightly, but in a very simplified way allows to use impl Trait not only in return types but in Type aliases or associated types; in this case the Output type. This is my most anticipated feature after GATs, and a lot of progress is been made to stabilize this.

I actually thought I knew rust until I read that

Yes, I know the syntax is not the most elegant, but the idea is simple. Let us see the pipe trait in action.

rust
fn double(x: i32) -> i32 {     2 * x } fn add_one(x: i32) -> i32 {     x + 1 } fn main() { let double_then_add = double.pipe(add_one); let add_then_double = add_one.pipe(double); assert_eq!(double_then_add(2), 5); assert_eq!(add_then_double(2), 6); }

That is cool. Don’t you think?. Now imaging using operator overloading to overload + or || to use instead of .pipe. But not even I’m crazy enough to challenge the Rust Gods in that way. I’m already pushing it too far.

Generic arithmetic

Types are great for writing correct and maintainable code. But I would lie to you if I say I don’t enjoy the agility that languages like Python or even MATLAB offer for quick and dirty numeric scripts. And I believe that is a big factor for its popularity. I firmly believe Rust can dominate a lot of fields of software development, but to do all the small data science scripts or small simulations I do with NumPy almost on a daily basis in Rust seems really far away.

Do you just say you enjoy Matlab?

One aspect I’m ashamed of enjoying is generic arithmetic, In languages like this you can create an integer a a float b and then add them like is nobody’s business a + b, and the language will automatically cast the result to float. Of course in Rust you will get rust_errors cannot add f32 to u32. And this is the correct way of handling the addition of two different numeric types if you care about preventing bugs and being efficient. But sometimes I just want to do the math as I do it on the whiteboard.

And guess what? you can actually kind of emulate generic arithmetic in rust in a very simple way:

rust
use std::ops; fn add<T,P>(x: T, y: P) -> T where P: Into<T> , T: ops::Add<Output = T>, { x + y.into() } fn main() { let a: u32 = 5; let b: f64 = 0.5; assert_eq!(add(b,a), 5.5); }

he “Generic” add function takes two inputs x,y of type T and P respectively, where P has to implement the Into<T> trait i.e. can be cast into type T; and the type T has to implement the addition operator ops::Add trait. Quite simply, the drawback of doing it this way is that you can do add(b,a) but not add(a,b) because you can’t cast an f64 to a u32. But maybe you can think of a workaround.

You can easily do the same for all other arithmetic operations. But as far as I know, it is not possible to use operator overloading with these functions because the ops::Add trait is already implemented for all numeric types. And that is probably for the better.

Conclusion

Maybe the lesson here is that with enough effort the rust type system is powerful enough that you can write code in almost every coding style. But that doesn’t means you should, there are very good reasons to use idiomatic rust. Nevertheless, I do see a future where we would be able to get the best of all worlds; and for me, Rust is the frontrunner in this old but eternal dream.

"Acceptance is usually more a matter of fatigue than anything else."
- David Foster Wallace.