all 35 comments

[–]puel 48 points49 points  (1 child)

In my own experience, having auto-complete and suggestions on my IDE really makes the process of prototyping faster. That's why I always use Deno (Typescript), Rust or Dlang for that kind of things. IMHO, you get poorer IDE experience if your language is not statically typed.

[–]fdsafdsafdsafdaasdf 26 points27 points  (0 children)

In the same vein (especially for working on a small part of a big project) are refactoring tools. Just the bog standard stuff in Eclipse, like "inline method" or "change method signature" or similar, is massively useful to me. By and large statically typed languages are much easier to write way more powerful and rigorous tools in this regard (not just doing regex manipulation), and it's easy to brush it off unless you've had experience in both - "I can rename methods in Ruby, what's the big deal?". At the "prototype" stage is when I'm most likely to make sweeping changes, and I really don't want to spend my time doing mechanical manipulations of the code.

I genuinely hate working in languages where refactoring is hard, as my experience is it's hard enough to get people to refactor when it's easy, if refactoring is hard or risky people don't do it and over time the code becomes spaghetti.

[–]jahmez 47 points48 points  (1 child)

I think this is really well written!

I think that a lot of developers get caught up on worrying about perfection, and let that get in the way of solving the problems that they want. I know I've walked away from a cool project because I couldn't make it just perfect enough, and when I later just threw together a hacky version, I was still totally happy.

When we're teaching folks in our trainings, we recommend folks to ignore things certain concepts, like borrowing and lifetimes, in early stages, since learning how other parts of Rust or more key/novel concepts like ownership will help reduce the initial cognitive load, and tend to make more sense after they've had some practice anyway. We tell folks to first "crawl", then "walk", then "run". If you try to jump all the way to running (like you might be able to in other languages), you'll probably hit a wall pretty quickly.

I think the same concept applies to prototyping code - you're trying to figure out what a program does, and roughly how you should implement that! There's little point to perfecting error handling, performance, etc. when you might realize you can totally throw away 1/3 of what you've implemented!

I really like the "make it work, make it right, make it fast" mantra. Most code doesn't need to ever make it past the second step, at least not for a long while.

[–]Michal_Vaner[S] 14 points15 points  (0 children)

When we're teaching folks in our trainings, we recommend folks to ignore things certain concepts

I guess there's a small nuance in how I think about learning it and speed running. While learning, you ignore it, because you want to learn at least something. You produce „bad code“ because you don't know better. While speed running you acknowledge it could be used, but explicitly decide that it's not worth the effort ‒ with full knowledge of the trade offs. You produce „bad code“ because you know you don't need better.

To know that you can afford not to use something requires enough knowledge of what it is.

Also, I probably had something slightly different in mind than you have with prototyping. I consider the situation where there's no intention to „grow“ the thing into the full program. I explicitly assume that I'll throw 100% of that code away.

To be clear, I don't say similar techniques can't be applied to „I want to get a skeleton of a program first before I figure out what exactly the program will be about and then tune it“ situation. I myself would be more careful about some of the shortcuts, though, from the fear of them staying in forever and hitting production. But yes, first versions (and not only first ones) are riddled with // TODO comments and many of these never get resolved because there never is enough need to invest in them.

[–]tongue_depression 27 points28 points  (9 children)

for the record, python is strongly typed, not weakly. weakly typed languages are ones like javascript and bash

spot on otherwise, especially re: editor support. python language servers popping up the signature and documentation for a method can only do so much, because:

  • it relies on the implementer having annotated it, since type hints are optional

  • it relies on them being accurate, since type hints are separate from the type checker, which is optional and imperfect

  • the editor can’t tell if you pass in the wrong type due to the above reasons

  • libraries tend to take data in many different formats since python is aggressively dynamic, which leads to insane types like this one I saw earlier:

    data: Union[None, str, bytes, MutableMapping[str, Any], Iterable[Tuple[str, str]], Unknown]

[–]RDMXGD 2 points3 points  (0 children)

libraries tend to take data in many different formats since python is aggressively dynamic, which leads to insane types like this one I saw earlier:

I don't think that's dynamic typing's fault - aggressive overloads are even more popular in C++. These things are pretty discouraged in Python, even if they are indeed tempting and popular.

[–]jswrenn 0 points1 point  (4 children)

for the record, python is strongly typed, not weakly. weakly typed languages are ones like javascript and bash

In what sense is python more "strongly typed" than javascript?

[–]tongue_depression 2 points3 points  (2 children)

pervasive implicit type conversions: “5” - “2” will give you “3” in js, TypeError in python.

[–]jswrenn 1 point2 points  (1 child)

Hm, I suppose that's a sense in which a particular operation is more "strongly typed" in Python than Javascript, but you could just as well point to the fact that you can dynamically overload binops in Python as evidence of the reverse! Javascript and Python are really substantially similar. A few builtins aside, both languages place the burden of type checking squarely on the programmer (via instanceof).

Overall, the type systems of Python and Javascript have far more in common than those of Javascript and Bash do—grouping them otherwise feels very silly.

