The abstract module that you describe has the following basic properties:
- This is a closed module, meaning that it provides an entire interface that allows you to interact with the module
- You can specify operations on the module
- You can specify types that are part of the module - usually these types are controlled by the above modules
- The implementation is not provided - this is the abstract part: there can be many specific implementations and which is actually used in the program will be set and selected by the program, since it is best suited
A function that allows you to specify a module that uses more than one source file is not a basic requirement, but it may come in handy.
In its basic form, the module describes an abstract data type (for example, a queue): what operations are available for interacting with a data type and any auxiliary types necessary for interaction.
In a more complex form, it can describe a whole subsystem (for example, a network).
In imperative languages, you usually use an interface for the same purpose:
- Attached
- You can specify operations
- You can specify types that are part of the interface.
- No implementation
As you already mentioned, if your module has a large interface (for example, describes a subsystem), it is usually impractical to write classes that implement a rich interface in a single file. If the language does not provide support for splitting the same class into separate sources (or more precisely: for “gluing” separate parts of the same class from different source files together), the solution is usually to lose the attached requirement and ensure a series of interfaces that define the interactions between them - this way you get an API for a subsystem (this is an API in the pure sense: this is an interface for interacting with a subsystem, without implementation yet).
In a sense, this latter approach may be more general (general in the sense of what you can achieve with it) than the closed type: you can provide the implementation of various subtypes (defined via interfaces) from different authors: while the subtypes rely only on the specified interface to interact with each other, this mix-n-match approach will work.
One of the strengths of most functional programming languages is parameterized data types, where you create a dayatype type with another as your parameter (for example, an integer queue). The same flexibility is achieved with Generics in Java / C # (and C ++ templates). Of course, the exact consequences and expressive power may vary between languages based on their type system.
This entire discussion is a separate form of Injection Dependency (DI), which attempts to weaken the strong relationship between a particular type implementation and its supporting parts by explicitly providing the necessary parts (as opposed to choosing an implementation), since the type user can better understand which implementation of these parts best to achieve your goal - for example, by providing mock implementations for testing functionality.
The DI problem is trying to solve exclusively for imperative languages, you can have the same dependency problems in functional languages: an abstract module implementation can use a specific subtype implementation (thus linking itself to these implementations) instead of using subtype implementations as parameters (to what DI strives for)