Rust for F# developers

Dark theme Light theme

1. Introduction

This document is a follow up for FSharpVsRust.pdf, which was meant as an comparison, but result was, that Rust already have most features than F# and quite a few of them were better in Rust. So let's take a look at what changed in the last two years, and add things I missed in the first attempt. I also changed my opinion about some of the things, like for example what is an character, and how does the languages represent it, and how to find it in a string (this will be huge improvement for F# over last time). So let’s start with my subjective comparison.

When you look at the source code of these languages, if you do not know either of them, you might mistake them one for another, because they have a lot of similarities. Rust will be the one that might have more braces (if the code will be about the same), and might be a bit more verbose, but I actually prefer that in most cases. Because without IDE’s syntax highlight and type hints (which both sometimes does not load in Barclays) F# looks like a wall of text, while Rust still carries structure thanks to braces, and types being required in function signatures. I will not lie, that the type signatures can not get too complicated sometimes, but more often than not they are helpful.

I created a scale F# is significantly better, F# is better, F# is slightly better, About the same, Rust is slightly better, Rust is better, Rust is significantly better, to use in chapters what I think about each point, so let’s see, much better will Rust end up.

1.1 What I learned, or wondered about, when writing this:

I put this on top for the interested readers.

2. Basic types

TL;DR Rust is better

2.1. Primitive types

I will go over the primitive types and their equivalents in the other language. If there are multiple names for the same type, I might use single or multiple rows and use the 3rd column as explanation. I will not repeat “F# inherited second name” in every row, so just keep it in mind.

F#

suffix

Rust

suffix

explanation

bool, Boolean

bool

byte, Byte

uy

u8

u8

sbyte, SByte

y

i8

i8

int16, Int16

s

i16

i16

uint16, Uint16

us

u16

u16

int, Int32

i

i32

i32

uint, Uint32

u, or ul

u32

u32

int64, Int64

L

i64

i64

uint64, Uint64

UL

u64

u64

nativeint, IntPtr

n

isize

isize

unativeint, UintPtr

un

usize

usize

Rust also have “*const T”, and “*mut T” that are actual pointers to generic type “T”.

float, double, Double

F, f, lf

f64

f64

F# have 3 names, and one of them is confusing because many other languages use float as 32 bit float.

float32, single, Single

LF

f32

f32

F# have 3 names

unit

()

In Rust unit is just empty tuple, in F# it is backed by a static class.

And now types that do not have exact mapping

F#

suffix

Rust

suffix

explanation

char

u16

u16

Microsoft incorrectly claims: “Unicode character values.”, but in reality it is only “UTF-16 code unit” so I am mapping it to u16.

uint, Uint32

u, or ul

char

Rust correctly describes it as: “Unicode scalar value (i.e. a code point that is not a surrogate)”.

decimal

M, or m

Decimal

Crate rust_decimal uses exactly the same 102 bits binary representation for decimal.

Int128

-

i128

i128

In F# This is not considered a primitive type.

Uint128

-

u128

u128

In F# This is not considered a primitive type.

!

The never type have no equivalence in as far as I know.

Both languages have tuples and array, but Rust considers those “primitive compound types”, which relates to their memory representation, but for F# developers starting with Rust that is an insignificant difference.

Both languages also have algebraic compound types, what F# calls Discriminated union Rust calls an enum. Rust also has an “unsafe” always untagged union, but that is mostly for C compatibility, so I will not get into too much details about it, and will not count it as a plus for Rust either. F# also has Option as a special type (as far as I can tell from the source code it is an actual language item), while Rust’s Option is a simple enum with cases None, and Some(T).

2.2. Structs

They are also called product types, the most significant difference is that Rust allows structs (and enums) to be empty. As far as I know F# does not have a concept of zero sized types. F# have also classes, but they are more or less equivalent to Rust’s `Box<T>` where T is a generic placeholder for the struct.

2.3. enums / Discriminated Unions

Rust’s enum can also be a number like F#’s enum, and the programmer that defines it, decides if all the values of the underlying number should be valid values, or only the listed ones, which means you can do exhaustive matches even on number like enums, if they are exhaustive (default) F#’s DU is always tagged, while Rust’s enum may not need tag, if the cases have an invalid values, that can be used instead of tag (for example reference can not be null/0, so Option of reference is same size as reference, and used null/0 as None case). For some technical reasons (and compatibility with C#) F#’s discriminated unions are backed by a static class, for .Net’s reflection to work, DU’s can not only need to have explicit tags, but also the variants need to be non overlapping, so the resulting size is actually the sum of the sizes of each variant plus the tag. Rust on the other hand allows the variant to be deduced, by using invalid states of the variants to eliminate the need for a tag, but also the variants can be fully overlapping. So if you have DU with many cases that holds a string (as an example of not small data, but of course it can be much worse than single string), you might actually be better writing a struct that will hold the string, and simple tag, because that will be approximately N times smaller, because strings are 32 bytes even if empty.

2.4. strings

          I skipped one type that F# calls primitive, because I quite disagree with it being a primitive type, when it does heap allocation, but let’s look at string, and what can be best Rust’s equivalent. F#’s strings are immutable, or more like Copy on Write, and replacing the object pointed to, kind of thing, could be a better description. Strings in .Net can be “interned” meaning that they are storing the data part in thread safe global (AppDomain) storage. `System.String.IsInterned` returns the string if it is interned, and null if it is not, so to get actual bool from a function that starts with `Is` you need to check the result for not being null. Another interesting fact is that .Net strings are 0 terminated. So what string types Rust have? That will be quite a list:

This might seem like quite a lot of different strings with similar purpose, but when you deploy software across the globe to 100 000s users, you will wish that being standard long time ago, because you will find out, that windows allows folder names to be invalid UTF8 (and UTF16) sequence of bytes, so having a path type, that will deal with that is important, and if you want to know what will .Net do with such paths, well the answer is simple it complains that it does not have an write permission, while the invalid symbol is not shown, because it does not know how to render it. And we will get to encoding differences later so do not worry, that it is not mentioned here.

When starting with Rust, do not worry about these types too much, you will get to them later, start with everything being `String`, when you will need paths use `std::path::PathBuf`, and as you go along, you will see a lot of APIs use `&str` for inputs, that it only reads, so you will learn how to do that over time, and maybe when the need will arise, you might already be ready to work with all these more specialized types.

There is a blog post detailing the .Net string size investigation so if memory footprint is important for you, you should really think about how you use strings.

I will skip over Functions, Closures, References, Traits, Interfaces, Attributes, and Exceptions, because I will get to them in a later chapter, and I do not see much of a reason to discuss them as just types.

2.5. Type summary

Type naming convention: Rust is better

Availability of basic primitive types: About the same

A way to handle any text (path) on any supported platform: Rust is significantly better

Not overwhelming users with too many type choices from the start: F# is slightly better

Type sizes, and performance trade off:

      Memory footprint: Rust is better (most cases)

      Performance: Rust is better (most cases)

      The other cases: About the same because Rust allows to make same types as F# have, including string interning.

Overall: Rust is better

2.6. Handling of text

TL;DR Rust is better

Here I will use an “simple” example to check if the string is palindrome or not (reads the same from both sides).


fn is_palindrome_ascii(string: &str) -> bool {
    
let bytes = string.as_bytes();
     bytes
      .iter().take(bytes.len() /
2)
      .zip(bytes.iter().rev().take(bytes.len() /
2))
      .all(|(&l, &r)| l == r)
}


This Rust example simply takes the bytes and iterates them from both ends taking only half the size from each side, but as you probably guessed, this will not work for all strings.


let is_palindrome_ascii (string: string) =
    string
    |> Seq.rev
    |> Seq.take (string.Length /
2)
    |> Seq.
zip (string |> Seq.take (string.Length / 2) )
    |> Seq.tryFind (
fun (l, r) -> l <> r)
    |> Option.isNone


The F# version does not look much worse. Rust is slightly better

