Abstractions are beating me up and I'm starting to hate Rust

Moderator note: Folks, please stop the meta discussion, which is really way off topic for this forum. Please stay friendly and keep the conversation productive.

9 Likes

@trentj Out of respect for your time I've taken a look at your links. The Is-A, Kind-A philosophy of oop design is absolutely a bad thing. It hasn't been preached in like.. 20 years? It's also not a structural part of oop at all. Wasn't it the A.I people who popularized it because they literally did build systems like that and for good reason? I guess that sort of design approach was attractive for game developers because they think in game objects, but divorce your idea of oop from that completely. At no point was it canonical.

Ironically this is kind of like how people still criticize the JVM for being slow or Php for being garbage. Outdated stereotypes that just wont die.

Unfortunately, the "L" in SOLID is often expressed and taught as the "IS-A" relationship, even though that isn't quite what it is meant to be.

I haven't seen any tutorials like that but I don't doubt bad ones exist. The L is specifically a principle against IS-A. The object that represents a thing is not actually that thing. You abstract Rectangle and have a area(length, width) and figure a square is a rectangle then wonder why you need both length AND width to calculate its area. Good interface design doesn't come with the OOP kit, you need patience and a thought process that goes further than "I just wanna get some actual work done, man". Which is an attitude I hear often from the anti OOP crowd.

@newpavlov True. I'm going to give that a shot

I accept your apology.

I regret commenting in this thread because it seems I am greatly miscommunicating. I prefer Rust's approach to OOP over Java's, I don't think inheritance is a major missing feature, and I suspect you're frustrated with the language because you're trying to use it wrong, not because Rust doesn't have inheritance. That's all, really.

I haven't used Java in years and I don't miss inheritance nor even make a great deal of use of it (if any actually) in my C++ projects. Composition. Too many assumptions on how I'm programming :confused: . Rust's traits are not foreign to me, I programmed in Ruby with mixins for a long long time. I just find the combination of the Generic system with the Trait system in Rust to be really really heavy mentally. The combinatorics of possible interactions really blows up

3 Likes

Can you expand on this part more?

    trait Trait
    trait Trait: SuperTrait
    trait Trait<Default=Type>
    trait Trait<Default=Type>: SuperTrait<Item=u32>

    trait Trait<Default=Type>: SuperTrait<Item=Type> {
        type AType;
    }

    trait Trait<Default=Type, AType=Type>: Iterator<Item=Type> {
        type AType;
    }

    impl Trait
    impl<T> Trait
    impl Trait for OtherTrait
    impl Trait for u32
    impl<T> Trait for OtherTrait<T>
    impl<T> Trait<T> for OtherTrait<T>
    impl<T: B> Trait<T> for A<T>
    impl<T: B> Trait<T> for A<T> where B: T<u32>
    impl Trait for Iterator<AssociatedType=Type>
    impl Trait for I where I: OtherTrait
    impl<I> Trait for I where I: OtherTrait<AssociatedType=Type>
    impl Trait for I where I: OtherTrait<AssociatedType=Type> {
        type AType<I: OtherTrait<AssociatedType=Type2>>
    }

    impl Trait for I where I: OtherTrait<AssociatedType=Type> {
        type AType<I: OtherTrait<AssociatedType=Type2>>

        fn foo<I: OtherOtherTrait<AssociatedType = i16>>() 
    }

    impl Trait for I where I: OtherTrait<AssociatedType=Type> {
        type AType<I: OtherTrait<AssociatedType=Type2>>

        fn foo<I: OtherOtherTrait<AssociatedType = i16>>() -> <Self as Trait>::AType
    }

And much much more. Not gonna lie, the syntax alone almost looks Turing Complete to me. I left out lifetimes parameters, marker traits and stuff like phantomdata and hrtb. Which do not merely add to the complexity of the type system, that's fine, actually they multiply it.

This feels like to me getting wrapped up in the number of possibilities of a large state machine instead of realizing that the only thing that matters at any given time is where I am and where I can go from there and under what conditions. The entire possibilities of states and transitions are irrelevant at any given time. I only need to focus on where I am and where I want to go next. Nothing else.

1 Like

No because the number of possibilities and where I need to go are exactly the same with that many combinations. Sure if you just need a generic type for a method field then easy peasy. Hate to pull the c++ templates card but why is that considered bad and this just fine? If this is fine, then what the hell could be considered language complexity? If not this then what?

