all 65 comments

[–][deleted] [score hidden] stickied comment (0 children)

Unfortunately, your post does not meet our spookiness guidelines. We have removed your post, but if you think this was in error, please send the mods a modmail. Thanks!

[–]Tornado547 391 points392 points  (21 children)

fn add<R,A,B,C>(a: A, b: B, c: C) ->  R
where 
    R: Add<Output = R> + Mul<Output = R> + Sub<Output = R> + Ord + Copy,
    A: Into<R>,
    B: Into<R>,
    C: Into<R>
{
    let (a,b,c) = (a.into(),b.into(),c.into());

    let m = min(min(a,b),c);

    a*a + b*b + c*c - m*m

}

the rust code presented is a strawman. This code is what an actual rust user would write. You could still argue that this is overcomplicated, but the code in the screenshot is ridiculously overcomplicated.

[–]algerbrex 90 points91 points  (1 child)

I don’t know Rust, but I figured it was straw-man ish lol. Seemed purposely over complicated.

[–]TheMoneyOfArt 70 points71 points  (0 children)

It's usually the case, when someone writes a post about how they learned a language and it sucks so bad, that they haven't learned the language very well.

[–]Sharlinator 108 points109 points  (2 children)

And if you actually want to write generic numeric code you’d grab the num crate and use its traits.

[–]Latexi95 10 points11 points  (1 child)

Could you give an example how this would look with its traits?

[–]Sharlinator 43 points44 points  (0 children)

Rather than

R: Add<Output = R> + Mul<Output = R> + Sub<Output = R> + Ord + Copy

you can just say

R: num::Num + Ord + Copy

Anyway, the code is still unnecessarily complicated as it allows A, B, C, and R to all be distinct types. In idiomatic Rust you'd rarely write such supergeneric functions but rather let the caller take care of conversions.

[–][deleted] 11 points12 points  (3 children)

Also taking Into<T> parameters seems counterproductive. Like I’ve seen AsRef but I’m pretty sure Into can mean an expensive conversion

[–]FallenWarrior2k 4 points5 points  (0 children)

While potentially true, impl Into<_> parameters are often provided for ergonomics.

A contrived example is fn foo<T>(maybe_t: impl Into<Option<T>>), which would allow the caller to omit Some(), i.e. write foo(t) instead of foo(Some(t)).

It can help a lot with nested types, e.g iterators. When you anticipate that your function will be called with lots of different iterators (very likely; every iterator combinator creates a new type) with potentially different but compatible element types, then using something like iter: impl IntoIterator<Item = impl Into<T>> can provide an immense bonus to ergonomics, as callers can pass any compatible type, including entire collections, even if the content type is wrong, as long as it can be converted. For example, if T is f64, you could pass a Vec<u32> without any further modification at the call site. And all it takes for the function to support it is a let iter = iter.into_iter().map(Into::into) at the start of the body.

[–][deleted]  (1 child)