But as you probably noticed these are for ascii strings, not for whole unicode, so let’s do that.


use unicode_segmentation::UnicodeSegmentation;
fn is_palindrome_unicode(string: &str) -> bool {
    
let g = string.graphemes(true);
    
let len = g.clone().count();
    
let r = g.clone().rev().take(len / 2);
    
let l = g.take(len / 2);
     l.zip(r).all(|(l, r)| l == r)
}
// and tests:
    
assert_eq!(true, is_palindrome_unicode("👨‍👩‍👧‍👦"));
    
assert_eq!(true, is_palindrome_unicode("👨‍👩‍👧‍👦 🫱🏻🫳🏿 👏🏽🫱🏻‍🫲🏿🫱🏿‍🫲🏻🫶🏻🫱🏿‍🫲🏻🫱🏻‍🫲🏿👏🏽 🫳🏿🫱🏻 👨‍👩‍👧‍👦"));
    
assert_eq!(false, is_palindrome_unicode("👦🏻👧🏻"));
    
assert_eq!(false, is_palindrome_unicode("🙇🏻‍♀️🙇🏻‍♂️"));


Not that much longer, and it handles the strings correctly.


let is_palindrome_unicode (string: string) =
   
let te = StringInfo.GetTextElementEnumerator(string);
   
let mutable graphenes = System.Collections.Generic.List<string>()
   
while te.MoveNext() do
           graphenes.Add(te.GetTextElement())

    graphenes
    |> Seq.rev
    |> Seq.take (graphenes.Count /
2)
    |> Seq.zip (graphenes |> Seq.take (graphenes.Count /
2) )
    |> Seq.tryFind (
fun (l, r) -> l <> r)
    |> Option.isNone


F# still does not look terrible, but some might already dislike the `mutable` variable, to which I say, show me a version without it. But what I do not like about the F# code is actually the fact that `graphenes` is a list of strings that are copies of substrings of the original text. This brings me to the same point I have last time, that .Net is using string, which is (thanks to padding) 32 bytes for short texts, but last time the comparison was between Rust’s `char` and F#’s `string`, which is still valid point, but I changed my opinion about how important that point is, because a single unicode value might not be enough to represent the whole symbol on the screen, which is called graphene. And when we compare the API to process graphenes they aren’t that different. I still claim that `&str` which is pointer and size (16 B) is much better than F#’s string which is a copy of the original data.

And because of that copy Rust is better even though F# is not as bad as I rated it last time.

3. Language stability

TL;DR Rust is significantly better

F# is part of .Net so many changes to .Net affects F# too. Lets look at .Net 8 as an example. There are 65 “Behavioral changes”, and 4 additional binary incompatibilities, if you will not find unless the application will actually run, and what worse, you might only find about them when it will run in a specific user environment. When Source code is incompatible, it might not be as bad, because if it will not compile you will not ship it broken, but F# does not require you to use types explicitly, so maybe it will still compile with some the 14 additional “Source incompatibilities”, and that Might be a problem.

Rust on the other hand uses RFCs that are discussed, and the whole community can read them in advance and join the discussion, if they feel like it. Most of the changes are new features, Rust is quite a bit afraid of breaking changes (maybe even too much afraid). So most breaking changes are announced over a year before they actually happen. Vast majority of breaking changes happen with editions, which is something like major versions every 3 years. Unless a critical bug is discovered, each change needs to be first implemented in the (stable) compiler for at least one release cycle, so 6 weeks (but such warnings are usually backported to stable, or beta, so more likely 12+ weeks). And the great thing is that when something is removed, the compiler will produce a useful error message for quite some time after. And I do not mean deprecation, that can happen too, and be removed with the next edition, but after that if you try to call removed function, you will get an error saying that it was removed, where to read more about it, and what to use instead.

Rust is quite proud of the offered stability. There is a saying, that people come to Rust for speed, but stay for the safety, and this is quite an important safety point for me, that I will not ship something broken to people just because I updated the compiler.  There is also a great way to test changes, before they happen. There is a beta compiler, that is exactly like stable, just 6 weeks earlier, but it rarely needs a fix or a change, during these 6 weeks, so it is relatively uninteresting, because there are no things that would not be fully ready. But the actually interesting is nightly compiler, as the name suggest it releases every night, and have all the unfinished stuff in, one example can be “async” (yes there was times before Rust have async, but that was long time ago)  wan in the works for over 4 years. Async was usable on nightly, where you still need to explicitly opt in for unfinished features, at least in 2016 (maybe even sooner, it is quite hard to find sources that old). In that time there were two bigger breaking changes, but they included syntax changes, so code did not compile, not that it would compile and not work correctly. Just a reminder for younger readers .Net did not have async for over 10 years, the last 2 of which there was an “extension” that added experimental support. So Rust getting experimental support before 1.0, and taking 4 years to stabilize, isn’t that bad.

With .Net I saw multiple breaking changes that affected production systems, and just second one “Custom converters for serialization removed” on the list for .Net 8, I can easily imagine, it breaking a production system somewhere, because that can easily slip through tests, and break something later in production, and that is only the list of announced changes, it would not be first time they break compatibility by accident. But so far I did not encounter even a single instance where Rust’s breaking change would break a production system, so Rust is significantly better.

4. Documentation

TL;DR Rust is significantly better

I know this seems like a weird point to some, but I actually promoted it from subchapter to top level chapter. F#’s documentation is almost non-existent offline, even if I consider the most basic thing, which are function definitions, they are often unreachable from the visual studio, it will open you a file, that does not contain any function (few examples: `WebApplicationBuilder.Services.AddSingleton`, `WebApplicationBuilder.Services.AddEndpointsApiExplorer`, `WebApplicationBuilder.Services.AddSwaggerGen`, `WebApplication.Environment.IsDevelopment`, `WebApplication.UseAuthorization`, `WebApplication.Run`). You might argue, that hovering over it will show you signature, and maybe even some comment, but if that works, it shows only that one, and not the other argument combinations, which might be what you looking for, it also does not allows you to search by approximate idea of expected signature, because there is just no where to look, unless you know the exact name. Rust has most of the documentation directly in the code, so you can navigate to the function, and the documentation is there. In the case of the standard library it also contains a lot of examples, but most libraries have examples too, just not that many in most cases. With F# it’s easy for the documentation above the function to be out of date, because it can change signatures, without any change to that file, which I think is more annoying than useful. With Rust these examples will never be wrong, because they are actual code that is compiled, so there will be a compiler error that will stop it.

As some people say, best documentation is the code itself, and as I mentioned above with F# that is often not an option, but Rust is distributed with source code, only libraries that link with binaries do not always have full source code available, but even then most link to open source libraries in another language, so the source code is often available in those cases too. Even paid software written in Rust, is sometimes open source, not saying often, but it is definitely much less rare than in other languages.

I have a funny story for you from about 2 weeks before I started to write this. In F# I wrote some code, and I needed to sum some fields on a records, on per field basis, and I knew there is an function on `Array` to do that, but because I could not find it by name, that I did not remember, I know it was more generic than `sum`, but less generic than `fold`, but not it’s name, I tried going over all the functions that IDE showed on `Array` module (I must have missed it, or the smart resorting feature moved it to past a point I already looked at), tried even a quick search online, but without a name no luck, without success, I given up, created a pull request with `fold`, with thinking that someone will point the correct function name to use to me. But that did not happen, and even better, what I got was so reinforcing this point about missing documentation in F#. I was told that there should be a function, that does that, without the need for the “initial element”. Well the solution to find the name of that function was to open the browser one again, but this not look for F#’s documentation but `rust doc iter` clicking first link and searching for `as fold` and at the time (std 1.76.0, in case it changes) it was actually exactly one on that page and it was the function I was looking for “For iterators with at least one element, this is the same as fold() with the first element of the iterator as the initial accumulator value, folding every subsequent element into it.”. That means Rust documentation saved two F# developers from not finding a better function. And when we are at it, let’s check signatures. Rust returns `Option<T>`, `None` meaning the iterator was empty . F# returns `’T`, and throws an `ArgumentException` when the array is empty and also can throw `ArgumentNullException` when the array is `null`(we will get to nulls and exceptions later). Do you already know the function name? Well if you did not figure it out it is `reduce`, and here is the link to it. And there is even an example comparing it to `fold`. And in this type every single function has at least one example.