How so? Are you saying that when you want to define or implement a trait that all possibilities must be defined/implemented all the time? I should think not and if you truly believe that you have a gross misunderstanding. I'm thinking that isn't the case and you are getting wrapped up in how many different kinds of candies there are in the store and having trouble choosing instead of asking yourself what you are currently in the mood for and selecting the best match.

I just don't agree with that at all. It seems to me like the combination of the generic and trait system combined with the many ways to apply them to impl are trying to somehow express the exotic deep under belly of category theory. The whole thing just looks like this to me

I feel like if I even look the other way I'll have to relearn Rust over and over again, especially if I ever want to even read anyone else's code.

2 Likes

There's certainly a lot of potential complexity as your snippet shows, but that's really just the result of a few features that you can combine into arbitrarily complicated types. You can do the same thing in pretty much any language with generics.

The basic building blocks are:

Generics

You can create types which are abstract over a concrete type:

enum LinkedList<T> {
  Cons(T, Box<LinkedList<T>>),
  Nil,
}

Generic Constraints

You can restrict the set of types you can abstract over:

fn get_bool<T>(value: T) -> bool
    where T: Into<bool> {
  value.into()
}

//alternatively, pick your favorite flavor of syntax sugar
fn get_bool<T: Into<bool>>(value: T) -> bool;
fn get_bool(value: impl Into<bool>) -> bool;

Traits

You can abstract over behavior:

trait Default {
  fn default() -> Self;
}

Associated Types

You can restrict how many times people may implement your trait for a specific type:

//can be implemented many times for a type
trait Into<T> {
  fn into(&self) -> T;
}

struct Foo;

impl Into<bool> for Foo { ... }
impl Into<u8> for Foo { ... } //allowed

//can only be implemented once for a type
trait Into {
  type Output;

  fn into(&self) -> Output;
}

struct Bar;

impl Into for Bar {
  type Output = bool;
  ...
}

//compiler error
impl Into for Bar {
  type Output = u8;
  ...
}

You can then mix and match these features as needed, just like you can in other languages. Of course, since these are composible features, you can make things as complicated as you want just like in any other language.

8 Likes

Is there a documentation gap on any of these features? Perhaps it wasn't clear how they work together? Or is there some other kind of knowledge gap we should fix in the documentation?

1 Like

The complexity is not like in any other language. As your link demonstrates with just some good ol c++. Not the features in isolation. As I said its:

No I understand the mechanisms. But what I'm not liking is the mental load. Maybe I just have a small brain but when read a language's code that each time I have to squint and bring my monitor closer and closer then I'm thinking "gosh this is a big boy language" in the worst possible way.

That's actually .Net code. (If you look in the top right corner, you should see the language switcher dropdown and you can view the signature in your choice of C#, C++, F#, or Visual Basic.) Which is basically my point, you can make arbitrarily complicated types in any language with generics.

However, just because types can be that complex doesn't mean that they will be in practice or that they should be or that they are meaningful. I can write this C#/Java code:

public class A<T> {}
public class B<T> {}

static void foo(A<B<A<A<A<A<B<B<B<B<B<B<A>>>>>>>>>>>> bar) {}

but it doesn't mean anything and it won't compile.

Can you explain why you feel that the complexity is different in Rust?

1 Like

It's actually not .NET (C#). It's C++/CLI. C++ with Microsoft extensions that allow you to write code targeting the .NET framework. The example you linked is basically this

static void foo(A<B<A<A<A<A<B<B<B<B<B<B<A>>>>>>>>>>>> bar)

Which I don't find complex. Complicated. Annoying. Not complex. That's just nesting. The operating logic is one thing, just repeated many times. What I showed, with a great many omissions I might add, was complex interactions leading to entirely different operating behaviors. I didn't cheat and write

impl<T>
impl<T, B>
impl<T, B, C>

I listed a different feature on each line(s), not just merely repeated or nested one syntax. Rust's generics are a lot like Kotlin's, but its the combination with trait and impl and to a lesser extent associated types that is what drives up the complexity.

I assure you, it really is C#.

I listed a different feature on each line(s), not just merely repeated or nested one syntax. Rustā€™s generics are a lot like Kotlinā€™s, but its the combination with trait and impl and to a lesser extent associated types that is what drives up the complexity.

