Rust-like scripting language

The topic of a Rust-like scripting language comes up from time to time. While I think most people means something different when they say it, my experience learning Python for work the past 6 months made me realize it's basically like Rust's soulmate in the scripting languages.

While it is very narrowly tied to C, Python is also very opinionated about what is "idiomatic", has iterators and list comprehensions without being a full-blown functional language. It also has the core tenet of being explicit rather than implicit. Etc. Then there's also the little thing that it also uses self to refer to the object in class methods.

All this lets me feel right at home in Python code. It's easy to follow, because of the language's strong opinions about explicitness and simplicity. I think we can kind of see Python as Rust's scripting language. This is not to say that another scripting language with even more common DNA with Rust won't become the popular, de facto Rust scripting language, but for now, I find Python really similar and a good fit alongside Rust.

7 Likes

I know what you mean there but Rust does not have classes.

Personally I don't see the "soul mate" thing at all. What with Python being dynamically typed. That makes a language so very different in my mind.

Judging by the questions and answers I see going by here I would question the "simplicity" likeness as well.

4 Likes

I'd say Python and Rust are very similar in simplicity, at least. They both have super nice, concise ways to represent concepts, and both make those ways idiomatic. You mention the questions, though - I think that python and rust often also share some of the same problems where idiomatic code is easy to read, but hard to write. It can be very difficult to figure out the "right" way to do things in either language as a beginner.

The biggest difference I see is that when you don't do things right, Rust complains, and Python is often a toss up between runtime error and it just working (until the data your wrong code can't handle is passed in).

5 Likes

But I'd think scripting languages are dynamically typed by necessity. So what I meant, was from all the languages which are dynamically typed "scripting languages", Python feels the most similar to Rust.

I didn't mean Rust is also simple. I meant as someone not knowing Python, I could read it easily because the community likes to do things in the one idiomatic way instead of the 1000 ways C programmers like to obfuscate their code and show off how clever they are with bit manipulation tricks and pointer magic. In that sense Rust is similar.

I have a strong suspicion that is not true.

For example, I imagine one could create a scripting language with a normal kind of function definition and call syntax:

    fn my_func(x, y, z) {
        ...
        ...
        return z;
    }
...
...
    a = my_func(42, "Hello", my_object);

Here none of the parameter or return types is spelled out.

But the first time "my_func" is called at run time parameter types are determined and fixed as integer, string and whatever object structure. The return type is determined by whatever my_func produces and fixed.

Then if that function is ever called again with parameters/returns of a different type it fails with a type check error. This idea can be applied to many other situations, all elements of an array would have to be the same type as first used, and so on.

I am no language designer or compiler/interpreter builder so I have no idea if this is feasible or would perform well but I note that modern Javascript interpreters already do this internally. They will just-in-time compile optimized code for the data types in use, but if the program ever uses a different type in that place the optimized compiled code is thrown away and it reverts to interpretation.

Hmm... Not sure I can agree there. A while back I was converting some very simple C code to Rust and came up with this:

fn do_it_0(a: &mut T, b: &T) {
    for i in 0..MAX {
        for j in 0..MAX {
            a[i][j] += b[j][i];
        }
    }
}

During a discussion here various "idiomatic" Rust solutions were suggested, firstly to get rid of clippy warnings like: "warning: the loop variable j is used to index b", secondly to speed it up by removing the need for array bounds checks. The suggestions looked like this:

fn do_it_3(a: &mut T, b: &T) {
    for s in 0..BLOCKS {
        for t in 0..BLOCKS {
            a.iter_mut()
                .skip(s * BLOCK_SIZE)
                .take(BLOCK_SIZE)
                .enumerate()
                .for_each(|(i, row)| {
                    row.iter_mut()
                        .skip(t * BLOCK_SIZE)
                        .take(BLOCK_SIZE)
                        .enumerate()
                        .for_each(|(j, y)| {
                            *y += b[j + t * BLOCK_SIZE][i + s * BLOCK_SIZE];
                        });
                });
        }
    }
}

I rest my case.

1 Like

But I mean in Rust everybody generally agree on one way to do it. In C, you see lots of different ways of doing the same thing with mostly similar frequency. I mean, I've seen some hairy stuff in C abusing the fact that you can use anything, not just booleans, in an if condition. And it is the normal way of life in C. There's almost no community consensus about which way you should use non-booleans in if conditions (or not).

