Composable modules

The idea

// Def
trait DoSomething {
    fn do_something();
}

mod foo : DoSomething {
    fn do_something() { ... }
}

mod bar : DoSomething {
    fn do_something() { ... }
}

mod baz {
    a: DoSomething;
    b: DoSomething;
    fn do_something() {
        self::b::do_something();
        self::c::do_something();
    }
}

// Usage
baz::<a=foo, b=bar>::do_something();

Why?

I prefer to separate business logic from data. (It is because I did some functional programming in the past. E.g., That's what elixir does.) Currently, rust does the opposite. We bind methods to data which makes everything stateful. If I want to follow this idea in rust, the code became unmanageable quickly, especially when I want to make it generic enough for unit tests. The above idea ease this by allowing modules to use other modules dynamically so that I could use modules instead of structs.

Why Modules?

I don't want state.
State is crucial in programming, but most of the Request -> Response subsystems can be implemented without state. So I would definitely use this for REST APIs. And that's a completely different world. Without states, some of the rust rules could be simplified.

Why Composable?

  1. For tests: baz::<a=fake_foo, b=fake_bar>::do_something();
  2. For different functionality:
    crud_controller::<repo=user_repo>::list::<User>::();
    crud_controller::<repo=order_repo>::list::<Order>::();

Let's play with the idea.

EDIT

A bit more specific description

  1. Modules could define module vars, and everything could be accessible through module vars. e.g.: foo has a module var bar, thus inside foo we can access everything through self::bar:: syntax.

  2. To ensure the elements in a module, there would be a need for module traits, which is an ordinary trait. But we could use them for modules, e.g.: mod foo: SomeTrait + SomeOtherTrait.

  3. Defining Module vars:

    mod foo {
         some_name: SomeTrait;
         some_other_name: SomeOtherTrait;

         // ...
    }
  1. The usage would be similar to the named type arguments baz::<a=foo, b=bar>::do_something();.

  2. Interpretation: for compiler, this would work like generics, so:

    // this would define a `do_something` function which would use `bar`
    foo::<a=bar>::do_something();
    // this would define another `do_something` function which would use `baz`
    foo::<a=baz>::do_something();

How this could help?

Instead of this:

struct Baz<A, B>(std::marker::PhantomData<(A, B)>);
impl<A: DoSomething, B: DoSomething> DoSomething for Baz<A, B> {
    fn do_something() {
        A::do_something();
        B::do_something();
    }
}

Or instead of this:

struct Baz<A, B>{
 a: A,
 b: B
);
impl<A: DoSomething, B: DoSomething> DoSomething for Baz<A, B> {
    fn do_something() {
        A::do_something();
        B::do_something();
    }
}

we would be able to write this:

mod baz {
    a: DoSomething;
    b: DoSomething;
    fn do_something() {
        self::b::do_something();
        self::c::do_something();
    }
}
1 Like

Aren't structs enough?

trait DoSomething {
    fn do_something();
}

struct Foo;
impl DoSomething for Foo {
    fn do_something() { ... }
}

struct Bar;
impl DoSomething for Bar {
    fn do_something() { ... }
}

struct Baz<A: DoSomething, B: DoSomething>(std::marker::PhantomData<(A, B)>);
impl<A: DoSomething, B: DoSomething> DoSomething for Baz<A, B> {
    fn do_something() {
        A::do_something();
        B::do_something();
    }
}

Baz::<Foo, Bar>::do_something();
6 Likes

Structs fulfill the needs. But they can provide more, and because of this, they need more boilerplate code.

For e.g.:

struct Something {
   foo: impl Foo
}

is not allowed.

But modules are a different world, where we could put trait bounds on the module definition. Maybe there are other semantic differences.

I don't understand what your end goal is. If you are just calling free functions how will requiring them to be the same signature help?

Wouldn't just calling them with the same inputs, or putting them into a map of some kind enforce those requirements?

7 Likes

I don't have a goal. I see that composable modules could help to reduce the boilerplate for stateless programs.

I think the same signature is unnecessary if the compiler finds out which module could fit in and which not.

I wouldn't put them in a map, but that's a good point; maybe I would. And that would be interesting because modules are not data. Currently, I don't see any use case for that, though.

AFAIK this code codesn’t compile right now, but should in the future once the work on existential has progressed.

3 Likes

In this case, please refrain from making an RFC. Changing the language l'art pour l'art is neither productive nor useful.

This is very abstract. You didn't even define what "composable modules" are, you just wrote some hypothetical code without explanation. Furthermore, your assertion that the way Rust programs are organized is messy is not really true according to real-world experience of many Rust users.

If this was meant to be an anti-OO thread: Rust is not an OO language, and you don't have to use it as such. You can create free functions as much as you like, and the language features a powerful type system and metaprogramming capabilities that are capable of expressing basically any sensible code organization requirement.

8 Likes

I would love to see this feature, However, as I know it is considered an anti-pattern.

Your example with impl Trait -typed struct fields effectively proposes global type inference, which is highly problematic for another, different set of reasons, and Rust's design explicitly avoids it, and this fact has been confirmed several times by the community and the team when it came up in the past. So that part of the proposal doesn't fit with the direction of the language, either. -- @H2CO3 Improve boilerplate for Trait impl · Issue #2676 · rust-lang/rfcs · GitHub

1 Like

This is very abstract. You didn't even define what "composable modules" are, you just wrote some hypothetical code without explanation. Furthermore, your assertion that the way Rust programs are organized is messy is not really true according to real-world experience of many Rust users.

I didn't want to specify it more until we need it. I have updated the post.

It sounds like the main thing you're after here is generic modules. This has come up a few times before (these are the first two examples I found):

2 Likes

Modules can't currently define state in the form of bindings. Just adding that would be a big change in design and a huge change to Rust's philosophy. It would also encourage people to use more static global state, because since modules cannot be instantiated, that's exactly what those bindings would be.

In addition, re the composability, bindings don't currently interact with the type system, and so just making something like baz::<a=fake_foo, b=fake_bar>::do_something(); work is such a huge change. So huge in fact that it wouldn't surprise me that if everyone dropped everything and started working on this, it still wouldn't be done in 2022.

3 Likes

It sounds like the main thing you're after here is generic modules. This has come up a few times before (these are the first two examples I found):

Indeed. The first topic discusses generic type parameters for modules, whereas I want generic module parameters for modules, so that's not relevant.
The second topic is almost the same, but it's closed due to timeout.

It would also encourage people to use more static global state, because since modules cannot be instantiated, that's exactly what those bindings would be.

Agreeing here. I wouldn't use it, but people probably would. Especially in situations where

A has B  
      B has C

But mod B requires some state and modules A, C requires B to be a module. For example, Runtime cache for repositories. That would be an abuse of the pattern.

I used to think that, using a state is better than using static but not the best in this situation because state in other languages means impure hard-to-test functions and the best would be passing the data to functions instead. However, as I was writing this I realized that with rust it is already easy to test stateful logic because we can init the struct with anything that I want, therefore it doesn't improve the code if I don't use state.

Thanks, Good point.

Any proposal should (must?) start with a clear goal that is well motivated. Without a goal, there is no point in even discussing a proposal (IMHO).

8 Likes

It is an idea, not a proposal, but thanks for your comment.

1 Like

I didn't intend to sound harsh, so if that is the way it came off, my apologies.

It is important for and idea (or proposal) to start with a problem statement and a motivation (goal). The problem statement should explain how there is currently no way to solve the problem, or the solution to the problem currently is too difficult or unergonomic. The motivation should explain why having the problem solved will be good for the community of Rust users at-large and not just a small, isolated group. Also, solving the problem should be a problem somewhat frequently encountered by large numbers of Rust users.

It is really difficult to discuss a proposal or idea that lacks a motivating goal because you can't explore properly what solutions might already exist for the problem. Also, it is important to clearly state the goal so as to avoid the X/Y Problem.

16 Likes

And to add to that list, without a problem/goal statement, it becomes impossible to evaluate if any proposed solution actually is a solution.

13 Likes