Writing Javascript in Rust ... almost

This is weird. JS fairly recently got "const" and "let" as superior replacements for the old "var" for declarations. Here I am busy changing to "const" to "let".

What is this all about? Well I just though I'd spend a moment reimplementing a little JS function in Rust. It was amazing how it went:

The JS code:

destinationPoint(distance, bearing, radius=6371e3) {
        // sinφ2 = sinφ1⋅cosδ + cosφ1⋅sinδ⋅cosθ
        // tanΔλ = sinθ⋅sinδ⋅cosφ1 / cosδ−sinφ1⋅sinφ2
        // see mathforum.org/library/drmath/view/52049.html for derivation

        const δ = distance / radius; // angular distance in radians
        const θ = Number(bearing).toRadians();

        const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();

        const sinφ2 = Math.sin(φ1) * Math.cos(δ) + Math.cos(φ1) * Math.sin(δ) * Math.cos(θ);
        const φ2 = Math.asin(sinφ2);
        const y = Math.sin(θ) * Math.sin(δ) * Math.cos(φ1);
        const x = Math.cos(δ) - Math.sin(φ1) * sinφ2;
        const λ2 = λ1 + Math.atan2(y, x);

        const lat = φ2.toDegrees();
        const lon = λ2.toDegrees();

        return new LatLonSpherical(lat, lon);
    }

The Rust version:

     fn destinationPoint(&self, distance: f64, bearing: f64, radius: f64) -> LatLonSpherical {
        // sinφ2 = sinφ1⋅cosδ + cosφ1⋅sinδ⋅cosθ
        // tanΔλ = sinθ⋅sinδ⋅cosφ1 / cosδ−sinφ1⋅sinφ2
        // see mathforum.org/library/drmath/view/52049.html for derivation

        let δ = distance / radius; // angular distance in radians
        let θ = bearing.to_radians();

        let φ1 = self.lat.to_radians();
        let λ1 = self.lon.to_radians();

        let sinφ2 = Math::sin(φ1) * Math::cos(δ) + Math::cos(φ1) * Math::sin(δ) * Math::cos(θ);
        let φ2 = Math::asin(sinφ2);
        let y = Math::sin(θ) * Math::sin(δ) * Math::cos(φ1);
        let x = Math::cos(δ) - Math::sin(φ1) * sinφ2;
        let λ2 = λ1 + Math::atan2(y, x);

        let lat = φ2.to_degrees();
        let lon = λ2.to_degrees();

        LatLonSpherical::new(lat, lon)
    }

Spot the difference? Not much is there? I could likely translate the whole library with some simple find and replace.

Not shown is a little "Maths" struct so that the use of sin, cos, etc hardly need changing at all.
And the magical "#![feature(non_ascii_idents)]" to get those Greek symbols in there.

14 Likes

I like it

1 Like

That's something I like about languages which are based on C's syntax. At work I had some complex C++ code for doing geometry math and could copy/paste it into my C# project and it compiled with minimal tweaks.

Rust can be a bit harder because match has different syntax to switch, but the translation is still pretty mechanical as long as you are only working with primitive types and operations.

Being able to use unicode identifiers is also pretty handy when you are translating math into code. Things like θ, λ, and ω all have standard meanings and writing statements with the unicode symbols instead of spelling it in english can turn a 100-character statement into 30 characters while also making the code more understandable to someone familiar with the problem domain.

2 Likes

… and less to everyone else. It's the same reason Wikipedia mathematics pages are unusable for anyone who hasn't studied math. You basically need a cheatsheet for all math symbols and a lot of them have multiple meanings depending on the topic. If you don't add an explanation for the weird symbols in your code, I'd immediately reject this as a code reviewer.

Back to the actual topic: Do you compile the Rust version to WASM, afterwards to replace as much JS as possible with Rust? :grin:

1 Like

I sympathize with your objection to weird mathematical notation that casual readers are unlikely to understand.

But in this case all those "weird" symbols are just local variable names. All of them only the names of temporaries within the function. Hence exactly as meaningful as using "lat_1" and "lon_1" instead of "φ1" and "λ1". Or any other names.

Anyone who has ever been exposed to trigonometry in high school maths should be comfortable with the use of Greek symbols.

Conversely, anyone who has not been exposed to high school math is not even going to begin to fathom what goes on in there anyway.

I see no reason for this to be rejected at code review.

Unless you have a better way to write it?

Ha! That is a wicked question. :slight_smile:

As it happens I need to do all kind of geo transforms and calculation on the server and in the browser.
Until such time I can drive an API like THREE.JS or babylon.js from Rust doing these little things in Rust is not going to help.

4 Likes

Oh, I forgot to say. There is so much "weird" syntax in Rust already. What with closures and lifetime tick marks and all the rest. I hardly think using non-English variable names should be an issue. Especially in such limited scope.

