Rust for JavaScript Developers – Functions and Control Flow
sheshbabu.com
sheshbabu.com
Rust made me worry about a lot more than I wanted to. Threads, locking, mutability. What was 80 lines of:
1. open USB device, get handle
2. start a WebSocket server, accept clients
3. proxy incoming USB device traffic to all WebSocket clients
4. proxy incoming WebSocket traffic to USB device
in node.js turned into battles with dyn FnMut traits to pass "callback/event" handlers around, spawning threads to not block on libusb reading / WebSocket client reading, borrowing, cloning, mutex locking, reference counting with Rust like you can in JavaScript.
You can't just "move variables from one scope into callback/lambda" scope with any sort of ease in Rust.
Rust is “move” by default. Passing ownership of a variable to callbacks requires making sure you have the correct types along the way, especially if you want to mutate. It can be easier in many cases to only deal with “owned” data in Rust at first. Clone a lot, and so on.
I can understand the move from C or C++ to Rust, the safety guarantees can be compelling. And it does feel like a much more modern language. However, if something with GC like C# can get the job done, I'd choose that every time over Rust.
But a lot of the extra effort in writing Rust code is about moving runtime errors to compile time. It's like static types - more effort writing it but you don't spend ages debugging typos at runtime. I've had way more instances of spending several hours writing complex code and having it work first time in Rust than in any other language. In C++ it's so rare it makes me extremely suspicious when it happens (did I actually recompile?), whereas in Rust I'm almost starting to expect it.
If you were talking about C, perhaps.
Modern C++ has a strict type system if you don't walk around it.
Nevertheless, the borrow checker can help with bugs that otherwise could be hiding, which is the actual advantage over C++. But those are not the kind you see right away after running your program again.
Again, I think you are talking about C's type system, not C++'s.
Have you tried `move || {...}`?
Therefore the example could be simplified to:
fn calculate_tax(income: i32) -> i32 {
if income < 10 {
0
} else if income >= 10 && income < 50 {
20
} else {
50
}
} fn calculate_tax(income: i32) -> i32 {
match income {
0..=9 => 0,
10..=49 => 20,
_ => 50,
}
}
https://play.rust-lang.org/?version=stable&mode=debug&editio...¯\_(ツ)_/¯
Rust code:
let foo = if x <= 0
{
0
}
else if something_else > 50
{
9000
}
else
{
42
};
And I miss this in Swift.The ternary operator is fine on its own but I’m not a huge fan of nested ternary operators. With a bit of whitespace it's somewhat readable still but still not as comfortable and easy to read correctly.
Swift code:
let foo = x <= 0 ? 0 :
(somethingElse > 50 ? 9000 :
42 )
One way of writing something that is more similar to the way I'd write it in Rust would be to make a closure and then run it.Swift code:
let foo =
{ () -> Int in
if x <= 0
{
return 0
}
else if somethingElse > 50
{
return 9000
}
else
{
return 42
}
}()
But it's a bit annoying still, both to read and to write. Also, even in the current version of Swift (Swift 5), the compiler cannot infer the return type of the closure on its own even though all of the branches return an Int, so I'd have to explicitly annotate that as I have done in the code above.I guess for a lot of people they would just make foo mutable and write the code as
Swift code:
var foo = 42
if x <= 0
{
foo = 0
}
else if somethingElse > 50
{
foo = 9000
}
I concede that this is probably the most readable out of all of the three Swift code samples in my comment. But the point is that I didn't want foo to be mutable. I wanted to assign a value to it based on some simple logic and have it be immutable. Cond1 // if this
? Result1 // return this
: cond2 // else if this
? Result2 // return this
: cond3 // else if this
? Result3 // return this
: ResultElse // else return this firstPossible ? thenThis
: secondPossible ? thenThisOtherThing
: thirdPossible ? thenThisThirdThing
: defaultThing
Always fun to see how people treat ternary nesting. They still feel a bit naughty to me. The terseness is sort of unbeatable though. Cond1 // if this
? Result1 // return this
: cond2 // else if this
? Result2 // return this
: cond3 // else if this
? Result3 // return this
: ResultElse // else return thisInstead of:
int salary = isEmployed() ? 2000 : 0
I prefer:
int salary = 2000 if isEmployed() else 0
func calculate_tax(income):
return 0 if income < 10 else 20 if income < 50 else 50 let foo = if is_employed() { 2000 } else { 0 }Nested ternary if statements scare me from my PHP days, and noticed inlined closures can at times be harder to grok at a glance.
What's funny is that expression oriented languages translate really nicely to stack VMs. My hobby language compiles down to WASM and it was almost trivial to code gen if expressions.
let foo: Int
if x <= 0 {
foo = 0
} else if somethingElse > 50 {
foo = 9000
} else {
foo = 42
}
That way you get immutable foo and the compiler will shout at you if you forget to assign the value in one branch. It's not quite as nice as an expression, but it's less punctuation than the closure and the result is just as safe. let foo = match () {
_ if x <= 0 => 0,
_ if something_else > 50 => 9000,
_ => 42,
};fn calculate_tax(income: i32) -> i32 { if income < 10 { 0 } if income < 50 { 20 } 50 }
on edit: tried to fix formatting but didn't work.
fn calculate_tax(income: i32) -> i32 {
match income {
0..10 => 0,
10..50 => 20,
_ => 50,
}
}
See this playground for a couple other alternatives using match too https://play.rust-lang.org/?version=nightly&mode=debug&editi...As others explained, you do need a default case (either an 'else' in your if statement, or an exhaustive match), otherwise how would the expression typecheck?
For a concrete example, if you use an 'if' only, you can have the following:
let x = if income < 10 { 0 };
At that point, there's no way to statically know x's type. If income is >= 10, x is what?In order to convince the compiler x has a statically knowable type, the "if" expression need an else statement, or should be converted into a match that the compiler can prove is exhaustive.
https://play.rust-lang.org/?version=nightly&mode=release&edi...
Def recommend it as a second / third language
:)
Edit: On hindsight, I should've picked a simpler example that doesn't deal with references.
I am a C# guy and every time I see modern Rust, C++ or Go syntax I am really surprised by its deviation from the norm (which is interestingly no longer C but a hybrid between JS/Java/C#). I mean why different Rust syntax for closures and functions. || vs ().
On the other hand, the linage is not Java/JS/C# to Rust but C/C++ to Rust. Considering that, it is an improvement.
I'm sure that code that's easy for machines and humans to parse is harder for humans to parse than code where humans are the first class citizens.
Rust’s philosophy has been to avoid that, and make it simple to parse, because that helps tooling and people alike.
Most people don’t see why this is a big deal. Here’s why: humans parse code when they’re reading it in a very similar way to how computers do. This holds true of natural language parsing, too, and is much better documented in that industry. If it’s hard or takes longer for a computer to parse it, it’s very likely to be hard for a human to parse it.
If you have fat arrow syntax, you need to skip ahead on the line to find it to confirm that what you’re looking at is a closure. Pipes say from the very start “this is a closure”. This could be said to be why Rust uses the `let` keyword too, which is theoretically unnecessary (though you might need something to replace it in a small fraction of cases). Not only does it make parsing way easier for machines (in a way that makes extending the language grammar later much easier too, but that’s part and parcel of the parsing philosophy), it makes the intent of the line immediately clear to humans.
But maybe I should take a step back, considering my limited Rust knowledge :)
We don’t just use a compiler or interpreter any more. We also use editors and refactoring tools and debuggers and profilers and style checkers and source formatters and static safety analysers and diff tools and… If the developers of these tools don’t have to worry so much about the mechanics of parsing the source, that leaves more resources to spend on making each tool useful.
To some extent you could achieve the same benefit by making a library available to parse the source and return some sort of annotated AST or similar data structure. However, unless you can provide an easy way to call that library from every language someone might want to use to write a tool, avoiding unnecessarily complicated parsing still seems advantageous.
For instance, VS Code already uses language servers to support autocomplete/refactoring for languages like Python and they're pretty zippy (I used to have concerns about speed, but in practice it's a non-issue). Those same language servers are also supported in Vim, emacs, etc. The editor itself doesn't need to know anything about the underlying language. And it looks like a Rust language server exists:
In fact, to take the idea further, the goal of the Roslyn [2] project is to expose APIs to the compiler itself in order to provide services to external dev tooling. Imagine a third-party generic debugger being able to tap into compiler or runtime internals and provide services around them it without explicitly knowing anything about the underlying language.
[1] https://en.wikipedia.org/wiki/Language_Server_Protocol
[2] https://en.wikipedia.org/wiki/Roslyn_(compiler)#Architecture
I think there is a wider issue here than the (still very useful) scope of LSP, though. If you are building a tool that depends heavily on the semantics of the source language, beyond common operations like “go to definition”, you might need more than a standardised language server can provide, and then you’re back to the position I described before.
You're right, at this moment, the LSP is circumscribed in that it doesn't expose the AST (even though it could) which means one is limited to the capabilities it does expose. The claim is that the goal is parity across languages and exposing the ASTs is contrary to this [1]. I'm hoping this philosophy is challenged. In the mean time one might have to rely on languages themselves exposing their ASTs, eg. Python is able to expose its ast (via the ast module).
So yes, right now one wouldn't be able to write say an IntelliJ IDEA type IDE based off a language server alone -- one wouldd have to be able to create the AST oneself.
[1] https://github.com/Microsoft/language-server-protocol/issues...
There are two, actually. That one, and https://github.com/rust-analyzer/rust-analyzer which is quickly supplanting it.
The intention is that the compiler will eventually be a language server. Someday.
Most of the designers have spent a long time writing C++, and C++ is famously extremely hard to parse for machines, and this has caused a lot of problems for compiler/language authors. It's normal for people to overcorrect when they have real trouble with something.
> I'm sure that code that's easy for machines and humans to parse is harder for humans to parse than code where humans are the first class citizens.
I'm not sure at all about that. Making syntax that is easy for machines to parse mostly means that your syntax needs to be wholly context-free and low-lookahead. Both these features help people too, even if they result in more different kinds of characters in your source code.
But in Rust, closures are different from functions, though they all implement the Fn, FnMut or FnOnce traits. Functions don’t close over any state (so you can cast them to a 'static function pointer), while closures can. Using the fn keyword for closures would thus muddy the conceptual waters—though you could argue that the names of the Fn* traits has already done that—and potentially hinder future language design.
I feel a stronger argument is that it’s also markedly longer and more visually noisy; `x.map(|x| …)` is seven characters shorter than `x.map(fn x => …)` and eight than `x.map(fn(x) => …)`, and to some extent words are more distracting than symbols.
Pipes isn’t perfect, but given the consistent philosophies of the Rust language, I am not aware of any better option. But it’s definitely a fairly subjective matter.
https://blog.appsignal.com/2018/09/04/ruby-magic-closures-in....
> Rust has something similar and they are called “Closures”. The name might be a bit confusing (...)
This is in turn confusing to me!
I can't imagine a world where you know JS and especially "functional" JS, but haven't heard of the term "closure".
"Arrow functions" are just sugar. They don't have any special meaning. All functions in JS, regardless of whether they are declared, assigned, returned etc., are closures and capture their environment.
Similarly a normally declared function in Rust are first class values as well and for example can be passed into higher order functions.
Giving the benefit of the doubt, I assume that the author knows this and just wanted to introduce the term in a beginner friendly manner. But I think it would be less confusing to just use the term and link to an official documentation:
Rust: https://doc.rust-lang.org/book/ch13-01-closures.html
JS: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Clos...
This statement is incorrect. There are important differences between arrow functions and regular function.
> An arrow function expression is a syntactically compact alternative to a regular function expression, although without its own bindings to the this, arguments, super, or new.target keywords
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
You can refactor an arrow function into a specific regular function but you usually end up with something more verbose.
Or in other words you can express all things "arrow" before it existed. Which is exactly what we did before they were adopted. And we still in fact do, just automated by transpilers like Babel[0].
But more importantly, in the context of the article: all functions in JS are closures, regardless of how you write them.
[0] https://babeljs.io/docs/en/babel-plugin-transform-arrow-func...
Fair enough, but you are probably alone in doing so.
Not only this is a bad idea from a programming perspective, or a teaching perspective, it is also a bad idea from a taxation perspective. In terms of taxation, what you suggest would result in [at least] civil penalties.
If you are going to create examples of code that manipulates currency, at least take it seriously and think about what would actually happen to people if they decided to use your code. Otherwise, use another concept to illustrate your example.
Using i32 for currency calculations is probably a bad idea (although there are situations where it would be OK), but this has absolutely nothing to do with javascript - it's no worse in js than it is in rust, since javascript fully supports all i32 numbers. In fact, it has a number of bitwise operators that operate on i32 numbers, and a bitwise operator that even works on u32 numbers.