There's only really about 4 different features in play here. What you've listed are those 4 features in various combinations, not different features. I agree with you that Rust's generics are more complex than Kotlin's but that's because they are also more powerful. Rarely do you find language features that add power without adding complexity.

In your original post, you said this:

My biggest pain point with Rust right now is that it is not clear where the lines for abstractions are and how the MANY interconnected rules for traits apply to each other.

Do you still feel that the rules are interconnected? How so?

From my point of view, I see 4 different features that are largely orthogonal to each other. You can compose these features together to create some truly monstrous types but I don't see interconnected rules.

2 Likes

Dude you linked me this

public:
generic <typename TRoot>
 where TRoot : Microsoft::CodeAnalysis::SyntaxNode[System::Runtime::CompilerServices::Extension]
 static TRoot ReplaceSyntax(TRoot root, System::Collections::Generic::IEnumerable<Microsoft::CodeAnalysis::SyntaxNode ^> ^ nodes, Func<Microsoft::CodeAnalysis::SyntaxNode ^, Microsoft::CodeAnalysis::SyntaxNode ^, Microsoft::CodeAnalysis::SyntaxNode ^> ^ computeReplacementNode, System::Collections::Generic::IEnumerable<Microsoft::CodeAnalysis::SyntaxToken> ^ tokens, Func<Microsoft::CodeAnalysis::SyntaxToken, Microsoft::CodeAnalysis::SyntaxToken, Microsoft::CodeAnalysis::SyntaxToken> ^ computeReplacementToken, System::Collections::Generic::IEnumerable<Microsoft::CodeAnalysis::SyntaxTrivia> ^ trivia, Func<Microsoft::CodeAnalysis::SyntaxTrivia, Microsoft::CodeAnalysis::SyntaxTrivia, Microsoft::CodeAnalysis::SyntaxTrivia> ^ computeReplacementTrivia);

That's visual C++. Your second link, this time to github, is C#.

I'm not talking about having too many features. Just that they are allowed to interplay in interesting and complex ways and that complexity is too damn high. For me. How long have you been programming in Rust? How long does it take you to grok code that's out there in the ecosystem?

Don't know why Ownership gets a bad rap. Doesn't even make me flinch and I like it, its nice. I would like to know if you could take what I showed and make it reasonably more complex. I think it would be challenging.

[EDIT]
The drop down saves your preference in a cookie or something. I think you saw the C# because you previously selected that, but it gave me the default choice. Extended C++

1 Like

My point is the same. That function is implemented in C# (the MSDN documentation in the first link that you're viewing in C++ is generated from the C# code in the second link). Any language with generics can create very convoluted types as you demonstrated in Rust.

Iā€™m not talking about having too many features. Just that they are allowed to interplay in interesting and complex ways and that complexity is too damn high. For me. How long have you been programming in Rust? How long does it take you to grok code thatā€™s out there in the ecosystem?

I'm not trying to downplay your experiences, I just want to understand what you find confusing. It's great when people post experiences like this because it gives us the opportunity to improve. :slight_smile:

The question is what should be improved? Is there not enough documentation? Is the compiler giving bad or unhelpful error messages? Is rustdoc doing something weird with the iterator documentation?

Donā€™t know why Ownership gets a bad rap. Doesnā€™t even make me flinch and I like it, its nice. I would like to know if you could take what I showed and make it reasonably more complex. I think it would be challenging.

IMO, the reason for this is because you have to understand 95% of the ownership system right off the bat to do anything in Rust. That's not generally how people learn; people learn a simplified model and then they refine that overtime into a more accurate, more complex model. Traits/Generics on the other hand, generally doesn't require you to understand the entire thing up front. Most people start by using generic types like Vec<T> or Option<T>. They write simple, non generic traits at first. Then perhaps they learn to make their code generic. Then they learn about implementing generic traits. Then generic constraints and associated types, etc.

The project you're working on (or perhaps just your chosen approach) is causing you to confront the full complexity of the type system head-on. That's different than many other kinds of "learning Rust" projects which would gradually introduce you to more parts of the language. Even so, it would be great to smooth this out so that the next person to travel down this road doesn't find it quite as bumpy as you :slight_smile:

3 Likes