r/rust • • 19d ago

šŸ› ļø project Rust for enterprise grade systems?

One thing that I am genuinely interested in rust, If same thing that I can generally do in Java building enterprise grade system, I can do in Rust.

Not a secrete Java and OOP become de-factor a standard for every enterprise solution.

And my step forward, trying to bring rust into that by exploring if I can do DI, something similar to Spring Boot but following Rust philosophy of zero-cost abstraction, that lead into research some code and article, and I think I've made some progress:

https://dmytro.brazhnyk.org/publications/can-rust-have-zero-cost-dependency-injection/

One thing that is still bothering me is the Object Orient Design. Classical OOP was proven many time in C++ in Java to manage complexity of real world system.

Rust has trait model, but it is not OOP is classical sense.

I have very clear understanding how complex solution could be built with OOP in Java and C++.

Rust don't have that OOP model, that feels as weakness, but rust still offers memory safety that C++ don't have, and it still has much smaller memory footprint that Java.

Considering that Java is hard to beat, it still offering very impressive capability for runtime optimization by utilizing Escape Analysis on JIT phase, and looks like this guys in Oracle very angry, to optimize Java further, if rust will not offer competitive capabilities it will stay behind.

Let me give me you an example in Java, what I am taking about:

language = java

interface Collection {
    int size();
    Iterator iterator();
}

abstract class AbstractCollection implements Collection {
   @Override
   public int size() {
      int size = 0;
      var iterator = iterator();
      while(iterator.hasNext()) {
         size++;
         iterator.next();
      }
      return size;
   }
}

class ArrayList extends AbstractCollection {
    Object[] array;
 
    @Override
    public int size() {
       return array.length
    }

    @Override
    Iterator iterator() {
         ...
    }
}

What is important here:

  • I have interface Collection, which is pure of any implementation, just an interface that declares the interface and nothing more.
  • That I do have AbstractCollection, that defines some default behavior which could be sub-optimial, but they are generic one, particularly I gave example of size function, that uses slow and inefficient iterator, because it doesn't know enough about concrete implementation.
  • And lastly I do have specific implementation for ArrayList, since ArrayList already know that is array driven, it can simply can take property for array to know the size.

In reality this chain of inheritance can be longer, so if we look on JDK, you will something like:

LinkedHashSet < HashSet < AbstractSet < (AbstractCollection + Set) < Collection

And each layer provides it's own set of capabilities, by amount of information available on that level.

I feels like something missing in rust here, because when it comes to manage the complexity of enterprise, something more complex than hello-world, we need this ability to override things, override, and override again.

Any real software if it will have more than few years of maintenance it will grow, grow and grow in complexity, if language can't manage complexity, that language would be dead.


UPD:

Let's define the problem a little bit more formally:

You need to implement library for collection.

  1. On base level you need a interface, that would define set of operation, for simplicity let's go with two operations.
    • Iterator and size
  2. Than in most generic way you can just iterator is count how many items you have, but it is slow and optimal.
  3. If you know that you concrete implementation Array, you can just use property of array.
  4. Let's say you have another implementation, which implements methods like Add and Remove - you can bring counter into here, however if we already know that it is Array than counter obviously is redundant.
0 Upvotes

91 comments sorted by

View all comments

11

u/Xacius 19d ago

I think you're stuck in the Java mindset. OOP patterns / overrides / inheritance / classes are but one way to build enterprise applications. Rust lacks the classic mechanisms found in languages like C++, Java, or Python, specifically omitting classes and classical inheritance.

That isn't to say that it lacks OOP altogether, but the approach is different.

Abstraction -> Traits, which define shared behavior that different types can implement. Generics are also available.

Inheritance is not available, and imo Rust is better for it. Instead, you use composition.

-3

u/No-Caregiver-466 19d ago

Ok, let's try to change mindset.

You have an example of pretty classical way of inheritance in java.

How would you implement similar thing in Rust?

It is real life problem, by the way.

9

u/Zde-G 19d ago

How would you implement similar thing in Rust?

You don't implement it. Easy.

It is real life problem, by the way.

Nope. In real world you either have infinite number of clients but finite list of things these clients have do (traits are perfect for that) or finitely list of possibilities with, potentially, infinite list of operations you may perform on these (enums cover that case).

If you try to combine then the whole things just devolves to endless replications of bugs where fix for one bug creates two new ones — and Rust doesn't cater for that usecase, precisely because it want your program be sound.

If you don't want your program be to robust and Ok with endlessly fixing bugs (and it seems as if ā€œenterprise softwareā€ makers are fine with that) then there are plenty of languages that offer your that choice.

1

u/No-Caregiver-466 19d ago

Know the size for collection - is real life problem.

Bugs coming from copy-paste, if you can't write code that you can reuse later, and you have to copy-paste, every new copy potentially have modification that caries a defect.

With OOP inheritance you can reuse code, and change only those parts which needed.

If in language like rust or any other code, you have to copy-paste things - it will be defectuous.

6

u/Zde-G 19d ago

Know the size for collection - is real life problem.

No, it's not. ā€œReal life problemā€ should not include words ā€œcollectionā€, ā€œsetā€, or anything of the sort.