I think that what is perhaps the most central feature of Rust in my eyes is that it can catch so many mistakes at compile time, and I don't think this is possibly to replicate in a dynamically typed scripting language. There are certainly some things that are similar, but I don't really buy it.

@ZiCog Damn, that iterator is pretty ridiculous, and it doesn't even remove the need for bounds checks (see that for_each). It might be interesting to try and rewrite your loop to use iterators, but in this case I don't think there is a good way to do so, and if there isn't, that makes the indexed version more idiomatic. You can often remove bounds checks in loops with an assert at the start, though if T is a type that allows ragged arrays, that might be difficult.

I don't use clippy because of this kind of thing. I don't think idiomatic code should have to hide warnings.

That said, I'm sure you could replicate that iterator in Python!

9 Likes

I'm not sure two programmers will ever agree on the "one way" to do anything. Witness the endless debates, often heated arguments, that programmers get into over choice of language, formatting style, tabs vs spaces, braces vs white space, editors, operating systems... pretty much anything. Heck it's only recently main stream languages stopped having "goto" and still that debate goes on after fifty years!

I do agree though, it's easy to make even simple things incomprehensible in C. Even if you write it yourself!

And that is why we are here. I can write whatever gibberish I like in Rust but if it compiles it will run. Perhaps the results are not correct but it will not crash and burn or mysteriously produce random results for reasons that are almost impossible to fathom.

I'm new to Python, I find it pretty inscrutable.

1 Like

I'm glad someone agrees with me. People were very insistent on making that "idiomatic". Nobody could do it in a way that was not insanely obscure and slower.

I made a mistake above. The actual code I started with was a "loop-blocking" solution that is optimized to do the matrix transpose in a cache friendly way is many times faster:

fn do_it_2(a: &mut T, b: &T) {
    for i in 00..(MAX / BLOCK_SIZE) {
        for j in 00..(MAX / BLOCK_SIZE) {
            for ii in (i * BLOCK_SIZE)..((i * BLOCK_SIZE) + BLOCK_SIZE) {
                for jj in (j * BLOCK_SIZE)..((j * BLOCK_SIZE) + BLOCK_SIZE) {
                    a[ii][jj] += b[jj][ii];
                }
            }
        }
    }
}

Which clippy complains about: "needless_range_loop".

The fastest versions I have require "unsafe", which I don't like, or get clippy warnings and are still slower than C. If anyone has suggestions the code is here: GitHub - ZiCog/loop_blocking: Experiments in achieving C performance in Rust when manipulating arrays.

Generally I like clippy. But sometimes I like fast code better than "idiomatic".

Nowadays I work a lot in Python, and while it is nice and convenient for certain things, I wouldn't classify it as Rust's soulmate at all. Sure, the string formatting syntax was taken from Python. But the attribute format was taken from C#, and derive was taken from Haskell, and macros were take from Lisp and the list goes on.

Python annoys me in a lot of ways that Rust doesn't, and the philosophy of the two languages is just totally different.

Python is a lot more on the imperative side of the spectrum. It doesn't emphasize working with values – Rust is the only non-functional language I know that can work with values thanks to typed, checked ownership semantics. In python, anything non-trivial is an implicit pointer just like Java objects are, and this, coupled with allowing shared mutability, causes all sorts of disasters. So I have to constantly think about whether I have copied this object if needed, whether I can return this property or it would invalidate my own class' invariants, etc.

On the other hand, this fast-and-loose nature and especially the REPL is very helpful when working with irregular data (something I do a lot as a data scientist). I can't just fire up a Rust REPL and pull in that big ol' table of garbage that then automagically makes sense of its own God-knows-how-typed self. For that purpose, I'll surely use Pandas.

Incindentally, I don't think "scripting" has to do with "dynamically-typed", they look orthogonal to me. There are several statically typed scripting languages out there, too. For example, well… Type… Script.

2 Likes

Almost orthogonal. I can't think if a dynamically typed and compiled language. Although I guess it could be done. Has it?

JavaScript nowadays.

I don't count JIT. That is an optimization for interpreting the source not running a compiled binary.

Then what's the point? In what way does it matter if the executable code is loaded into RAM from a file or generated directly in memory?

1 Like

Does assembly count?

I mean, it's compiled to machine code and the type associated with memory can change depending on whether you want it to be an integer or a boolean, or a pointer to something, or a string, or whatever.

2 Likes

With compiled code I can keep the source you get the compiled executable. Not so with interpreted languages.

