this post was submitted on 16 Oct 2024
66 points (97.1% liked)

Rust

6029 readers
2 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

!performance@programming.dev

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 1 year ago
MODERATORS
 

If we were to create a Rust version of this page for Haskell, what cool programming techniques would you add to it?

top 50 comments
sorted by: hot top controversial new old
[–] SorteKanin@feddit.dk 30 points 1 month ago (3 children)

Not exactly the same thing but this is still pretty funny. This is code that is technically 100% legal Rust but you should definitely never write such code πŸ˜….

[–] AsudoxDev@programming.dev 12 points 1 month ago (1 children)
[–] SorteKanin@feddit.dk 23 points 1 month ago (1 children)

It's a test for the compiler which ensures that these legal yet extremely weird expressions continue to compile as the compiler is updated. So there is a purpose to the madness but it does still look pretty funny.

[–] nilloc@discuss.tchncs.de 8 points 1 month ago

That’s make sense. We used to write some ridiculous tests too, but users still managed to find a way


fn union() {
    union union<'union> { union: &'union union<'union>, }
}

Is my favorite.

[–] barsoap@lemm.ee 6 points 1 month ago* (last edited 1 month ago)

That makes complete sense. Ranges implement fmt::Debug, .. is a range, in particular the full range (all values) ..= isn't because the upper bound is missing but ..=.. ranges from the beginning to the... full range. Which doesn't make sense semantically but you can debug print it so add a couple more nested calls and you get a punch card.

I totally didn't need the Rust playground to figure that out.

EDIT: Oh, glossed over that: .. is only the full range if standing alone, it's also an infix operator which is why you can add as many as you want (be careful with whitespace, though). .. .. .. .. .. .. .. .. .. .. is a valid Rust expression.

[–] silasmariner@programming.dev 4 points 1 month ago

This is excellent content

[–] Ephera@lemmy.ml 20 points 1 month ago (2 children)

A cool thing you can do, is to store values of all kinds of types in a big HashMap, basically by storing their TypeId and casting them to Box<dyn Any> (see std::any).
Then you can later retrieve those values by specifying the type (and optionally another ID for storing multiple values of the same type).

So, you can create an API which is used like this:

let value = MyType::new();
storage.insert(value);
let retrieved = storage.get::<MyType>();
assert_eq!(retrieved, value);

There's various ECS storage libraries which also implement such an API. Depending on what you're doing, you might prefer to use those rather than implementing it yourself, but it's not too difficult to implement yourself.

[–] SorteKanin@feddit.dk 11 points 1 month ago

There's a crate for it too: anymap2

[–] Buttons@programming.dev 3 points 1 month ago (1 children)

What if I specify the wrong type? let retrieved = storage.get::<SomeOtherType>();?

Is it a runtime error or a compile time error?

[–] Buttons@programming.dev 7 points 1 month ago (1 children)
[–] Ephera@lemmy.ml 2 points 1 month ago* (last edited 1 month ago)

Well, you would determine the TypeId of SomeOtherType, then search for that as the key in your HashMap and get back a None, because no entry exists and then you'd hand that back to the user.
I guess, my little usage example should've included handling of an Option value...

So, it's only a runtime error, if you decide to .unwrap() or similar.

[–] silasmariner@programming.dev 15 points 1 month ago (1 children)

Rust isn't really a language that lends itself to terse point-free functional idioms... The sort of examples I might want to share would probably require a bit more context, certainly more code. Like I think type guards and arena allocation are cool and useful tricks but I don't think I could write a neat little example showing or motivating either

[–] little_ferris@programming.dev 7 points 1 month ago (1 children)

Yeah I don't mean just terse functional idioms. Any programming technique that blew your mind the first time you came across it would qualify.

[–] silasmariner@programming.dev 3 points 1 month ago (1 children)

Type guards, then :) very cool, much compiler power, love it

[–] little_ferris@programming.dev 2 points 1 month ago (1 children)
[–] naonintendois@programming.dev 6 points 1 month ago (1 children)

Maybe they're referring to "where clauses"?

[–] silasmariner@programming.dev 2 points 1 month ago

Indeed I am. Forgot the name, lol, not worked with rust for a few months πŸ˜…

[–] tuna@discuss.tchncs.de 12 points 1 month ago (3 children)

Something i didnt know for a long time (even though its mentioned in the book pretty sure) is that enum discriminants work like functions