Documentation availability: Rust is significantly better

Documentation completeness: Rust is significantly better

Documentation being helpful: Rust is significantly better

Documentation in 3rd party libraries: Rust is significantly better

5. Types and simple use cases

TL;DR Rust is better

Both languages claim to have immutable values by default, but only for Rust that is actually true. Any type coming from C# is mutable. And that might be a problem, that something you expect to be immutable changes, because it is a class. Even after all this time, being exposed to mostly single F# codebase I still feel like F# is pretending to be a purely functional language, where state does not exist, but at the same time being a weird hybrid, because of .Net legacy, having so many mutable things, but things that I see as useful to be shared mutable state like logger are by default supposed to be passed in as arguments. F#’s array is more comparable to Rust mutable slices, because they can not be resized, but elements can be modified. Ranges in F# either do not have a bound, or they include it, which is different from all other languages I used so far. Rust does this much better. `..2` is a range containing 0, 1, and `..=2` is a range containing 0, 1, 2.  Rust’s way is much better, because it follows the expectations, while allowing more options. As far as I can tell F# does not actually have an object representation and can not be assigned to variables, while Rust ranges are actual objects. I can make mutable Range in Rust or return a range from a function, which is impossible in F#.

Control flow is again better in Rust, because I can break out of the loop or return from the middle of function, and I do not need super complicated loop conditions to do something that `break` would do. In Rust I can even have a nested loop and decide from which one of them I want to `break` out. F# have computation expressions, but they can not be easily combined, so they can handle simple cases, but as I showed last time on the reading structured data from a file example, F# is not really suited for that. That example would probably end up a bit better thanks to the new `task` compute expression that can be used instead of `async`, but I do not think it would end up much better. Rust also has a function coloring problem, but I do not think it is as severe as combining different computation expressions. F# does not have a way to read a file, it calls C# so we will not get Result, but we need to catch exceptions.

Summary Rust is better, I can see that in theoretical code base that would be F# only, and having replaced some of the C# types, F# could be much closer, can not say if it would be close enough to call them equal, because it is too much “What if”.

5.1. Standard library

TL;DR It is complicated, and I am willing to say subjective in range F# is significantly better ..= Rust is significantly better

          Rust has quite a small standard library, which means you can not even generate a number using standard library, without writing the generator yourself, or using a 3rd party library. You do not have parallel iterators, so you need to write them yourself, or again use a 3rd party library. Async runtime/executor is also not present in the standard library (for more than a decade now) and you again need to write your own, or use a 3rd party library. From that you can say that F# is significantly better because it inherits all of that from the .Net (let’s ignore the fact that most of it is C#, and let F# have it’s win for a moment). But I will argue that Rust’s way is better, for 95+% of cases you can use rand for random number generation, rayon for parallel iterators, that span all CPU cores, and tokio for async runtime/executor. You may still be convinced, that F# is significantly better on this point, I will try to describe the difference between these 3 and their .Net counterparts. Libraries in .Net in most cases, depend more or less on the specific default implementation, which is not a problem, if your use case align with it perfectly, but that is almost never the case, but mostly the difference is not significant enough to even consider doing something different, if you are bit further apart, you probably consider how to bend your use case, and if you are even further, you will probably have strong consideration for other programing languages, that match the use case better, or even offer a choice, like Rust. Great thing about the 3 example libraries I mentioned above, is that they have an API that does not bind you to specific implementation, so all 3rd party libraries that use that API will work with your code too, if you decide to follow the same API, and those API’s are quite generic. But I picked those 3, because in the last few years I actually used their API’s without using their implementation. I needed to match an existing random number generator written in another language, to not change system behavior, but I could still conveniently use all the features of rand with my implementation, like generating a sequence, or picking a random element from an array. For rayon the API is actually just an `Iterator` from the standard library, so when I do interop with C++ binary I can just make it work by implementing `Iterator` trait, and all the things that work with std `Iterator` will just work, and same for rayon, and libraries that work with it.

To summarize the above paragraph the F# standard library is much more complete out of the box so F# is significantly better, Rust’s 3rd party libraries might be too much choice for newcomers so F# is better on that too, but in the end having compatible API’s and optimizations for everyone instead of a few selected implementations that do not fit well for many use cases is in my opinion a great win for Rust so Rust is better.

Next feature of .Net’s standard library is json serialization, and again Rust does not have this feature in standard library, so some might argue, that F# is better, but do you personally use `System.Text.Json`, or `Newtonsoft.Json`? These two are API incompatible, so switching between them means changing the code. And that is especially true for F#, where DU’s can not be correctly serialized and deserialized by either, without writing the code yourself, or using another library. So let’s look at Rust, and there we get 99.9+% of cases covered by `serde`’s API, and notice that I wrote API, not implementation, because there is serde, miniserde, microserde, nanoserde, and many more, but at least these I know have exactly same interface, so if you rename them in dependencies to `serde`, you do not need to change any other code. If they are replaceable, you might be wondering what is the difference, and that is a very good question, there are pages upon pages of analysis of this topic, so anything I write will be oversimplified. It is a trade off between compile time, run time and binary size. If you do not care about compile time, and binary size at all the choice is simple pick `serde`. If you on the other hand want to deploy to a device with less than 1 MB of RAM, you should definitely not pick `serde`, because it will generate different functions for every type, including all generics, and that costs space, but there will be no dynamic dispatch. Dynamic dispatch has non 0, but small runtime cost. `serde` also does not define how to serialize something to a specific format, it only offers a very generic framework to get all the fields, and there are libraries like serde_json, serde_json_fmt, simd_json, and many others. So you can replace significant parts of how serialization and deserialization works, with no to minimum changes to the code, just by changing dependencies. And you can make it so, that dependencies are chosen automatically depending on target platform, so when compiling for an server you get slightly bigger binary, with better run time performance using simd, but when deploying same code on an arduino client, it will automatically make the binary smaller, and avoid including simd, because the target will not support it anyway. So in my opinion Rust is significantly better.

Another thing is logging, .Net got an official interface quite late, so each logging library has its own interface, and only recently they started offering the alternative interface compatible with the .Net’s standard logging interface, but that is a bit too late, because all existing code bases already have a wrapper around it. Rust on the other hand never had logging in the standard library, and probably never will have. The fact that the standard library of Rust is small also means that it does not really need logging, compared to .Net’s standard library, that handles database protocols, and has the whole web server included. Rust has a log library that has existed since early versions, and provides a unified logging API. All the logging libraries for Rust I ever heard of use this exact API, and no other. So to enable logging on multiple platforms like Android, Linux with syslog, or Linux with systemd journal, or logging to db, or “simple” file, you just replace the logger dependency, and small code that initializes it.For asynchronous applications it was superseded with tracing, but that does not mean log is obsolete, tracing is only addition, and it is fully compatible both ways, so if there is an library that uses tracing, your application using simple logger does not need to care, it will still get the log events from it, and the other way around, if you depend on library that only uses log, you will get the trace events from it, and if you call it in async span they will be correctly assigned, so you will not lose any information. In .Net most of the logging APIs expect you to pass in the message, and even though Microsoft did find a way to fix it in their API. This fix was not very effective, thanks to so many wrappers that enforce a string. So in most cases the thing that may or may not be logged is evaluated, the string is formatted, and only then it is checked, if it should be logged or not, because the log level is disabled. Rust on the other hand uses macros for this, so the check if it should be logged or not always happens first, before any evaluation, so you can literally have database access in trace logs, and it will not affect production code, where trace logs are disabled. and even better, you can have a logger set up in a way that it will only counts how many times it was logged in production, without logging the data, and it will still not evaluate the database access. I actually set up something like this, because I wanted the log data to be very detailed, but only from a very small time frame, that I could not identify unless it actually ran and encountered a problem, so I make it so that trace logs are disabled until I say so at runtime. It was a huge help to not need to add any ifs to the code, find out that I need to investigate slightly different parts of the code, and move all the logs with all the ifs, I just did mostly normal logging, and only slightly changed configuration. So in my opinion Rust is significantly better.