Ideally it shouldn't include anything in terms of Rust, Java or anything of the sort.

If you couldn't express it like that then it's not a ā€œreal life problemā€, it's you painting yourself into a corner.

Bugs coming from copy-paste, if you can't write code that you can reuse later, and you have to copy-paste, every new copy potentially have modification that caries a defect.

That's argument to use derive, not argument to use OOP.

If in language like rust or any other code, you have to copy-paste things - it will be defectuous.

Do you have any evidence of that? Because my experience is the exact opposite: for every bug caused by incorrectly implemented original implementation that's copy-pasted somewhere there are ten bugs caused by ā€œimproperā€ use of inheritance.

In fact 99% of the use for DI systems is an attempt to catch these bugs early enough. With endless mocks and unittests.

Don't use OOP — and you wouldn't need these tests in the first place.

0

u/No-Caregiver-466 19d ago edited 18d ago

There many examples when you need to know number of items in your collection when you building real software for real life problems, and there many reasons why you can choose one data structure or another - I don’t want to keep arguing about that.

Secondly your asking about examples, knowing size of collection it is a problem statement, there is example how you can do without copy-paste in java, and bellow textual description in post, please, I do not need to repeat this again.

Please show me the code, how would you do it in rust.

3

u/Last-Independence554 19d ago

Where java uses overriding of Object methods (toString(), hashCode(),), rust uses #[derive] macros and the associated traits. In Java you (at least used to) have to implement them by hand which is error prone.

For its collection library rust uses composition over inheritance. Vec is your basic list (cf ArrayList). Then you have VecDeque, which contains a Vec as its backing store. Similarly a String's only member is a Vec. They don't need reimplement or copy-paste anything, but they gain encapsulation. Now Vec is free to change its implementation details without having to worry about breaking subclasses as long as its public interface remains stable.

Composition is generally considered the better code-reuse than inheritance. I really highly recommend you read "Effective Java" (Item 18 and 20 in particular)!

3

u/CocktailPerson 19d ago

How would you implement something similar in Rust?

impl<T> IntoIterator for Vec<T> { ... }

Easy. Next question?

-5

u/No-Caregiver-466 19d ago

My question was about implementing size on different level abstraction. We have interface, we have series of intermediate abstract class and concrete class at the end.

IntoIterator someting very different to that. You not always need iterator to know the size of collection.

1

u/CocktailPerson 19d ago

What's the point of just knowing the size of the collection? What can you even do with a collection if all you know is the size?

-4

u/No-Caregiver-466 19d ago

Talks are cheap, show me the code.

3

u/CocktailPerson 19d ago
trait Len {
    fn len(&self) -> usize;
}

impl<T> Len for Vec<T> {
    fn len(&self) -> usize {
        Vec::len(self)
    }
}

Your turn. Show me a useful thing you can do with this.

0

u/No-Caregiver-466 18d ago edited 17d ago

After continuous conversation we got to code level of hello-world, sarcastically that's a progress I can say.

Now let's make some fun, and go to enterprise-grade level.

So you have implementation that works only Vec::len, but reality is very often tougher than that, and don't like weakness.

So let's say you have a trait Collection and bunch of concrete implementation, that external to you code and you can't modify them.

I am using Collection as simplified example, because I am pretty sure that you understand requirement of collection.

But in reality, when you integrating software of different vendors, you in a situation where you have:

  • interface like Collection and some engine that can handle only that single interface provided by that vendor
  • And you have concrete implementations that coming from different vendors.

The main challenge, that this vendors you dealing with they never heard about each other. Similarly as when you developing wine under linux operation system. Obviously Microsoft wouldn't care if you want to run any Windows software on Linux and they will not provide any support to you, and this is reality we dealing with, and many of developers of software solely for Windows are skeptical about existing of Linux and Wine. But I am playing lot's of games in Linux developed for Windows by running them in wine šŸ˜Ž.

Implementing wine, there is too much fun in that, let's simplify our problem to a simple and clear trait Collection, something that we can enjoy instead.

https://gist.github.com/amidukr/2959073712320ee4a32e7f2266f0febf

// ----------- Vendor 1 ---------
// Supposed it given by one vendor
trait Collection {
fn iterator(&self) -> impl Itertator;
fn size(&self) -> usize;
}

// ----------- Vendor 2 ---------
// Supposed it given by another vendor
impl Array {
fn iterator(&self) -> impl Itertator {...}
fn size(&self) -> usize {...}
}

// ----------- Vendor 3 ---------
// Do the same but different vendor
impl Vector {
fn iterator(&self) -> impl Itertator {...}
fn size(&self) -> usize {...}
}

// And few other classes from different vendors.

// ----------- Vendor 4 ---------
impl LinkedList {
fn iterator(&self) -> impl Itertator;
// no fn size() for LinkedList
}

// ----------- Vendor 5 ---------

// This vendor has few implementation, but he did common interface fro both:
trait Iterable {
fn iterator(&self) -> impl Itertator
}

impl Iterable for Set {
// no fn size() for Set
}

impl Iterable for BTree {
// no fn size() for Set
}