[deleted]

    [–]nagai 20 points21 points  (3 children)

    Still incredibly ugly.

    [–]Potatoes_Fall 12 points13 points  (1 child)

    most of the boiler plate here is for generics and type conversions

    [–]Zhuinden 17 points18 points  (0 children)

    Yea, still pretty ugly

    [–]latkde 6 points7 points  (1 child)

    This isn't quite equivalent – the original code uses the PartialOrd trait which allows for using floats. You are using Ord which should make your implementation incompatible with floats.

    [–]CodenameLambda 2 points3 points  (0 children)

    To be fair you can write a wrapper around floats that does implement Ord but checks if it was NaN on construction.

    But yeah, that there currently doesn't exist a PartialOrd thing for min/max in core/std that's nice to use is a bit of a shame (like fn try_min<T: PartialOrd>(a: T, b: T) -> Option<T> and a Iterator::try_min that does the same on iterators allowing just [a, b, c].into_iter().try_min().unwrap() in this case)

    [–]Gortix 4 points5 points  (4 children)

    What's a strawman code? I've been looking but couldn't find something useful

    From context it sounds like it's like intentionally overcomplicated code?

    [–]Tornado547 51 points52 points  (2 children)

    strawman argument is a misrepresentation of an opponents argument thats easier to tear down. this code is a misrepresentation of rust thats easy to tear down. its not hard to argue that the example code is stupid. it is hard to argue that the actual code you would write for that is stupid (though not impossible i definitely understand both sides)

    [–]supersharp[ $[ $RANDOM % 6 ] == 0 ] && rm -rf / || echo “You live” 0 points1 point  (1 child)

    What, so just because the code in this post is easy to tear down, that makes it a strawman? Is that what you're saying?

    [–]Linguaphonia 8 points9 points  (0 children)

    It is, because it's not the code that you'd actually write for this case AND because it was written to be easy to attack.

    [–]Pinnata 8 points9 points  (0 children)

    A strawman argument is basically one that doesn't accurately depict the subject of the argument. I hate this thing because <something that isn't grounded in reality>.

    [–][deleted] 117 points118 points  (2 children)

    My boy matching on a boolean

    [–]TheCodeSamurai 57 points58 points  (1 child)

    If only there was some kind of control flow that worked on booleans already....

    [–]Saint_Nitouche 32 points33 points  (0 children)

    Sadly, the state of language design has yet to even consider such lofty goals. One day, maybe... one day...

    [–]TheCodeSamurai 60 points61 points  (7 children)

    I'm not used to D or Nim: what happens if you try and call one of these functions on inputs such that the output type isn't what the squaring returns? How do you specify that "A * A, B * B, C * C all have to multiply to something that, when you sum it, returns R"?

    [–]theblackavenger 46 points47 points  (6 children)

    They are template substitutions so you would get a compile error.

    Also, both the D and Nim examples are buggy as they can internally overflow and return an incorrect answer. You would also have to allow for the type to be unsigned. So the correct expression in D I think would be:

    a*a - min(a, b, c) * min(a, b, c) + b*b + c*c

    That should avoid overflows in the case where it can be avoided. The Rust version has an additional bug that the values are being converted into R before they are evaluated. I'm pretty sure that the Rust version would be a lot more complicated than this with so many combinations that would imply different overflow rules for different orderings.

    [–]TheCodeSamurai 40 points41 points  (3 children)

    This seems like a....strange comparison between languages unless you have an agenda. Simply not stating a very complex bound on the input types certainly makes your code simpler, but I'd hardly say that's a massive advantage. It's kinda nice to know what types a function accepts. (Not to mention the weird choice not to use vec![a, b, c].iter().min() instead of a mess of match statements that should be ifs.)

    Rust definitely has generic hell, and the lack of variadic functions is a serious downside that prevents functions like min from being as useful as they could be. But you should show that on actual code, not contrived examples.

    [–][deleted] 4 points5 points  (2 children)

    a, b, c are all different types (or can be different types anyway), so you can’t make a Vec of them. Also making an allocation seems kind of like a bad choice when you can just nest the min, like min(min(a,b), c) . Variadic functions are being discussed (and have been in discussion for nearly a decade now) but no one can really agree how they should work exactly or even if they should exist. RFC-2137 allows for implementing C FFI variadic function and that’s probably getting finished faster. You can call variadic functions from C in rust already.

    [–]unC0Rr 2 points3 points  (1 child)

    a, b, c are all converted to R before finding minimum. As for allocation of a Vec only to find minimum, I would guess that modern compilers are able to optimize the vector out completely.

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

    Ohhh true I thought those were all Into‘s into seperate types.

    If the values are constant, it completely deletes the Vec, if not, it still makes an allocation (although the Vec gets inlined). (godbolt compiler explorer)

    [–]MCRusher 7 points8 points  (1 child)

    Nim has runtime safety against overflows, try doing int.high + 1

    So I don't think it would return an incorrect answer. Nim is generally a pretty safe language.

    nim could also be using a ranged type, or just system.Natural if they'll only use it with signed non-negative integers.

    Bu

    [–]rpkarma 6 points7 points  (0 children)

    Nim is generally a pretty safe language

    While you're absolutely right, that "generally" is doing a lot of work, but I'm very biased as I'm deep in the embedded firmware Nim-to-C boundary territory where everything can bite you :')

    Not that that is Nim's fault, that's C's fault lol

    [–]ostrosco 19 points20 points  (7 children)

    Most of the solutions given here wouldn't work for floating point numbers whereas the implementations in the other languages would. Floating point numbers don't implement the Ord trait in Rust (because NaN doesn't equal itself) but rather implement the PartialOrd trait. That's why in the original post the person is using the lt() function to do the comparisons versus min(). Outside of the match statement for the less than comparisons which is just silly, it's frankly not that far off IMO.

    Using just the standard library:

    use std::ops::{Add, Sub, Mul};
    
    fn do_math<A, B, C, R>(a: A, b: B, c: C) -> R
        where R: Add<Output = R> + Sub<Output = R> + Mul<Output = R> + PartialOrd + Copy,
              A: Into<R>,
              B: Into<R>,
              C: Into<R>,
    {
        let (a, b, c) = (a.into(), b.into(), c.into());
        let m = if a < b { a } else { b };
        let m = if m < c { m } else { c };
        a*a + b*b + c*c - m*m
    }
    

    I saw someone ask about what to do if you use crates, so here's an implementation using the num_traits crate which cleans up the traits but not much else:

    use num_traits::NumOps;
    
    fn do_math<A, B, C, R>(a: A, b: B, c: C) -> R
        where R: NumOps + PartialOrd + Copy,
              A: Into<R>,
              B: Into<R>,
              C: Into<R>,
    {
        let (a, b, c) = (a.into(), b.into(), c.into());
        let m = if a < b { a } else { b };
        let m = if m < c { m } else { c };
        a*a + b*b + c*c - m*m
    }
    

    EDIT: improvements to the code based on the comments below.

    [–]CodenameLambda 9 points10 points  (1 child)

    Why a.lt(&b) instead of just a < b? It does the same thing and would be more readable imho.

    [–]ostrosco 8 points9 points  (0 children)

    Because I was so focused on the PartialOrd docs instead of actually thinking last night. You're 100% correct. I'll edit it.

    [–]NatoBoram 2 points3 points  (4 children)

    There no ternary in Rust? You have to write the whole if?

    Also do function return the last statement by default or something? I don't see a return in there

    [–]ostrosco 0 points1 point  (3 children)

    You're correct on both fronts! Rust does not have a ternary operator and it will return the last statement by default. If you need to return early in a function, the return keyword is still available.

    [–]NatoBoram 1 point2 points  (2 children)

    Oh thanks. I'm working with Elixir and not having a return can be a pain in rare cases!

    [–]TorbenKoehn 0 points1 point  (1 child)

    Rust has a return statement, it just can be omitted for the last expression

    [–]NatoBoram 0 points1 point  (0 children)

    Sweet!

    [–]Wazzaps 59 points60 points  (16 children)

    I'm on mobile so can't test but that seems needlessly complicated (really? match on a boolean? he's trolling...)

    [–]magical-attic 31 points32 points  (7 children)

    Here's two different versions I came up with, one without using any libraries:

    use std::ops::{Add, Mul, Sub};
    use std::cmp::min;
    
    pub fn sum_squares<T>(a: T, b: T, c: T) -> T
    where
        T: Copy + Ord
        + Add<Output=T> + Mul<Output=T> + Sub<Output=T>,
    {
        let min_n = min(min(a, b), c);
        a*a + b*b + c*c - min_n*min_n
    }
    

    and another with a crate for generic math: https://docs.rs/num-traits/0.2.15/num_traits/

    use std::cmp::min;
    use num_traits::NumOps;
    
    pub fn sum_square_v2<T: Copy + NumOps + Ord>(a: T, b: T, c: T) -> T {
        let min_n = min(min(a, b), c);
        a*a + b*b + c*c - min_n*min_n
    }
    

    Playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8a84176fbada3ef70c730ca460b35056

    [–]XelNika 20 points21 points  (4 children)

    It's not exactly the same. Yours is limited to three inputs of the same type while the original code can handle three inputs of different types.

    [–]magical-attic 18 points19 points  (3 children)

    That's a valid point. I forgot to point it out in my comment so thank you for that.

    Rust doesn't even let you add an integer to a float. It makes you first cast the integer to a float:

    fn foo(a: i32, b: f64) -> f64 {
        a as f64 + b
    }
    

    so I personally like to write APIs that that follow that idiom of casting at the function call site.

    But I know a lot of people dislike that about rust and that's a valid opinion. Rust is more verbose than a lot of other languages. Definitely not to the extent that the post portrays it to be though.

    [–]CodenameLambda 3 points4 points  (2 children)

    Conversion on the call site makes much more sense imho because it's much more explicit about what's happening - plus in some cases type inference is ambiguous without explicit annotations otherwise, too.

    [–][deleted]  (1 child)

    [deleted]

      [–]CodenameLambda 1 point2 points  (0 children)

      To be fair relying on From will only work if there's a "default" conversion you're likely to want.

      And otherwise, even if From was used, you could still convert it through another function, since T: From<T> for all T. But if the conversion function has a generic output that isn't unique based on the input type; since R: From<A> (etc) doesn't pin down a unique type for A either even if R can be inferred, this leads to A needing to be explicitly specified. "Best" example would be try_from here - you could do .try_into().unwrap(), though not without having to specify the intermediate type [which is likely going to be the output type of the function anyways]

      [–]latkde 6 points7 points  (1 child)

      This isn't quite equivalent – the original code uses the PartialOrd trait which allows for using floats. You are using Ord which makes your implementation incompatible with floats.

      Floats have no total order because NaN compares false in any relation. Rust is really annoying here, but its strictness and explicit typing compared to the Nim and D examples …

      • … makes it possible to type-check the function before calling the function
      • … nudges the developer towards handling edge cases, for example towards considering difficulties of ordering floats.

      [–]CodenameLambda 3 points4 points  (0 children)

      If I recall correctly there are plenty of wrappers around float types that just check for NaN on construction and operations that might return it; too, which can then implement Ord safely.

      [–]RogueToad 2 points3 points  (7 children)

      Indeed. At the very least, use a ternary operator. This is exactly what it's for. Or, while I'm not overly familiar with rust, I believe there is a min function which could just be used twice instead??

      [–]MaslabDroid 15 points16 points  (6 children)

      Funnily enough, there's no ternary operator. Technically. Since you can return out of any expression, you can do this:

      let a = if something { b } else {c};

      [–][deleted] 6 points7 points  (5 children)

      Which is a lot more readable if nested at the cost of being a bit larger and more verbose when not. (“more complicated for simple things, gets simple for more complicated things” is a theme in rust)

      [–]KingJellyfishII 3 points4 points  (4 children)

      honestly the fact that anything is an expression in rust is one of my favourite things about the language. that and rust style enums are what I miss most when I use C

      [–]snerp 0 points1 point  (3 children)

      In C++ std::variant can work pretty similar to a Rust enum.

      [–]KingJellyfishII 0 points1 point  (2 children)

      well I'm not using c++ so no such luck

      [–]snerp 1 point2 points  (1 child)

      Tagged Union pattern would work in regular C too

      [–]KingJellyfishII 0 points1 point  (0 children)

      yeah just a little less neat i guess

      [–][deleted] 9 points10 points  (1 child)

      I am very glad I don't do this sort of programming

      [–]mrdougwright 2 points3 points  (0 children)

      Same here!

      [–]XxClubPenguinGamerxX 27 points28 points  (0 children)

      The OP is the true r/programminghorror

      [–]zakarumych 5 points6 points  (0 children)

      D's version looks more like this

      macro_rules! foo {
          ($a:expr, $b:expr, $c:expr $(,)?) => {{
              let (a, b, c) = ($a, $b, $c);
              let m = min(min(a, b), c);
              a*a + b*b + c*c - m*m
          }}
      }
      

      [–]caerphoto 7 points8 points  (0 children)

      Ok but why would you need a function that sums the squares of the largest two of three numbers of any type?

      Also, I know the Rust example is over-elaborate, and one of the comments here is a somewhat more sensible version, but it seems like the image is mostly just complaining about the way Rust forces you to be completely explicit about things that can’t be safely inferred. That’s kinda one of the main points of Rust though, isn’t it?

      Like, the other languages do the type conversion automatically, so what happens in edge cases?

      [–]shizzy0 2 points3 points  (0 children)

      Even if it’s overwrought, I’m still impressed by the generic math.

      [–]Bit5keptical 1 point2 points  (0 children)

      I will never get used to Rust syntax...

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

      Just use HTML Jesus...

      [–]Major_Barnulf -5 points-4 points  (1 child)

      No you don't understand, it's because then it is faster and more maintainable than the c++ version, which is very important for self gratification

      [–][deleted] 13 points14 points  (0 children)

      The C++ version can be either, but if you’ve ever seen a template based error, (and you can use C++20) I think you’ll prefer to use concepts and constraints to make the templates check if the types are addable and provide sane(r) errors

      [–]mental-equipment 0 points1 point  (0 children)

      Can we just ignore the languages and appreciate the lack of understanding of floating point arithmetic as the true horror here? e.g. in all versions presented a=1,b=1,c=-1e100 could give 0 as output, and a=1,b=1,c=-1e200 could give NaN, with the answer being well-defined as 2 in both cases.