But to not be only positive about Rust, I wish there would be an official set of libraries that are recommended as safe default options to do something specific, that will work well together, and cover common use cases people might come with to Rust. There are sites like “Are we web yet?”, “Are we async yet?”, “Are we game yet?”, “Are we GUI Yet?”, “Are we (I)DE yet?”, “Are we learning yet?”, “Are we in space yet?”, and many others, but as far as I can tell neither of them is officially maintained as part of Rust’s ecosystem. So while I believe more good options is better, I am not ignoring the fact, that not having to choose things too early may be beneficial in a short run, so F# is better.

Printing a value in Rust is quite easy, if it implements `Display` trait it can be printed, or if it implements `Debug` trait it can be printed using debug formatter, which is good enough for many use cases, even when displaying to users. F# have `%A` format specifier, that will print content of records, and discriminate unions correctly, but will not work for most reference types, and by not work I mean it will compile, but will not print correct output. The rules when the “%A” specifier will indent something, or not seems a bit crazy to me, some arrays will be horizontal, some not, new lines seem to be inserted semi randomly too. Rust’s formatting is quite consistent, and you can even decide on the caller’s side, if you want single line, or multiline expanded arrays, structures, and other similar things. I hope everyone agrees that for formatting things to strings, streams, and all other things Rust is significantly better.

5.2. Operators

If I counted correctly F# language reference symbol-and-operator-reference list 123 operators, but the precedence table have only 27 rows and does not list all the operators, and then there are user defined operators, as far as I can tell they do not have defined precedence over each other, if they have same first character, if they differ you are supposed to use the precedence table in the link above, but there is no explanation, what happens, when they have same first character, or if their first characters fall in same category. Rust has a much smaller table that defines precedence not only for operators, but for all expressions and that table has only 19 rows, it defines complete precedence rules, and there are no user defined operators. I would also like to point out, that comparison and range operators require parentheses, while F# claims to not require any. Rust’s stricter requirement for parentheses eliminates practically all cases, where precedence might be unclear, or unintuitive. This F#’s expression “1 < 2 < true <> false”, would require 2 pairs of parentheses and it would get rid of all ambiguity. Just to be sure, I will mention that parentheses are required, only if there is another expression. So while someone may claim that custom operators mean F# is better, I claim, that simplicity, and ambiguity means that Rust is significantly better.

5.3. Loops

          F# have only `for in`, `for =`, and `while`. Rust does not need special syntax for range based loops so in both cases it will be the same `for Something in` followed by anything that implements `std::iter::IntoIterator` which ranges do. `while` loop is the same too. But Rust has an additional `loop` that just loops without any condition. But also `while let` which you can use with anything that returns matchable object (commonly `Option`, or `Result`). And most importantly of all you can do `continue` and `break`, but not only that, if you have nested loops, you can use labels to select which one you want to break out of, or continue, which is very useful, when parsing complex formats. Rust is significantly better

5.4. Units of measure

F# has compile time only units of measure, but they can not be carried over to run time, not even to print the unit after the value, because that would not be compatible with the rest of .Net. While Rust allows crates like uom to be transparent wrapper over the numeric type, which allows truly 0 cost abstraction of the units away at run time, and for printing them, well that can be resolved at compile time, so you will get nice prints including SI prefixes if you decide to use those, but you can easily define your own units. This way also allows you to add meters, to centimeters, but the other way around only if the underlying type you choose supports fractions, so you can not add `u32` centimeters to `u32` meters, but you can add `u32` meters, to `u32` centimeters. Another disadvantage of F# way of doing units of measure, is that they work only with some primitive types, while `uom` does not care as long as you can implement few basic traits for the type, like Zero, One, PartialEq, NumOps (Add, Sub, Mul, Div, Rem), and a way to convert it with Conversion trait. So in my opinion Rust is significantly better.

5.5. Traits and Interfaces

          F# uses interfaces, and Rust Traits, but for most uses these two words can be used interchangeably. Both can inherit from multiple other interfaces/traits, and both can have generic types. So at first look you might not see a difference in functionality, only in name. If they often serve the same purpose, let's look at the differences.

      Static vs Dynamic dispatch

      Rust’s traits are always resolved statically, unless the programmer opts in for dynamic dispatch. Unfortunate fact is that this is one of the many reasons why malware authors switched to Rust. Static dispatch allows easily generating complex call graphs in the binary, so it requires more complex and more costly analysis to figure out what such binary does.

      F# always generate dynamic dispatch in the binary, and since last time Microsoft actually improved on the promise that JIT will optimize it, now you can easily end up with single if, to make sure, it is the expected type, and direct call, after JIT compilation, but on the other hand Microsoft is more promoting native build without this JIT step, so not really an improvement.

      So my rating here is Rust is slightly better

      Implementation place

      In F# you need to open the path to the interface before the call definition, and implement it in call body, and you have no way to implement an interface for class define before the definition of the interface, so your code that pretends to be single pass compilation needs to be weird mix of data defining files, interface defining files, followed by more data definitions that implement these interfaces, followed ,ore interfaces, that depend on these data, and so on, and so on… All the interfaces implemented on type need to be implemented in its body, so the type definition can easily be 1000s lines long, just because it wants to implement many interfaces.

      Rust enforces a strict rule that “if you own the trait, or the type you can implement that trait for the type”, That mean if you create new trait you can implement it for any existing type (you can do a blanket impl for not yet existing types, but that is much harder to get right). You can avoid the limitation by creating a wrapper type with one field, and implement the trait for the new type. This means that if you do not want to have all your implementations in a single file you can just do it. Just to add emphasis to this point you can implement new functionality for types from standard library / primitive types, like `Option<T>`, or `u32`.

      I am convinced, that there should be no doubt that Rust is significantly better.

      Generics

      In F# Interfaces can have generic parameters, and that is the only way. So a class can implement the same interface multiple times with different generic types, for example you can implement both `IEnumerable<string>` and `IEnumerable<int>` and depending on how you call it, you will get different types.

      Rust allows defining generic types on traits in relatively similar way, but the big difference is that traits can have associated type, which to follow the example above, you can have one implementation of the `Iterator` trait for a type, and that single implementation will have one type it iterates over in as associated type named `Item`. If you have a generic structure like `Vec<T>` it’s iterators will be also generic, and will use that generic type as the associated type `Item`.

      Because Rust allows also more limiting definition I think Rust is better

5.6. Functions

5.6.1.              Function signatures

          F# infers types for function parameters as all other places, while that makes the code shorter, I will argue that it makes the code less readable. F#’s `’a` to me means nothing, and I assume that it is most likely incorrect, because I can not think of a function for which an unbounded type would be useful. Unfortunately I can not say, if that is an generic tooling issue, codebase issue, or Barclays specific issue, but I am seeing `’a` quite a lot in IDE’s tooltips in function signatures, and only when I go to the function (if navigation actually works) it may or may not change to a more useful type information. I think the `let inline add x y = x + y` example I used previously is quite unrealistic, especially on Rust’s side I can not imagine adding two different types, and getting a third type as an result of addition, but I picked it as an worsts example where Rust’s explicit types will make the signature over twice as long. But I also included a more reasonable signature, where the tree types are all the same, and the difference was not that big there. I believe Rust is better, but I could understand why someone would say F# is better.