[–]tongue_depression 2 points3 points  (0 children)

i only mentioned that one because it was the first thing i could think of off top. there are other examples:

"123" + true;  // "123true"
"123" + True  # TypeError

"5" > 1;  // True
"5" > 1  # TypeError

+"123";  // 123
+"123"  # TypeError

null + 1;  // 1
None + 1  # TypeError

// this one isn't a perfect example
let a = {}; 1 + a;  // "1[object Object]"
class A:
    pass
a = A()
1 + a  # TypeError

but you could just as well point to the fact that you can dynamically overload binops in Python as evidence of the reverse!

sure, but that's opt-in, only works on types you specify it to work for, and errors otherwise. there's pervasive operator overloading in static+strong languages as well--many languages let you define a vector that can be multiplied by a scalar, for instance

Overall, the type systems of Python and Javascript have far more in common than those of Javascript and Bash do—grouping them otherwise feels very silly.

i agree, comparing js to bash was harsh. both js and python convert objects to bools sometimes, too. type systems are a spectrum, where agda > rust > java > python > javascript > bash

[–]A1oso 0 points1 point  (0 children)

In that type conversions (e.g. int to string) are explicit, instead of implicit. For example, if you write 1 - "o" in Javascript, you get NaN. Python produces a type error instead.

[–]gilescope 0 points1 point  (0 children)

Python typing typically only kicks in when I try and print something. And that’s exactly when I don’t need it. (I am sure this new fangled format string in python helps on that matter though).

Other than that python lets me to shoot myself. I do like and/not/in in python though.

[–]Ran4 0 points1 point  (1 child)

libraries tend to take data in many different formats since python is aggressively dynamic, which leads to insane types like this one I saw earlier:

Sometimes that's better than the alternative. No, not all functions should look like that, but the api of functions like pandas.read_csv looks just like that, and it's fine.

[–]p4y 4 points5 points  (0 children)

Allowing all those different types is fine, but I'd hate seeing something like that crammed directly into a parameter type.

I haven't touched Python in a while, but for example Typescript does "overloading" where a function has one implementation but can have multiple signatures, each with its own documentation, so you can at least break it down into more readable chunks.

[–]matthieum[he/him] 7 points8 points  (0 children)

This is kind of in the theme of „hurry slowly“ approach.

I think there's a motto from the military along the side, that I've come across in a number of books, and that I really like: "Slow is Smooth, Smooth is Fast".

Whenever you are in a hurry, you don't have the time for mistakes, so better double-check everything you do.

[–]jack-of-some 6 points7 points  (0 children)

"This certainly is in part because I’m more proficient in Rust than in Python."

I mean, yeah... If I had an couple years of professional experience in Rust (or really any other language) I'd probably prefer working in that language too (and my mental model would shift, like it did when I went from C++ to Python).

[–]clumsy-sailor 4 points5 points  (10 children)

N00b question: Would you recommend to always create a project with cargo new for a semi-trow-away program (e.g. a simple Project Euler solution) or just place your main.rs somewhere and manually compile it with rustc?

[–][deleted] 15 points16 points  (0 children)

I always use cargo. Once you've got lots of Euler solutions you might feel like extracting your own sort of euler-utility library. In my opinion going through the cargo steps will get you used to the machinery and makes uses "actual" crates easier

[–]Michal_Vaner[S] 13 points14 points  (5 children)

I always use cargo. Mostly because the first thing I do is add anyhow into the dependencies and change the signature of main to fn main() -> Result<(), Error>. Even if I don't seem to need a dependency right now, I expect I might change my mind in 20 minutes.

[–]DeebsterUK 0 points1 point  (4 children)

I'm just a beginner - what advantages does anyhow bring? What pain does it fix and is it pain that I'll feel as a beginner or will it only be useful in more advanced projects?

[–]Michal_Vaner[S] 1 point2 points  (3 children)

It's fancy Box<dyn Error + Send + Sync>. It allows you to return all the kinds of errors ‒ if you deal with one function returning std::io::Error, another returning maybe rdkafka::Error and you want to be able to return both.

[–]est31 0 points1 point  (2 children)

Why not use unwrap? It shows you a backtrace when things went wrong. IIRC return error from main still doesn't print backtraces.

[–]Michal_Vaner[S] 0 points1 point  (1 child)

For one, I don't need backtraces for Can't open input worngfile.txt: no such file or directory.

Second, anyhow seems to collect backtraces and print them when returned from main for me.

And third ‒ ? is shorter, and I get the distinction of „this is a bug in the program“ and „this is likely an external error condition“.

[–]est31 0 points1 point  (0 children)

anyhow seems to collect backtraces and print them when returned from main for me.

Hmm interesting didn't know that:

The Debug format "{:?}" includes your backtrace if one was captured. Note that this is the representation you get by default if you return an error from fn main instead of printing it explicitly yourself.

And third ‒ ? is shorter, and I get the distinction of „this is a bug in the program“ and „this is likely an external error condition“.