1 Like

How is this different from a programmer using variable names in their native non-Roman-font language? If the variable names were in Arabic or Hanzi or Thai script, would you expect to read them? Or would you discourage programmers from using variable names in their native language just because you couldn't read them, even if that was the most effective way for them to communicate with their own colleagues?

1 Like

I think it this depends on who is the intended audience. When I write physics simulation software, it's never a goal for someone who doesn't already understand statistical mechanics to understand my code. I expect that it's common to expect developers to understand the problem domain before understanding the code. So using standard notation seems like an unambiguous win in my field.

4 Likes

Well, now you remind me...

Back in 1983 I was contracted by Northern Telecom (Later Nortel) in England to do some work on the BIOS code that they had for a PC compatible system they were selling.

Turned out that the hardware and hence the BIOS they were dealing with came from the then unknown company NOKIA in Finland.

Turned out the variable names and comments in all that code were basically in Swedish.

Let's just say that tackling thousands of lines of x86 assembler in Swedish was a challenge.

As such I don't see a few common Greek letters used in equations where they are expected to be seen as an issue.

But now we have the possibility of non-ASCII identities in Rust we can expect to see all kind of Rust crates written in all kind of languages that are not clear to the English-only world.

Welcome diversity !

2 Likes

You had to change const to let, because Rust const and JavaScript const have totally different semantics.

With an additional #![allow(nonstandard_style)], you could've used a mod Math { ... } instead. That way, nobody would accidentally instantiate your zero-sized struct.

2 Likes

Thanks. Using mod Math .. is a buch better idea.

In case anyone is wondering the JS I'm turning to Rust here comes from the "geodesy" JS library:
https://github.com/chrisveness/geodesy
https://www.movable-type.co.uk/scripts/geodesy-library.html

In Rust the 'geographiclib' crate is likely a better idea though.

https://geographiclib.sourceforge.io/html/

2 Likes

Well, actually const in JS is not equal to let in Rust:

JS:

Rust:

That is because all objects are wrapped in an implicit Rc<RefCell<T>> in javascript.

Very true.

But in the context of the code I presented it is plenty close enough as we are dealing with numbers not objects.

Rust:

    let number = 2.1;
    number = 3.2;
    //error[E0384]: cannot assign twice to immutable variable `number`

JS:

    const number = 2.1;
    number = 3.2;
    // Attempting to override 'number' which is a constant

You mean like this:

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
pub struct MyStruct {
    x: f64,
    y: f64,
}

pub fn mutate_my_struct (s: Rc<RefCell<MyStruct>>) {
    s.borrow_mut().x *= 10.0;
    s.borrow_mut().y *= 100.0;
}

pub fn main() {
    // Not mutable s
    let s: Rc<RefCell<MyStruct>> = Rc::new(RefCell::new(MyStruct{x: 1.0, y: 2.0}));

    // But let's mutate it anyway!
    mutate_my_struct(s.clone());

    println!("{}, {}", s.borrow().x, s.borrow().y);
    // 10, 200 
}

Whoah! Mind blown. What happened there? We have mutated a thing with no sign of a mut anywhere!

Shamefully after a year of writing Rust I had never read the chapter on Rc and RefCell. Until now. Never knew I needed one.

Sorry if that is a crappy example of using Rc<RefCell<>>.

1 Like

Yes, exactly.

This is why you’ll see some commenters here referring to & and &mut as “shared” and “exclusive” references rather than “immutable” and “mutable” references. The actual guarantees are:

  • While an &T reference exists, the T will not be dropped or moved.
  • While an &mut T reference exists, all access to T will go through this reference, and it won’t be dropped or moved.
1 Like

Sounds reasonable.

I think of “immutable”/“mutable” and “shared”/“exclusive” as interchangeable. The one implies the other, or vice versa, in my mind.

But there you are talking about something being dropped or moved. Where did the ability to mutate the actual values at the end of the references, whilst they are alive, come from when there is no mut anywhere?

(moving my edit to a new reply)

Interior mutability is all about providing safe ways to alter data that you haven’t been guaranteed exclusive access to, which must involve some kind of protection against anyone else seeing an intermediate state:

  • Atomics use special CPU instructions to maintain consistency
  • Cell only allows its value to be swapped out, but not inspected
  • RefCell keeps track of accesses and panics if it would reach an unsafe state
  • Mutex and RwLock use advanced synchronization techniques to work cross-thread.

The more pedantic answer to your question is UnsafeCell: It’s a marker for the compiler that its contents might change through an & reference, and all of the safe types above use it internally.

1 Like

That all make sense to me and is as expected.
Oddly enough I have a good sprinkling of Arc<Mutex<>> in the codes I have written in the last year. No probs.

But still I'm not seeing where the permission to change the variables came from.

What if I did not want to allow changing one or more or all of them?