#[derive(Debug, PartialEq, Eq)]
enum Foo {
    Bar(i32),
}

let x: Vec<_> = [1, 2, 3]
    .into_iter()
    .map(Foo::Bar)
    .collect();
assert_eq!(
    x,
    vec![Foo::Bar(1), Foo::Bar(2), Foo::Bar(3)]
);

Not too crazy but its something that blew my mind when i first saw it

[–] Ephera@lemmy.ml 8 points 1 month ago (1 children)

This works with anything that one might call "named tuples".

So, you can also define a struct like so and it'll work:

struct Baz(i32);

On the other hand, if you define an enum variant with the normal struct syntax, it does not work:

enum Foo {
    ...
    Qux { something: i32 } //cannot omit braces
}
[–] barsoap@lemm.ee 2 points 1 month ago* (last edited 1 month ago) (1 children)

Named function arguments would occasionally be nice to have instead of the single n-tuple they take now. Currently I'm more or less playing a game of "can I name my local variables such that rust-analyzer won't display the argument name when I stick them into functions (because they're called the same)).

[–] Ephera@lemmy.ml 2 points 1 month ago

Yeah, I do miss those, too, although I've noticed that I'm becoming ever more consistent with just naming my variables like the type is called and that works out nicely in Rust, because then you can also leave out the field name when filling in a struct with named fields. I'll often have named my function parameters the same name that I ultimately need to pass into structs fields.

At this point, I'm secretly wondering, if a programming language could be designed where you don't normally fill in variable names, but rather just use the type name to reference each value.
For the few cases where you actually do have multiple variables of the same type, then you could introduce a local (type) alias, much like it's currently optional to add type annotations.
Someone should build this, so I don't have to take on another side project. πŸ™ƒ

Clippy will warn you if you don't use this feature.

[–] little_ferris@programming.dev 2 points 1 month ago (1 children)

Yea it's like when we writeSome(2). It's not a function call but a variant of the Option enum.

[–] barsoap@lemm.ee 3 points 1 month ago (2 children)

Enum constructors are functions, this typechecks:

fn foo<T>() {
    let f: fn(T) -> Option<T> = Some;
}

I was a bit apprehensive because rust has like a gazillion different function types but here it seems to work like just any other language with a HM type system.

Woah. That's quite interesting. I didn't know that.

[–] anton@lemmy.blahaj.zone 1 points 1 month ago

I was a bit apprehensive because rust has like a gazillion different function types but here it seems to work like just any other language with a HM type system.

The fn(T)->R syntax works for functions without associated data, it discards details of the implementation and works like function pointers in C. This allows them to be copy and 'static.

The other function types can have data with them and have more type information at compile time which allows them to be inlined.
These functions each have their own unwritable type that implements the function traits (Fn(T)->R, FnMut(T)->R and FnOnce(T)->R) depending on their enclosed data.

I hope I remembered everything right from this video by Jon Gjengset.

[–] tatterdemalion@programming.dev 10 points 1 month ago (1 children)

One thing I like a lot about Rust is that it rarely does blow my mind.

But one crate that actually did blow my mind is corosensei. It's not Rust per se that is so crazy here, but the way it's essentially implementing a new language feature with assembly code. This is how you know Rust really is a systems programming language. I invite you to read the source code.

[–] FizzyOrange@programming.dev 2 points 1 month ago

Oh wow I've been looking for something nice like that for ages. Python can do this and it's really great for silicon verification test stimulus. I've also done it in C++ using C++20 coroutines, but they are so complicated and low level I ended up having to use a library to help (libcoro). Felt like a bit of a gap in Rust, but this looks like a great solution!

[–] snaggen@programming.dev 10 points 1 month ago* (last edited 1 month ago) (2 children)

Mindblowing features are basically, by definition, a result of bad language design. They blow your mind, since they are totally unexpected behaviours. They may still be cool, but they are unexpected and hence unintuitive.

A language that are full of these is Perl. And one simple one is that you can take the string "AAAAA" and use addition on that, like "AAAAA"++ and you will get the result "AAAAB". Cool you may think, but is it really? Addition is normally used to increase the value of a number, that is a completely different operation than modifying a String. The string "AAAAA" cannot be said to be greater or less than "AAAAB", besides the very special case when we order it. But in general the name "John" is not considered to be higher/lower than "Mark", they are just different. So, even if it is cool to manipulate strings by using addition/subtraction, it is still bad language design and very unintuitive. Also, since perl is so loosely typed, it may also cause very unexpected bugs.

[–] BB_C@programming.dev 8 points 1 month ago (1 children)

The general theme of your comment is good, but the example is...

The string β€œAAAAA” cannot be said to be greater or less than β€œAAAAB”

But in general the name β€œJohn” is not considered to be higher/lower than β€œMark”

// rust
  eprintln!("{}", "AAAAB" > "AAAAA") // true
  eprintln!("{}", "Mark" > "John") // true
// C
  printf("%d\n", strcmp("AAAAB", "AAAAA")); // 1
  printf("%d\n", strcmp("Mark", "John")); // 1

strcmp() docs:

strcmp() returns an integer indicating the result of the comparison, as follows:

  • 0, if the s1 and s2 are equal;

  • a negative value if s1 is less than s2;

  • a positive value if s1 is greater than s2.

So basically, if C had higher level constructs, it would be identical to Rust here.

So, even if it is cool to manipulate strings by using addition/subtraction, it is still bad language design and very unintuitive.

Rust has impl Add<&str> for String and impl AddAssign<&str> for String. Both append as expected.

But maybe you meant numeric addition specifically.

In that case, yes, Rust doesn't have that, although it's an impl<'a> Step for &'a str away from having something similar (it would be ("AAAAA"..).next()).

impl Step for char already exists of course, but shouldn't if we take your argument to its logical conclusion.

Oh, and C would most definitely have this feature if it could. Numerical manipulation of chars is commonplace there.

[–] barsoap@lemm.ee 1 points 1 month ago* (last edited 1 month ago) (1 children)

Rust has impl Add<&str> for String and impl AddAssign<&str> for String. Both append as expected.

I wouldn't go so far and say "as expected": "Addition" implies that we're dealing with a ring, that there's also multiplication, and that really doesn't make sense for strings (unless you indeed consider them numbers). It's why Haskell's Monoid typeclass comes with the function mappend, not madd.

In Rust's defence though std::ops traits aren't meant to carry any semantics they're about syntax: It's called Add because it's +, not because it means something. I do think it would've been worth it to introduce ++ for general appending (also vectors, sets, maps, etc), though, to keep things clean. Also ++= for the mutating case.

[–] BB_C@programming.dev 1 points 1 month ago (1 children)

I do think it would’ve been worth it to introduce ++ for general appending.

I already mentioned (val..).next() which is both safe* and explicit about it being a generic stepping operation instead of possibly being sugar for {x = x + 1; x}.

Also, calling it "appending" is weird for us folks coming from languages like C πŸ˜‰

* you don't have to worry about what i32::MAX++ would/should return.

[–] barsoap@lemm.ee 1 points 1 month ago* (last edited 1 month ago) (1 children)

Pre- and post-increment are only really useful when you're doing C-style looping and there's a good reason we don't do that in Rust.

I actually honestly can't recall ever making an off by one error in Rust, I'm sure when implementing specific data structures or when doing pointer manipulation it's still a possibility but you can write a gazillion lines of code without ever running risk of that particular annoyance. Also while C folks may have an argument regarding operator semantics, C++ folks don't they're doing unspeakable things to <<.

Also, FWIW Haskell uses ++ to append lists and therefore also strings. It's not like it's an odd-ball usage of the symbols, that'd be .. which I vaguely remember some language using. Would cause a whole new class of confusion regarding 'a'..'z' vs. "a".."z". Not to mention that "aa".."zz" actually makes sense as a range all that's missing is &str: Step. Probably not a good idea to have built-in because do we mean printable ASCII? Whole unicode range? Just the alphabet? Not an issue when you're doing it to single chars but strings get ambiguous fast. Does Rust even guarantee stuff about Char ordering C certainly doesn't really do that, short of I think 0..9 being contiguous.

[–] BB_C@programming.dev 1 points 1 month ago (1 children)

It’s not like it’s an odd-ball usage of the symbols, that’d be .. which I vaguely remember some language using.

I take it, you don't bash/zsh/...?

[–] barsoap@lemm.ee 1 points 1 month ago (1 children)

I try not to and if I have to I'd use string interpolation. I'm not even sure whether you're pulling my leg right know, I literally don't remember whether they have a string append operator.

Like 99.999% of the sh I ever wrote was in Makefiles and short wrapper scripts which could just as well be aliases. No argument handling past $@, no nothing the language is just too fickle for me to bother dealing with. The likes of zsh are make-up on a pig, I think I had a quick run-in with fish but never really got the hang. Nushell is different, it's actually bold enough in its changes to get rid of all the crufty nonsense.

[–] BB_C@programming.dev 2 points 1 month ago (1 children)

Sorry, I thought you meant the use of .. in Rust is odd. So I pointed out that {0..9} and{a..z}is also used at least in bash and zsh. That's at least 10s of millions of users!

I know of .. being used for appending by lua at least. So still not odd-ball I would argue, since the people who interacted with lua code in their life probably outnumber those who interacted with all functional languages combined.

[–] barsoap@lemm.ee 2 points 1 month ago (1 children)

Now that you mention it yes Lua is probably the one that I remember. It's an incredibly well-designed language from start to finish but also culturally an odd-ball. .. isn't even the biggest offender: Their indices start at 1. Haskell accosts you with zygohistomorphic prepromorphisms but at least [1,2,3] !! 1 is 2.

[–] BB_C@programming.dev 1 points 1 month ago

Their indices start at 1

Why do you hate zsh so much? πŸ™‚

[–] barsoap@lemm.ee 7 points 1 month ago (1 children)

The string β€œAAAAA” cannot be said to be greater or less than β€œAAAAB”, besides the very special case when we order it.

I hate it to break it to you but it's the same with numbers.

[–] snaggen@programming.dev 1 points 1 month ago

Ok, I then have some business proposals....

[–] solrize@lemmy.world 5 points 1 month ago* (last edited 1 month ago) (2 children)

I'd like to see a Rust solution to Tony Morris's tic tac toe challenge:

https://blog.tmorris.net/posts/scala-exercise-with-types-and-abstraction/index.html

His rant about it is here:

https://blog.tmorris.net/posts/understanding-practical-api-design-static-typing-and-functional-programming/

I did a Haskell GADT solution some time back and it's supposed to be doable in Java and in C++. Probably Rust too. I wonder about Ada.

[–] LPThinker@lemmy.world 6 points 1 month ago (1 children)

This could be done almost trivially using the typestate pattern: https://zerotomastery.io/blog/rust-typestate-patterns/.

[–] solrize@lemmy.world 2 points 1 month ago* (last edited 1 month ago) (1 children)

Neat that looks interesting. There's a similar Haskell idiom called session types. I have a bit of reservation about whether one can easily use Rust traits to mark out the permissible state sets that an operation can take, but that's because I don't know Rust at all. I do remember doing a hacky thing with TypeLits in Haskell to handle that. Basically you can have numbers in the type signatures and do some limited arithmetic with them at type level. I'd be interested to know if that is doable in Rust.

[–] Ephera@lemmy.ml 2 points 1 month ago

Rust has "const generics" which are, for example, used to pass the length of a fixed-length array via the type system. Apparently, const generics also do allow for some mild arithmetic.

https://doc.rust-lang.org/reference/items/generics.html#const-generics

[–] silasmariner@programming.dev 2 points 1 month ago* (last edited 1 month ago) (1 children)

That doesn't look like a particularly difficult challenge? Like, it's just an implementation game, move returns a data type that you write yourself

Edit: I suppose there's life in that kind of ambiguous variation though

load more comments (1 replies)
[–] Infernaltoast@programming.dev 5 points 1 month ago* (last edited 1 month ago)

You can manually implement PartialEq and Eq on an Enum that implements Hash to manually determine how the hashmap keys override/collide with one another.

[–] someacnt_@lemmy.world 1 points 1 month ago (2 children)

The haskell examples look more like an arcane wizardry.

My favorite example of haskell arcane wizardry is lΓΆb. It's mentioned in this list but not really done justice imo.

[–] little_ferris@programming.dev 2 points 1 month ago* (last edited 1 month ago)

πŸ˜‚ Ikr!

[–] solrize@lemmy.world 1 points 1 month ago

Here's another Haskell example where I'd be interested to see a Rust counterpart. It's a red-black tree implementation where the tree invariants are enforced by types. The code would less ugly with more recent GHC features, but same idea.

https://gist.github.com/rampion/2659812

https://old.reddit.com/comments/ti5il

load more comments
view more: next β€Ί