5.6.2.              Mutability

          F# does not have any good way to indicate, that function will be mutating its inputs, it it is an array, or any other C# based type, there is an element of hidden mutability in it. Rust does not have this issue, there is a pretty clear indicator if something is mutable or not in the function signature with the `mut` keyword. Of course there is an exception to it, you can have types, that have a safe way to change from multiple threads at same-ish time, few examples of such types are `Mutex`, `RWLock`, `AttomicUsize`, and other atomic numbers, these types provide a safe API to modify them, you need to lock `Mutex`, and `RWLock` for writing, and that will at run time ensure there is no other thread holding that lock, and Atomics have hardware backed operations, that ensure the ordering of operations on the memory. F# have atomic operations too, but that is more like a bad joke, than actual API you can call `System.Threading.Interlocked.Increment` on a variable, while another thread is using it in a non atomic way. Really what is the point of an API, that can not guarantee correct result? I mean sure on a small code base with only a few developers, you can probably remember to check all uses when doing a code review, but can not imagine ensuring everyone remembers that in a large code base with many developers. I say Rust is better.

5.6.3.              Currying

          Currying is one of things F# is significantly better at, but I can not say it is impossible to do currying in Rust, there is crate auto_curry, which is successor to the crate I used last time, to demonstrate it being possible. I still do not like currying and passing functions around, with data attached to them, because it makes debugging practically impossible. So even though F# is significantly better I do not think this is a significant advantage. But let’s recap how Rust can do it:


fn add(a: u32, b: u32, c: u32, d: u32) -> u32 {
    
return a + b + c + d;
}
fn main() {
    
println!("1 + 2 + 3 + 4 = {}", add(1)(2)(3)(4) );
    
let add_4_b_c_d = add(4);
    
println!("4 + 5 + 6 + 7 = {}", add_4_b_c_d(5)(6)(7) );
    
let add_4_7_c_d = add_4_b_c_d(7);
    
println!("4 + 7 + 8 + 9 = {}", add_4_7_c_d(8)(9) );
    
println!("9 + 8 + 7 + 6 = {}", add(9)(8)(7)(6) );
}


5.6.4.              Computation expression

Computation expressions are great thing in F#, but not because they would be a great thing in general, but because they allow you to return from a function whenever you want (well technically not from the function, but from the expression, which can be the only thing in a function). As I showed last time, using the macros, it is possible to make computation expressions like things in Rust using my public POC crate.


let option = option!(
     let! x =
Some(3);
    
let y = 7;
     x + y
);
dbg!(option); // x == Some(10)


But I would still prefer the closure version (if any at all):


let option = (|| {
    
let x = Some(3)?;
    
let y = 7;
    
Some(x + y)
})();
dbg!(option); // x == Some(10)


I agree that F# is better, but I do not want to say that it is significantly more useful than being able to use `return` in the middle of a function, or `break` in a loop.

5.7. Exceptions

          The unfortunate fact is that C# uses exceptions instead of proper error handling, and F# uses a lot of C#, so many functions can throw an exception in an edge case. My favorite one `OutOfMemoryException`, why F# needs such exception? Creating that exception requires memory allocation, throwing it requires even more memory, if you did not have enough memory for your operation, are you sure, you will have enough memory to throw this exception?

          Rust on the other hand does not use exceptions for control flow. Let’s look at the same thing “out of memory”, if you are writing a client application, it will be terminated, but what about kernel modules? Well it is reasonable to consider handling such exception there, but F# is not used to write kernel modules (I hope). Userland application crashing is bad, but if there is not enough memory, there is probably not much you can do anyway, but not handling it in a kernel would be bad, because that would take down the whole operating system. Rust exceptions are used to terminate applications in most cases, if you want to look, how they work, look for `panic!`. I have a code base that interacts with C++ a lot, and there I decided to handle exceptions, by logging and killing the process.

6. Complex use cases

6.1. Do Not Repeat Yourself in bigger code base

6.1.1.              Implementing same functions for multiple types (and only those types)


macro_rules! impl_id_functions {
     ($t:ty) => {
   
impl $t {
          
pub fn base_id(self) -> Self {
               
Self(self.0 % ID_MODULO)
           }
          
pub fn as_res_id(self) -> ResId {
                ResId(
self.0)
           }
          
pub fn upgrade(self) -> u8 {
                (
self.0 / ID_MODULO) as u8
           }

          
pub fn all_upgrades(self) -> [Self;4] {
               
let base_id = self.base_id().0;
                [
               
Self(base_id + 0 * ID_MODULO),
               
Self(base_id + 1 * ID_MODULO),
               
Self(base_id + 2 * ID_MODULO),
               
Self(base_id + 3 * ID_MODULO),
                ]
           }
    }

   
impl From<$t> for ResId {
          
fn from(id: $t) -> Self {
                ResId(id.
0)
           }
    }
     };
}

// and then using like this:
pub struct ResSpellId(pub u32);
impl_id_functions!(ResSpellId);

pub struct ResSquadId(pub u32);
impl_id_functions!(ResSquadId);


Full source code rust-libraries/cff/src/lib.rs#L127

6.1.2.              Simple parsing of any type and attribute macro

We create a trait and implement it for primitive types, and maybe some other types (just few examples)


pub trait Persist {
    
type Target;

    
fn parse(input: &[u8]) -> IResult<&[u8], Self::Target, VerboseError<&[u8]>>;
}

impl Persist for u64 {
    
type Target = u64;

    
fn parse(input: &[u8]) -> IResult<&[u8], Self::Target, VerboseError<&[u8]>> {
    nom::number::complete::le_u64(input)
     }
}

pub struct Vec16<T>(Vec<T>);

impl<T : Persist> Persist for Vec16<T> {
   
type Target = Vec<<T as Persist>::Target>;

   
fn parse(input: &[u8]) -> IResult<&[u8], Self::Target, VerboseError<&[u8]>> {
       
let (input, c) = nom::number::complete::le_u16(input)?;
        nom::multi::count(T::parse, c
as usize)(input)
    }
}

impl Persist for String {
   
type Target = String;

   
fn parse(input: &[u8]) -> IResult<&[u8], Self::Target, VerboseError<&[u8]>> {
       
let (input, str_size) = nom::number::complete::le_u32(input)?;
       
let (input, string) = nom::bytes::complete::take(str_size as usize)(input)?;
       
match std::str::from_utf8(string) {
           
Ok(string) => Ok((input, string.to_string())),
           
Err(_e) => Err(nom::Err::Failure(
                VerboseError{errors:
vec![(
                    input,
                    nom::error::VerboseErrorKind::Context(
"Invalid UTF8 string"))]}
            ))
        }
    }
}


Then we create an attribute macro:


#[proc_macro_attribute]
pub fn simple_persist(_args: TokenStream, input: TokenStream) -> TokenStream {
   
let mut struc = parse_macro_input!(input as ItemStruct);
   
let mut parse_statements = Vec::new();
   
let mut ctor_elements = Vec::new();
   
// We make every field type T into <T as Persist>::Target
   
match &mut struc.fields {
          Fields::Named(FieldsNamed{ named: fields, .. })
        | Fields::Unnamed(FieldsUnnamed{ unnamed: fields, .. }) => {
           
for (field_cnt, field) in fields.into_iter().enumerate() {
               
// Modify type
               
let ty = &field.ty;
               
// Add a statement and ctor element
               
if let Some(ident) = &field.ident {
                   
// Named
                    parse_statements.push(quote!{
let (input, #ident) = < #ty as ::persist::Persist >::parse(input)?; });
                    ctor_elements.push(
quote!{ #ident });
                }
               
else {
                   
// Unnamed
                   
let id = Ident::new(&format!("e_{}", field_cnt), Span::call_site());
                    parse_statements.push(
quote!{ let (input, #id) = < #ty as ::persist::Persist >::parse(input)?; });
                    ctor_elements.push(
quote!{ #id });
                }
               
// Modify type
               
let new_ty = quote!{ < #ty as ::persist::Persist > :: Target };
                field.ty = syn::parse(new_ty.into()).unwrap();
            }
        },
        Fields::Unit => {},
    }
   
// Constructor
   
let ctor = match &struc.fields {
        Fields::Named(_) =>
quote!{ Self{ #(#ctor_elements),* } },
        Fields::Unnamed(_) =>
quote!{ Self(#(#ctor_elements),*) },
        Fields::Unit =>
quote!{ Self },
    };
   
// Assemble
   
let sname = &struc.ident;
   
let result = quote!{
        #struc

       
impl ::persist::Persist for #sname {
           
type Target = Self;

           
fn parse(input: &[u8]) -> nom::IResult<&[u8], Self::Target, nom::error::VerboseError<&[u8]>> {
                #(#parse_statements)*
               
Ok((input, #ctor))
            }
        }
    };
   
//println!("{}", result);
    result.into()
}


And finally we use it:


#[simple_persist]
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct Ability
{
   
pub version: u8,
   
pub id: u32,
   
pub ability_line_id: u32,
   
pub priority: u32,
   
pub flags: u8,
   
pub faction: u8,
   
pub duration_steps: u32,
   
pub finishing_spell_id: u32,
   
pub icon: String,
   
pub buff_category_id: u32,

   
pub preview: Vec16<u32>,

   
pub ability_parameters: AbilityParameterContainer,
   
pub effects: EffectsContainer,
}


This was a relatively long example, full code: trait and base implementations, macro, uses.

Let’s get over it in more detail, now that we know the code a bit.

The trait defines single function that takes slice of bytes, and returns bit complex type, but I simplify it for you it is just an type alias for `Result` that have `OK` as tuple of slice of bytes (the input slice minus what was parsed from it), and the object that was parsed from it. The error variant is bit more complex, so lets say, that it contains a lot of metadata, about the context in which the parsing failed plus optional messages by developer that describe the error, and slice location at which part of the input it failed, and yes that is really simplification, but it is actually relatively easy to use, and when you parsing complex array of structures something like call stack is totally useless in many cases, but knowing it failed on 5376th element in the array is much more useful, especially, if you can get all the byte offsets, where last successfully parsed element from the top level array ended. And notice the associated type, it will be important later.

The base implementations for primitive types are literally single call, and yes it could be replaced with a macro, replacing only the type, and the function, but I guess no one was interested in replacing 55 lines by 14. There are few more implementations, `Vec<T>` reads 32 bit number which is how many elements follow, and the elements, `Vec16<T>` does exactly the same, but the size is only 16 bits, `String` is an text, and quite similar to `Vec` plus and conversion from bytes to UTF8 with validation, there is also `WString` which is UTF16 encoded. Notice that `Vec16<T>` have associated type `Vec<<T as Persist>::Target>`, and `WString` has associated type `String`, which means they do not map to themselves, when parsing.

Macro implementation is quite straightforward, we use macro to parse the struct, and match on its fields. We enumerate them, to have a number in case on unnamed fields, we generate parsing statement, and “constructor” statement for each of the fields, and then the magic happens we change the type of the field to their targets, which is exactly same type for most, but `Vec16<T>`, and `WString` gets changed, which is the reason why this can not be derive macro, because those only add implementations, and we needed to modify the fields, so there can be two string fields in a struct, that will have different implementation, because EA decided for some reason to mix UTF8, and UTF16 strings (and also local encoding, but that is not an issue here). Then we generate constructor, which is almost like F# but the name of the type is before the curly braces 'MyStruct{ fields }`, explaining that, just to make sure you do not imagine it as a function call, we use `Self` keyword, because we will be using it in an `impl` block. And finally we assemble the whole thing, the modified `struc` and the `impl` block. `struc` holds all its other attributes and derives macros that will be applied in order top to bottom, left to right, which mostly does not matter, but for this attribute it does, because it changes types, so it must be the first one to apply.

Last example code was a structure definition and apart from the attribute that changed the type of `preview` field there isn't anything special about it. I could have pointed out more complex macros I use like message_request_command, or template, but they are both used as attribute macros. I can not think of a good way, to show, how useful `#[derive(TRAIT)]` is because in Rust it is used everywhere, and even showing comparison how much code a single instance saves, will not really do it justice, because it is not only about not writing same-ish code over and over again, but also about ensuring correctness, the macro will regenerate to code, when you add a field to the structure, or rename it, while there is no way to ensure that any handwritten code will be correctly updated. Rust is much better on this too, that it does not compile if you do not initialize all the fields, if you taking fields you need to be explicit, if you want to be non exhaustive, but unfortunately there is now was any language can notice, that you missed a field when accessing them one by one.

6.2. How to parse complex strings

          Imagine you would want to write a function that takes a string, and based on the text it generates a filtering expression to filter cards collection. Bit outdated specification in BehindTheScenes_AdvancedFiltering on pages 7..=15, outdated, because there was few more filters added and most important of them is custom extensions, where user can define new filter combination, to have a shortcut that allows user to faster access their favorite filters, and more importantly it allows them to bypass the 255 character limit UI is imposing on them. The full code definition is this Expr type, that contains everything by what users can filter. I wanted to write it in F# to have another comparison, but I given up, I was not been able to find any easy to follow tutorial how to parse text with F# that may contain both prefix and infix operations, because that requires recursion, and I guess I was really missing the types how to combine the functions, I would compare it to jigsaw puzzle, where in Rust you just put the pieces together, but in F# all the edges are hidden from you so you need to guess the shapes, and if you guess wrong, you get totally useless errors about type mismatch, without any help.

6.3. Error messages

          Error messages are one of the best Rust features. But I don't really know how to convey the feeling, when you do a type, or any other mistake and the compiler tells you that in a clear readable way, and asks you if you did not mean something else with the code you wanted to write. I think you can get such a feeling only if it happens to you, when writing your code, and out of nowhere a totally unexpected error appears, that prevents you from making a mistake that would be hard to find otherwise and cost you many hours of debugging. I could repeat the error comparison from last time, that will show F# at best barely catches up to Rust error message quality, but mostly falls way behind. But I did not get the `almost_swapped` error in the last two years. I could use errors I encountered recently, but because how I feel about the `almost_swapped` error I do not think listing an error comparison would not convey the feeling I got when I seen the error stopping the compilation, I do not think any comparison of error message would show anything new, I hope everyone can agree that Rust’s error messages are better, and showing how incredibly useful they are would be nearly impossible, if not totally impossible, unless the reader experienced them, in which case there would not be a need for comparison, because such reader would just know.

7. Actual codebase example

7.1. Artificial Intelligence code controlling a player in a game

          After such a mouthful name, let's compare the source code. I will be using Rust code base, and C# (+ F#) code base, if you think you can write better AI to compete, feel free to join HERE. Because both code bases have additional things, that will not be too interesting for this comparison I will be pointing to lines and including links so you can follow along, copying here 4628 lines of F# would not be too readable, so I will point out only the important stuff, and not repeat the same point multiple times. SPOILER ALERT Rust is significantly better

7.1.1.              Data model definition

We start with api/src/lib.rs and F#-api-and-example/Types.fs, because Rust’s file includes much more things, and also does much more things, let’s start at lines 22, and 8 respectively, where we have a constant defining version of the API. Rust have `pub const` to say it is an public constant, while F# have only `let` because public is default, at the end of the line Rust have semicolon, that F# does not need, but it instead inferred the type of the value `16`, which F# for some reason can not do, even though it has the type explicitly named before the `=` sign. I have no idea, how F# can manage to work without types in function signatures, but require them on constants assigned to a known type. Rust is slightly better

Then we have an `Upgrade` enum on lines 24, and 10, Rust have derives, repr, braces, and commas, so you can argue that F# is better, but I disagree, even though Rust is slightly longer, it does not implement useless things for the enum, like Hash, but there is much more important point Rust’s enum have exactly 4 cases, while F# one have 4 named cases, and 4 294 967 291 not named cases, and for that reason alone I think Rust is significantly better, as far as I know F# does not have a way to have a number representing enum, that will be exhaustive.

Then we have a simple named number `CardId` on lines 34, and 18. F# comment is 3 times longer for consistency, and because xml tags look weird on a single line. Rust again has derives, and rep. But now we get to more interesting differences. Rust uses public struct with public not named field containing the number, while F# uses Discriminated Union with a single case that has the same name as the DU with the number. But now to the reason why Rust is significantly better for this simple type alias/wrapper. F# needs an implementation of `JsonConverter` to do a transparent wrapper.

Then there are a bunch of IDs so let’s look at the first one only `SquadId` at lines 40, and 30. I just compare it to `CardId`, because they are all similar, with one major difference, Cards have a special case, that says no card, and before you think `Option<CardId>` I will tell you that not all languages can do `Option` as nicely as Rust, and I needed some binary compatibility there, but do not worry we will get to some `Option`s. Rust does not use `u32` for these IDs, because they can not ever be 0. My judgment Rust is significantly better.

We skip over `CommunityMapInfo` and straight to `MapInfo` on lines 99, and 142. First thing to point out, is that F#’s line number is now higher than Rust’s, because the `JsonConverter`s in F# are larger than derives and braces in Rust. I will not subtract any rating for `JsonPropertyName` because I consider that a reasonable way to rename a field, Rust’s way isn’t much shorter (I think, I did not count the characters). Rust have `community_map_details` as `Option<CommunityMapInfo>`, while F# has `CommunityMapDetails` as `CommunityMapInfo voption`, both mean the non heap, directly in the object variant. And I am happy to say, that new versions of .Net account for `voption` in a way, that there is no need for custom `JsonConverter`, so Rust is slightly better because of the comments.

Next shortstop is `Deck` at lines 109, and 159, where I will note use that Rust uses `Cow<'static, str>` which allows to have either static string constant, or an owned string, which is similar to .Net’s string in a way, that you can think about the constant as in interned string, and owned one as a string constructed at runtime, not exactly, but close enough I guess. And F#’s inability to have a fixed size array, so Rust is better.

And our first multicase Discriminated Union is `AbilityEffectSpecific` on lines 343, and 392. The definitions looks almost identical, and F# people can complain about Rust’s braces, because F# needs also piles. But the real difference is that F#’s DU needs a converter, and for multicase DU it needs to be a beefy one. 186 rows of pure boilerplate code and even more does need to be written for longer DUs. In my opinion these `JsonConverter`s make it quite easy to assign a victory, so Rust is significantly better.

Next bigger one is `Aspect` and again F# would look nearly identical in definition, if the comments would not need xml tags, but what really kills it is the `JsonConverter`, so Rust is significantly better.

Summary for type definition Rust is significantly better, F#’s biggest weaknesses are DU needing custom serializers, and comments spanning 2 additional lines, for xml tags.

But I did not mention one thing that makes Rust even better, than that is `bot_api` attribute, which is used to generate the types file for other languages, like C++, Python, TypeScript, C#, and F#, so the F# file was mostly generated by these attributes.

7.1.2.              Bot examples

          And now we get to an example AI that spams a single unit and attacks with it. In Rust we have the nam on line 9 so top of the file, while F# have it on line 122, which is bottom of the file (yes it can be extracted to the constant, but that is not the point). Rust have the bot’s structure on line 11, while F# have it on line 121, and so far you can argue, that you can read the Rust’s file from the top, and F#’s from the bottom. And it will hold true also for a `decks_for_map` function. I decided that F#’s implementation does not need a prepare function at all. When I go from top to bottom in Rust’s file, I will find `match_start` on line 47, and `on_tick` below it on line 77. In F# `start` start is in the middle on line 63, and when you finish reading `start` you can not say “above it” because you are at the bottom part of it, and `tick` on line 26 is 60 lines up, compared to Rust’s 2 lines down, or directly below it, but we are still not at the top of the file. We can not read from the bottom, because functions have instructions from top to the bottom, so we need to go down, to find for what to look up, and then read it top to bottom.

7.1.3.              Do you think you make a better bot in a language of your choosing?

          Then do not hesitate and join HERE.

7.1.4.              Do you think F# is better than Rust?

          Then join HERE and write an F# alternative, that will be better, to prove it.

8. Advantages and disadvantages

          Let’s look at the old list and see if I changed my mind about any of the points.

8.1. Advantages of F# as found on the internet

          TL;DR About the same ..= Rust is significantly better

8.1.1.              Algebraic Data Types

Rust has them too, and I will argue that better. Rust is slightly better Same example as before NonZero integers.

8.1.2.              Transformations and Mutations

Again Rust make it easy to not make mistakes when doing mutations, while F# is limited by “rest of .Net” so Rust is better.

8.1.3.              Concise Syntax

I think F# white space aware syntax can easily be terrible, because it can produce invisible issues that can be solved only by removing all whitespaces and adding them back, which is annoying at best. I still prefer to know function signatures, without relying on tooling. And as I showed above, syntax is maybe a bit shorter in practice, but code is not (`JsonConverter`s…), so my judgment Rust is better.

8.1.4.              Convenience

          To me it is more convenient to write Rust, than F#, because there are so many small things that F# is missing, to be interesting for most use cases, and if it is not useful for most, it makes it less likely to be considered for others, because it is more convenient to reuse code in one language, than porting it to multiple. Rust is better

8.1.5.              Correctness

          I still disagree, because F# is depending on so many things on the .Net, which is C# first, which means nulls, and exceptions. Rust is significantly better

8.1.6.              Concurrency

          Still not sure if it was referring to .Net’s `Thread`s, or `Task`s, or `ValueTask`s, or F#’s specific `async` that is lazy compared to the previous 3 that executes eagerly. My decision Rust is better

8.1.7.              Completeness

          No change there .Net is biggest disadvantage of F#, but at the same time, the only thing that makes it viable. Rust is significantly better

8.1.8.              Pattern-matching

About the same

8.1.9.              Type providers

          Rust can easily create bindings for other languages, and serialize to many different and sometimes exotic types, while F#’s type providers have a lot of issues, the main one being wrong nullability guarantees. Rust is significantly better

8.1.10.          Sequential compilation

My 3 issues still stand, not being faster, not being actually true (because .Net needs multipass compilation, but can be bit limited in case of F# but not to single pass), and terrible readability. Call me a cheater, but for me incremental compilation is the base benchmark, and Rust is faster for me, might be because it does incremental compilation better, or because it is just faster, I don't really care, I do not see what should be the advantage for F# on this point. Rust is significantly better

8.1.11.          No Cyclic dependencies

          It is annoying that F# does not allow them, again `JsonConverter` to convert something you need a converter, but that converter needs to know the type, or work on any object and use ugly runtime reflection to construct the type, and to define which converter to use using attribute, you need that converter to exist before the type, because the attribute is placed above the type. Rust allows this on module levels easily, but on type level it is quite restricted, but might actually be useful to interact with some weird C code, like Linux kernel. Rust is significantly better

8.2. Disadvantages of F# as found on the internet

          TL;DR About the same ..= Rust is significantly better

8.2.1.              Naming is more challenging

          I still think that it is a good thing, and not a disadvantage. About the same

8.2.2.              More complex data structures

          I did not really changed my opinion on this either Rust is better at handling complex types, and probably uses more of them.

8.2.3.              Less advanced tools

          I changed my mind about this, but I do not have more space in my scale, so I rate it only Rust is significantly better. RustRover landed…

8.2.4.              Microsoft documentation is out of date, moved, 404

Hell yes I agree with that, it is maybe even worse than last time. Rust is significantly better I do not think I need to repeat, that old links to Rust’s documentation still works and are still useful. But I will repeat that even if Google (or whichever search engine you decide to use) points you to an old version, there will be a button to go to the latest, or any other version you might be interested in.

8.3. Rust advantages as found on the internet

          No need to repeat what was written above.

          TL;DR Rust is slightly better ..= Rust is significantly better

8.3.1.              High performance

          There are so many reasons why Rust is faster, even Microsoft is rewriting some projects to Rust. Rust is better

8.3.2.              Memory safety

I did not change my mind about a garbage collector being a worse option. Rust is better

8.3.3.              Amount of crates on crates.io

I changed my opinion here definitely Rust is significantly better because I do not count C# packages as a big advantage, more like liability.

8.3.4.              Community

          Rust is significantly better, I do not know any F# focussed Discord or forum with more than 3000 members (because the biggest I found have less than that).

8.3.5.              Backward compatibility and stability

Well I upgraded a Rust project that was using 6 years old dependencies, so I know my claims were true, and I still think Microsoft is unreliable on announcing breaking changes, and they for sure do not care about backward compatibility too much, because they push everyone to the latest version. Rust is significantly better

8.3.6.              Low overhead makes it ideal for embedded programming

I did a bit of that, so Rust works where there is no space for .Net, try measuring RAM with kB instead of GB. Rust is significantly better

8.3.7.              Rust facilitates powerful web application development

Deploying Rust to browsers is quite easy, web server also easy, but F# can do an easy web server too, with similar effort, until you need to receive, or send json data. Rust is better

8.3.8.              Rust’s Static Typing Ensures Easy Maintainability

          I still think Rust’s types are a bit more expressive and explicit. Rust is slightly better

8.3.9.              Cross-Platform Development and Support

Targeting Windows from Linux got even easier, while F# did not get any new targets, so I think Rust is slightly better

8.3.10.          Rust Has an Expansive Ecosystem

Rust is significantly better There seems to be no limit yet.

8.3.11.          Security

          Rust got to both Linux and Windows kernels, for this specific reason. Rust is significantly better

8.3.12.          Great error handling

I am again missing a point on the scale to show how much better it is. Rust is significantly better

8.4. Rust disadvantages as found on the internet

          TL;DR Rust is slightly better ..= Rust is significantly better

8.4.1.              Compile times

I feel like Rust is slightly faster on the bot example. Rust is slightly better

8.4.2.              Hard to get code to compile

I never had such issue with Rust, but I did have it with F#, so even though it might be personal bias Rust is better.

8.4.3.              Learning Curve and Development

No change I still see F# as Rust like syntax for C# without all the features so Rust is better.

8.4.4.              Strictness

I still see that as an advantage. Rust is better

8.4.5.              Rust is not a scripting language

F#’s `fsx`scripts are more annoying to work with, than having a simple one `fs` file application, that tracks its dependencies in a project file. If you want to test your small code, you can run it using the bot on Discord, or on Rust Playground directly. Rust is better

8.4.6.              No garbage collector

Nothing changed, I see that as an advantage. Rust is better

8.4.7.              Bigger binary files

          Simply no, and I have no clue, to what the author of that claim was comparing it, but 3 Rust bots in single binary are smaller, than 1 in F#, to get size under C++ you would need to try a bit harder, and either limit the amount of debug strings on Rust’s side, or add the same amount on C++’s side. Rust is significantly better

9. Questions

9.1. Can you cover using Rust in Enterprises, i.e. what support model there is plus does it cover the basic functions required to work in an enterprise i.e. Kerberos auth etc.

42 Companies using Rust in production I would point out Linux and Windows kernels.

I do not think there is any support model for F# neither, And for kerberos I do not think there was any change, so you should be able to do it, but I did not test any of the crates.

9.2. Coming from Rust, what do you like/don't like about F#

9.2.1.              Like

F# to me is still just a different syntax for C#, and it is good that it is similar to Rust.

9.2.2.              Don’t like

C# legacy, C# nuggets, stupid CLI naming rules, ranges always inclusive of right bound, “open”-ing by namespace, not by item, not integrated tests, things are public by default, no transparency, if object is reference, or an unintended copy, owned strings to represent text, graphenes and characters, no access to library code from IDE, even if it is from standard library, reflection is often chosen over compile time guarantees by library authors, no autocomplete for DU matching, and terrible autocomplete for type names, no autocomplete for creating instances.

9.3. What kind of 'mindset' does Rust teach its users? (for example F# I think teaches/preaches heavy domain modeling with DUs etc...)

To write correct code, using types to eliminate impossible states, and unlike F#, Rust has also never type.

9.4. How to do asynchronous call (users point of view)

Simply call the function and you get future (that did not start yet) back, then you can do something with it, but unless you await it (or pool it), or send it to an executor that will pool it, it will not start.

9.5. Do you „fight the borrow checker“

No, not even for UI anymore.

9.6. Generics a inheritance

Rust uses inheritance only for traits (interfaces with no data attached), and there are a lot of generic (and associated types) used in Rust.

10. Summary

F#

Rust

Better?

Documentation

Online only, hard to find, often outdated references, many things missing

Excelent, detailed, offline, in actual code, with tests

Rust is significantly better

Compiler errors

Often bad, and useless

Mostly great

Rust is significantly better

Platform support

.Net (x86, x64, arm64)

aarch64, i686, x86_64
arm, armv7, powerpc, powerpc64, riscv64, s390x
and many, many more, with sources available, so people can add even more

Rust is significantly better

Conditional compilation

yes it kind of exist

Great

Rust is significantly better

IDE, and tooling

yes, fine

Great

Rust is significantly better

Exceptions

Yes (and they are hidden everywhere, because C#)

Not used for control flow

Rust is significantly better

interop

Great with .Net, complicated

elsewhere

Great for anything with C

interface (including .Net if the

work on .Net’s side is done), there is even stable `stabby` ABI

Rust is significantly better

Concurrency and related issues?

Yes, but mostly trough C# with a lot of hidden mutability, and footguns

Great, with almost impossible to trigger an runtime issue

Rust is significantly better

3rd party libraries

yes, some, but everything else is C#

great

Rust is significantly better

Code generation

Yes, but too complicated

Yes with options for beginners, intermediate, and experts

Rust is significantly better

Logging

Microsoft still did not managed to standardize it

Great

Rust is significantly better

Hidden mutability footguns

YES C#

No

Rust is significantly better

Units of measure

Compile time only

Yes, and work great with 0 overhead at run time

Rust is significantly better

Loops

basic, with missing break and continue

unbunded and bounded, with labels for break, and continue

Rust is significantly better

Performance

Fine

Better

Rust is better

Primitive types

With multiple inconsistent names

With very consistent names

Rust is better

Determinism by not doing garbage collection at random times

Hard

Of course

Rust is better

Testing options

Many incompatible frameworks, and many of them are reflection based, because C#

Multiple options, all are an extensions to `cargo test`, or `cargo bench`, no reflection needed

Rust is better

Editions/Versioning

Hidden breaking changes

Compiler informs about most of the breaking changes

Rust is better

Data inheritance

Yes

No

Rust is better

Operators

Too many + additional user defined

For everything I need

Rust is better

Formatting objects to strings

Yes, unless C# that does not implement `ToString()`

Great

Rust is better

Function syntax

Type inference, useless without IDE, or tooling help to

All types REQUIRED

Rust is slightly better

Chaining calls

many operators

just `.`

Rust is slightly better

Generics

Yes widely supported (C#)

Yes widely supported

Rust is slightly better

Heterogeneous collections

Yes (always boxed)

Easy to do with `Box`es

Rust is slightly better

Visibility

Public (default)

Private (default)

Rust is slightly better

Debugging

Fine

Already better I would say

Rust is slightly better

Behavior inheritance

Interface default impl

Trait default impl

About the same

Currying

Yes

Possible, and does not look terrible

F# is slightly better

Computation expressions

Yes

Possible

F# is slightly better


If you would want me to add anything, just let me know.

11. Revisions

28/02/2024 - semi-manually make html version

26/02/2024 - initial version