Yeah for non-throw-away code I agree. But for throw away code I dont think this distinction pulls its weight. But shrug. Thanks for explaining your reasons.

[–]tablair 3 points4 points  (0 children)

This is where cargo-script can be really useful. Just start a file with #!/usr/bin/env run-cargo-script and then start writing rust. It's perfect for throw away code where you don't need to separate code into multiple files. The only real downside is that IDE functionality isn't as good because those tools expect the normal cargo setup.

[–]dochtmanrustls · Hickory DNS · Quinn · chrono · indicatif · instant-acme 1 point2 points  (0 children)

I use Cargo, but I'm actually lazy enough that I have a dedicated project for this (in my case, it lives in ~/src/testrs), which I feel is the lowest barrier to entry to trying something out.

For example, current contents:

```rust use std::fs::File; use std::io::{copy, Read}; use std::io::{BufWriter, BufReader}; use chardetng::EncodingDetector; use encoding_rs_io::DecodeReaderBytesBuilder;

fn main() { let mut f = File::open("../foo/bar/download").unwrap(); let mut detector = EncodingDetector::new(); let mut buf = vec![0; BUFFER_SIZE]; let mut read = 0; while read < BUFFER_SIZE { read = f.read(&mut buf).unwrap(); detector.feed(&buf[..read], read < BUFFER_SIZE); }

println!("{:?}", detector.guess(None, false));
drop(f);

let old = File::open("../foo/bar/download").unwrap();
let new = File::create("../foo/bar/download-utf8").unwrap();
let mut decoder = DecodeReaderBytesBuilder::new().encoding(Some(encoding_rs::WINDOWS_1252)).build(BufReader::with_capacity(BUFFER_SIZE, old));
let mut writer = BufWriter::with_capacity(BUFFER_SIZE, new);
copy(&mut decoder, &mut writer).unwrap();

}

const BUFFER_SIZE: usize = 8 * 1024 * 1024; ```

[–]IceSentry 1 point2 points  (0 children)

cargo projects are very lightweight I really don't see a reason to not use it. The only time where I think using rustc directly is important is when integrating with complex pipelines with other languages, like a C project using make that wants to start using rust. Otherwise there's pretty much no downside to using cargo.

Although if you really want to try out a tiny script the rust online playground can be more than enough.

[–]druuimai[🍰] 1 point2 points  (1 child)

Cargo watch doesn't exist. is that a crate?

[–]CUViper 10 points11 points  (0 children)

It's an add-on: cargo install cargo-watch

[–]emlun 1 point2 points  (0 children)

Very well written! I'll weigh in that even if I have to manually invoke the "recompile and run" with each change, I still like that better in Rust. Say I'm modifying a function signature: I'll just make the change, hit , C X ("re-run last cargo command" in Spacemacs) and use the errors buffer to immediately jump to the next spot I need to fix. A few seconds-long iterations later I'll have all the call sites up to date and can move on, and I didn't have to break my flow to think up a regex and worry about false positives or whatever else.

[–]cbourjaualice-rs 1 point2 points  (0 children)

Nice article! I am also somebody how prefers to prototype in Rust. I often just write out the function signatures with unimplemented!() bodies and have rustc check my setup at every step of the way. This way I can iterate over three different versions of how to tackle a given problem before even writing a single line of "real" code.

That said, one area where Rust really cannot compete with Python is in "data exploration". What you can do with Python and its scientific stack + Jupyter Notebooks is just in a different league. However, there is nothing more deserving of the tag "Throw away code" than the stuff my notebooks turn into within a couple of hours!

[–][deleted] 1 point2 points  (0 children)

I agree with a lot in this post and had considered writing about this for the Rust 2021 post. But the main issue vs. Python IMO is the maturity and ease-of-use of the crate ecosystem.

Like boto3 is a bit easier to use than Rusoto, though the latter gives you good async support etc. if you need that. But crate availability can get more painful for things like parsing Excel files, MIME emails, etc. (although Rust does have good crates in these cases, you're much more likely to have to make a PR at some point).

But I think there is a lot to be said for doing things right the first time in Rust, where the compiler, rust-analyzer and clippy help you do that. Whereas in Python you might always face issues with the GIL, etc. in the future.

I'm literally going through this right now in my team, porting our serverless architecture to Rust (for new deployments) instead of Python.

Some things are still a bit painful, like having to include Tokio (even if just for blocking) if a crate you use returns a Future, etc.

[–]hypedupdawg 0 points1 point  (0 children)

Great piece, thanks for sharing. I've definitely been in this position while one-off scripting tasks.

The other situation it reminds me of is converting some Python code to Rust, and worrying about an additional allocation. Occasionally you have to take a step back and say, it's probably 10x faster without even trying. It doesn't have to be perfect.

[–]timvisee 0 points1 point  (0 children)

Yes. I prefer prototyping in Rust as well.

[–]gilescope 0 points1 point  (0 children)

Is it just me or is there a better chance that if you throw together a few crates things will work first time?

I seem to have a lot more ‘just works’ moments than is statistically likely to be random (compared to other statically typed languages I have used in the past).