With interpreted code you need the entire run time and all the library source to run my code. with compiled code you don't.

Many JS run times do not JIT anything, they are just good old fashioned interpreters. I don't think the fact that some run time do JIT changes the category of the language itself.

But yes, point taken, these distinctions are all a bit woolly now a days. For example CERN has a REPL for C++ !

Currently I'm working on a scripting language called Moss. Beside the interpreter moss there is an experimental second compiler mossc, which targets the same bytecode, but additionally implements a type system.

For example, the compiler processes the following already, providing type inference, type checking and byte code generation.

let compose = |g,f| |x| g(f(x))
let f = |x| x+1
print(compose(f,f)(0))

Another example:

function filter[T](a: List[T], p: T->Bool): List[T]
   let b = []
   for x in a
      if p(x)
         b.push(x)
      end
   end
   return b
end

function qsort[T: Ord](a: List[T]): List[T]
   return ([] if len(a)==0 else
      qsort(filter(a[1..],|x| x<a[0])) + [a[0]] +
      qsort(filter(a[1..],|x| x>=a[0])))
end

let a = list(1..10).shuffle()
print(qsort(a))

The interpreter only accepts this:

function filter(a,p)
   b = []
   for x in a
      if p(x)
         b.push(x)
      end
   end
   return b
end

function qsort(a)
   return ([] if len(a)==0 else
      qsort(filter(a[1..],|x| x<a[0])) + [a[0]] +
      qsort(filter(a[1..],|x| x>=a[0])))
end

a = list(1..10).shuffle()
print(qsort(a))
1 Like

Possibly, maybe, if you squint hard enough.

One does not usually talk of "compiling" assembly language programs so the distinction makes no sense.

Thinks....

Actually I'd say no. If I write a procedure in assembler that takes a parameter in some register, which is is then treated as an integer, that same procedure is not going to do anything different when I pass it a pointer to a string in that parameter. Same like a compiled language like C. Not like an interpreted language like JS.

Wow, I didn't realize I'd trigger such a discussion! This is a case in point about this:

Luckily we didn't get to the heated arguments stage yet :wink:

I guess maybe it's just a case of, since I don't write Rust at work, the language I do write at work which I enjoy most because it feels similarly well thought-out, etc. is Python. The other options are JS (yuck, especially with how little I know of it), C/C++ on STM32 microcontrollers, PICs and even an ESP8266, or C++/Qt on desktop.

I have never understood the loathing people have for Javascript.

For most of my life I have used compiled languages, from ALGOL 68 to C++. Like all "real programmers" I looked down on silly scripting languages. They were hardly better than BASIC. Performed terribly, had no chance of deterministic timing and must be terribly buggy, what with the lack of types and all, and could not be used on speed/space constrained systems. Kids stuff, not for real software.

Around the year 2000 an unfortunate set of circumstances saw me working for a start up doing web development for a year or so. Oh my God, I had to get into PHP, Python and deal with the whole big wobbly mess that is the WWW. From HTML to MySql. I hated it. "What a swamp of bugs and security vulnerabilities is the world creating with this junk", I though, as it was later was come to pass and is still passing. When I escaped from that nightmare I swore never to have anything to do with web development again.

A few years back I needed to do some web dev. With trepidation I looked into how things had changed. I was amazed to find this new thing called webgl, behind an experimental flag in Chrome. Great I can do some neat 3D data visualization with that and THREE.js. I heard of this new websocket thing. Great I can push data in real-time to animate that visualization. Oh but I need to use JS, a dreaded scripting language.

I was stunned. JS turned out to be an even more sophisticated language than C++ in many ways. It has an event driven programming model that gets you away from all the problems of threads in C++ and performs even better sometimes. It has things like first class functions and closures, first time I'd even heard of such things. Not only that the new JS engines like V8 performed really well. In tests parsing XML streams I could out perform C++ !

And, at about the same time I saw Ryan Dahl introductory presentation of node.js. That was it. No more C++ for me. I could write "real software" in JS.

Oh and by the way JS runs fine on STM32 micro-controllers: https://www.espruino.com/ and is perfect for Linux based embedded systems that need to juggle events from networking, peripheral devices and the like.

Oops, sorry, that went on a bit.

Of course today I'm busy creating all that server side stuff that would have been C++ and node.js with Rust. Our remote devices run Rust applications. Only the client side browsers stuff is still JS, perhaps not for long....

5 Likes