Why fnm was rewritten in Rust
Tags: rust, fnm, rewrite
Published: 2021-04-06 17:35:45

---
title: Why fnm was rewritten in Rust
published: true
date: 2021-03-05 00:00:00 UTC
tags: rust, fnm, rewrite
canonical_url: https://gal.hagever.com/posts/why-fnm-was-rewritten-in-rust/
---

This blog post was in the “drawer” for a long time, so I thought it would be nice to just publish it.

* * *

[It wasn’t very long ago](../windows-long-paths-in-rust), when I mentioned I’m rewriting [fnm](https://github.com/Schniz/fnm) in Rust. At first, I was just experimenting in order to learn the language: fnm is kinda wide in its scope — works with the file system, network and user input, and I know exactly what to test. After a couple of days experimenting with Rust I had some insights, comparing it to Reason.

Reason and Rust feel very close, as if they were family. I guess that most of it comes from having ML-style type system with C-style syntax. Rust is usually treated as a lower-level language (it is), and as a very verbose one, because if you compare it to higher-level languages like Reason, you find yourself much more explicit about what you want, like passing references and static strings, etc. But seems that while you still think about memory more, Rust provides great ways to abstract knowledge and build higher-level components to your system.

## The Trait System[<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#the-trait-system)

I think one of Rust’s biggest strengths is its trait system. AFAIK, it is very similar to Haskell’s typeclass, Swift’s protocol extensions, and they’re very similar to Java’s interfaces, with a unique superpower: You can implement your trait on 3rd party struct — including the standard library.

That means you can implement a custom trait on `std::fs::File`, or implement a standard library trait to your type, like `std::default::Default` or `std::fmt::Debug` (both can be simply derived, more on that in a bit).

Reason and OCaml do not have anything like traits. You can use functors and almost get with it, but it’s not 100% there.

Seems that using traits can get you very far and provide great higher-level APIs that preserve type safety. Check out [`structopt`](https://docs.rs/structopt), a derive macro that allows you to use the [`FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html) trait on any type and parse command line information from it. That means that you can declare ANY TYPE you want and use it as a CLI argument, parsing it from the user input. Do you want to use a URL? Luckily, `reqwest::Url` is implementing `FromStr`.

In fnm, we also get `UserVersion` which is some kind of a version requirement: it can be the system version (literal `system`), it can be an alias or a semantic version `MAJOR.MINOR.PATCH` optionally prefixed with `v`, where MINOR and PATCH are optional too. In fnm, we implement `FromStr` to `UserVersion` and let `structopt` to handle the parsing errors and print a nice message to the user.

This is one of the ways the trait system allows to build better software simpler, comparing to [Cmdliner](https://github.com/dbuenzli/cmdliner) which is a great utility but was VERY hard for me to use and reason about.

## Deriving[<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#deriving)

Another great Rust feature is the `#[derive()]` annotation. Similar to some Reason/OCaml PPX extensions, when put on top of a data structure, it can generate code for you in compile time, _that will be type-checked_. Rust comes with some deriving macros out of the box, like `Debug` (which allows you to pretty-print with the `{:?}` format string or `dbg!` macro), `PartialEq` and `Eq` for equality checks, etc.

The Rust crates embrace this type of macros, and awesome libraries use it to create higher-level abstractions:

1. [Serde](https://docs.rs/serde) allows you to derive a Serializer and Deserializer for your `struct` or `enum`, allowing you to serialize or de-serialize these data structures _safely_ into various formats, like JSON, YAML, TOML, RON…
2. [StructOpt](https://docs.rs/structopt) allows you to derive a [Clap](https://docs.rs) configuration from your data structures. This is amazing, because it removes so much boilerplate, and co-locate the CLI definition with the data structures.
3. [Snafu](https://docs.rs/snafu) allows you to get a great developer experience for error handling, by allowing you to create error `enum`s and define the error messages next to their declaration. It also provides some nice macros to `ensure!` that something works, and some extensions to data structures like `Option` and `Result` for converting them into the meaningful errors you have defined.

## The Community[<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#the-community)

Last year I visited Chicago to attend ReasonConf US. I talked there to great people like [Jordan](https://twitter.com/jordwalke), [Sander](https://twitter.com/sander_spies), [Peter](https://twitter.com/peterpme) and [Patrick](https://twitter.com/ryyppy). It was tons of fun, but also made me realize something I didn’t understand before.

The Reason community was divided — OCaml vs Reason, Native vs BuckleScript vs Js_of_ocaml. Being a small community, we should invest in providing the best tools and experience, and when we can’t provide the best experience if we’re not focused.

When writing an HTTP server in native Reason you might lack a library for New Relic, or Bugsnag, or Sentry. Stack traces are not really a thing. Using Node as a mature VM is very appealing — and if you need something that performs better — you can write it in a different language. Realistically, people ship Node apps all the time, so the performance and safety guarantees Reason provides should be enough.

In fnm, I couldn’t really understand how to use the `tar` opam package. This is why fnm used to shell out to `tar`, to extract the Node versions. This would obviously fail on Windows, but fnm didn’t work on Windows anyway, and I had no idea how to make it work in OCaml. In Rust, it is very easy to do conditional compilation for a specific configuration, so there are some differences in the Windows vs \*nix versions of fnm now — especially regarding:

- The installation files: Node.js is distributed in `.tar.xz` for \*nix binaries, and `.zip` for Windows binaries.
- The simple “shell inference” fnm has, to know on what shell type you’re using, is using `ps` on \*nix, and `wmic` on Windows. Both share the same return types so the module abstracts away the differences.

## Documentation[<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#documentation)

Rust, being a newer language, has some niceities to it, like centralized documentation on [https://docs.rs](https://docs.rs). It’s an amazing experience, having all the documentation for every crate on one site with a familiar theme. I know both [Ulrik](https://twitter.com/ulrikstrid) and Patrick are very into it and I hope they’ll succeed doing so.

Another cool feature is the doctests which is something that I knew from Elixir and Rust supports it natively too. When you write code in your docs, `cargo test` will test it. It’s that easy. No more unmaintained README or other documentation. This is the future.

These are two things most languages should learn from.

* * *

Overall, feels that since the (big) Rust community is focused around the native toolchains (and not compile-to-JS), makes it easier to build native, performant CLIs in Rust compared to Reason. I no longer have to find out environment variables to compile C libraries with specific flags, because Cargo has feature toggles and I use it instead. I build an ARM-compatible binary to distribute for those running fnm in their Raspberry Pi’s. We now fully support Windows, so no more gatekeeping for people with different OS choices.

All without pounding my head against the wall.

Except from the build time, which is much faster in Reason.