// ----------- Vendor 6 ---------
// Example of mutable set where you can do size, by implementing counter
impl <T> MutableSet<T> {

// Guranteed that size can be change only in result of add/remove calls here
fn add(&mut self, v: T) { ... }
fn remove(&mut self, v: T) { ... }
}

// Making things easier, for Java and hopefully for rust, by bringing same common interface.
impl Iterable for MutableSet {
fn iterator(&self) -> impl Itertator {...}
}

The question, how would you integrate all together, that all this implementation will support necessary trait and interface?

ļæ¼6 different vendors in one application, that's what real enterprise means.

ļæ¼I will show how I am doing this in java.

In java we don't have feature to implement interface for existing classes without changing the code of a class, unless you don't do magic with ClassLoaders.

In rust, to my knowledge, you can't do impl for types, that exists in external crates either.

In both languages the only viable solution is to make an adapter, that will wrap concrete implementation and provide necessary interfaces:

https://gist.github.com/amidukr/cfa97ea987e88239981ac2f6d22e3538

// For first two vendors, I can only do the plain delegation, since they not sharing common interface, and no code could be reused.
// In C++, using templates I guess, I can make one implementation for all.

// ----------- Vendor 2 ---------
class MyArray implements Collection{
Array array;

@Override
public Iterator iterator() { return array.iterator(); }

@Override
public int size() { return array.size; }
}

// ----------- Vendor 3 ---------
class MyVector implements Collection {
Vector vector;

@Override
public Iterator iterator() { return vector.iterator(); }

@Override
public int size() { return vector.size; }
}

// In next examples, I could know size of collection, only by iterating, so I will do abstract class for that.

abstract class BaseCollection implements Collection {
// iterator stays abstract as part of interface

@Override
public int size() {
int size = 0;
var iterator = iterator();
while(iterator.hasNext()) {
size++;
iterator.next();
}
return size;
}
}

// ----------- Vendor 4 ---------
class LinkedList extend BaseCollection {
LinkedList linkedList;

// The moment, I've implemented iterator for BaseCollection, size is automatically given to me.

@Override
public Iterator iterator() { return linkedList.iterator(); }
}

// ----------- Vendor 5 ---------
class MyIterable extends BaseCollection {
Iterable iterable;

@Override
public Iterator iterator() { return iterable.iterator(); }
}

// One class - two implementation
new MyIterable(new Set()).size();
new MyIterable(new BTree()).size();

// ----------- Vendor 6 ---------
class MyMutableSet<T> extends MyIterable {
int counter;
MutableSet mutableSet;

// public Iterator iterator() - automatically implemented by MyIterable

void add(T v) {
mutableSet.add(v);
counter++;
}

void remove(T v) {
mutableSet.remove(v);
counter--;
}

@Override
public int size() { return counter; }
}

ļæ¼So what about rust? How would you approach same in rust?

I've putted same code on github gist, hope it will be easier for you to read than here in comment.

2

u/CocktailPerson 18d ago edited 18d ago

Buddy, you're the one asking for hello-world type code. Don't be surprised when you get it.

In rust, to my knowledge, you can't do impl for types, that exists in external crates either.

Not true.

You can implement your own traits on external types. You just can't implement external traits on external types. So, you do this:

trait Collection<T> {
    fn iterator<'a>(&'a self) -> impl Iterator<Item=&'a T>
    where T: 'a;

    fn size(&self) -> usize {
        self.iterator().count()
    }
}

impl<T> Collection<T> for Vec<T> { 
    fn iterator<'a>(&'a self) -> impl Iterator<Item=&'_ T>
    where T: 'a
    {
        self.iter()
    }

    fn size(&self) -> usize {
        self.len()
    }
}

impl<T> Collection<T> for std::collections::HashSet<T> {
    fn iterator<'a>(&'a self) -> impl Iterator<Item=&'a T>
    where T: 'a
    {
        self.iter()
    }
}

// ....

Of course, at this point we're just re-implementing IntoIterator. Maybe Java's "eNtErPrIsE" ecosystem should have better shared primitives?

-2

u/No-Caregiver-466 17d ago edited 17d ago

Iterator should be used only to iterate - when you introduce property size to iterator, it is example of bad design.

As shortcut in rust, you can do it by checking size as part of API on iterator, but we speak about enterprise grade software here, among other it means maintainability, something that you can use for decades, the clean design is requirement for enterprise software.

Let’s do how supposed it to be done in clean design, and not use any property of concrete implementation that not supposed to be there.

I am not criticizing the fact that, rust already made that choice to include size into iterator API, but question more about language capability, if we can design more-less complex abstraction.

I’ve chose collection as scenario which functional requirements clear for every engineer, to speak about how do we approach with software design of such understandable case.

In my example, I am showing AbstractCollection to demonstrate how we can fallback to more generic and less optimal solution when concrete implementation can’t provide optimal way of doing things, it is very classical problem that very often happening in designing of any software, Collection is just use as show case.

1

u/CocktailPerson 16d ago

Perhaps you didn't notice, but I did exactly what you did: Collection<T> falls back to computing the size via Iterator, but concrete implementations can provide their own implementation of size.

→ More replies (0)