quilt-standard-libraries icon indicating copy to clipboard operation
quilt-standard-libraries copied to clipboard

Musings on Soft Intermodule Dependencies

Open Haven-King opened this issue 2 years ago • 3 comments

This discussion is relevant to both QSL and our eventual quilt-gradle plugin, but this seems like the best place to have such a discussion for now.

Currently, there is a hesitation to add intermodule dependencies to Quilt's standard libraries that is carried over from a similar mindset in Fabric API. In Fabric API, it makes a fairly small amount of sense, since Fabric API is packaged and distributed as a monolith in 99% of situations anyway. In QSL, on the other hand, avoiding inter-module dependencies makes a great deal of sense, as it allows dependency downloading to avoid downloading more niche modules that not many mods will need.

There are, however, situations where intermodule dependencies could be used to make modules easier to use. I will use a config API as an example. Let's say we were to break the config API into the following modules: qsl_config_base, qsl_config_screens, qsl_config_sync, and qsl_config_gamerules. qsl_config_base might provide a builder that makes it easy for mods to build a config tree:

Note: Any code shown is for illustration purposes only and is not necessarily representative of any actual config API.

public final class ConfigBuilder {
  <T> void addField(String name, Class<T> type, T defaultValue) { ... }
  Config build() { ... }
}

qsl_config_base should not have a hard dependency on any of the other three modules, but what if methods themselves could have dependencies?

public final class ConfigBuilder {
  <T> ConfigBuilder addField(String name, Class<T> type, T defaultValue) { ... }
  
  @Requires("org.quiltmc:qsl_config_screens:1.x")
  ConfigBuilder registerScreen() { ... }

  @Requires("org.quiltmc:qsl_config_sync:1.x")
  ConfigBuilder sync() { ... }

  @Requires("org.quiltmc:qsl_config_gamerules:1.x", "org.quiltmc:qsl_content_other_gamerules:1.x")
  ConfigBuilder addGameRules() { ... }

  Config build() { 
    if (checkOrThrow(this.registerConfigSceen, "qsl_config_screens")) { ... }
    if (checkOrThrow(this.isSynced, "qsl_config_sync")) { ... }
    if (checkOrThrow(this.registerGameRules, "qsl_config_gamerules")) { ... }

    ...
  }
  
  private static boolean checkOrThrow(boolean required, String module) {
    boolean present = QuiltLoader.isModLoaded(module);

    if (required && !present) {
      throw new SomeException();
    }

    return present;
  }
}

These annotations could be processed at build time by quilt-gradle to add dependencies to the built mod if the annotated methods are referenced in any way. Such an annotation could also be added to classes, such that extending or instantiating an annotated class would also indicate that a mod has a dependency on the indicated module(s). This could be used by other libraries as well, such as Cardinal Components having only their synced components require qsl_networking.

I'm not sure this is the best approach, but I thought it was a discussion worth having. I did ideate most of this while exhausted and falling asleep, so forgive me if anything is not entirely coherent.

Haven-King avatar Aug 25 '21 18:08 Haven-King

Looking for thoughts from @QuiltMC/quilt-standard-libraries @zml2008

Haven-King avatar Aug 25 '21 19:08 Haven-King

This is somewhat similar to the idea that lead to the current work on CHASM.

To set the stage, at the time QSL was going to consist of 10 or so libraries that were manually depended on, instead of the current automatic module dependency system we have today. As such, some of the hard problems (how do we determine where an extension-added method came from?) didn't exist, because everything was explicit.

My original plan was to introduce a basic system for purely additive "extension methods" (similar to the concept of what's seen in Kotlin, and to a lesser extent Rust) that were applied at compile-time. In your example, this would look something like:

@Extension(ConfigBuilder.class)
final class ConfigBuilderSyncExtensions {
     @Instance
     public static final ConfigBuilder sync(ConfigBuilder instance) {
        // code 
    }
}

The end result would be the same as Haven's annotated example above, except you would see the extension methods if and only if you had explicitly depended on the library that provided the extension. I feel like Haven's approach here is better than what I had, especially since mine would require using decompiled sources of modules and doesn't handle the build() problem, but I think it should still be documented here.

TheGlitch76 avatar Aug 26 '21 02:08 TheGlitch76

Currently, there is a hesitation to add intermodule dependencies to Quilt's standard libraries that is carried over from a similar mindset in Fabric API. In Fabric API, it makes a fairly small amount of sense, since Fabric API is packaged and distributed as a monolith in 99% of situations anyway.

To clarify: The "endgame" goal on our side was for automatic dependency resolution in fabric-loom, and packaging the modules a given mod needs with the mod itself automatically as a result. Distributing Fabric API as a monolith always felt like a bit of a hack.

asiekierka avatar Nov 29 '21 21:11 asiekierka