Ada has the best modularity of all the programming languages I have used.
It was designed for building large real-world applications and separates specification from implementation by using separately compilable specification and body files:
The US military uses it less than they should, the mandate didn't last long, no clue about other nations. Aviation should also use it more than they do, but most new systems get a choice between: C, C++, Ada, and maybe Fortran. It's usually up to the subsystem developer to determine which language(s) they will use rather than mandated from above.
In particular because most planes (this has been true for a long time but become increasingly true over the last 30 years) have highly modular designs with respect to these kind of components. An ICD (interface control document) will lay out the physical and digital interface requirements, but the internals are mostly left to the developer. This includes the language choice.
Pretty much every language today has a modularity story. The issue isn't the language, it's deciding what are the modules and how they depend on / integrate with each other. You can have really good modularity support in the language and still end up with a big ball of mud.
IMO, you always need what I call an "application meta-model"; a set of decisions/constraints you enforce to make developing a large monolith sane. Examples: integration between two features should be implemented in a separate module that depend on both features, such that neither feature depend on the other; exposing well-defined interfaces from modules for functionality that is expected by cross-cutting concerns, e.g. security/logging/caching/etc.
Pascal - I've been writing units in Turbo Pascal, Delphi and Free Pascal. I wrote a cooperative multitasker for it under DOS back in 1987.[1] Units have been part of it for a very long time.
The advantage is that the Interface, Implementation and Initialization are all in one source file, and dependencies are resolved automatically. You don't need a separate build system.
Plus, strings just work. You don't have to allocate them, dispose of them, etc.
If you like Pascal, you will love MODULA 2/3 for its brevity and expressiveness. As its name implies, MODULA 2 is predestined for building modular software. One special feature of MODULA 2 is the concurrence routines.
I hated Modula 2 when I looked at it, it was broken is some really irritating ways. Taking a quick look now, as I compose this
It's CaSe SeNsItIvE which is NUTS, it's one of the worst aspects of C
You have to specify every function you import from a module? That's NUTS too
Matching the names when you end a block... that actually sounds reasonable
Strings as Array of Char? No... not back to the insanity of C strings
I don't see any added features that make jumping to an even less supported language something appealing.
> What programming language would you choose if you wanted to make a modular monolithic architecture?
i'm sort of cheating, but: if you add more context, the decision will likely be easier. what's the architecture for? what are the key constraints of your context that you need to optimise for? is modularity the primary constraint, or is it secondary or tertiary to other concerns? e.g. if the architecture will be used to build engines for fancy AAA games, where memory bandwidth is a major consideration, maybe your choices are limited to C/C++ so you can squeeze the most out of the hardware, and make use of existing middleware. if your monolithic architecture needs to be adopted by a company that has 1000s of developers who are familiar with java, then maybe to make it easier to sell internally, it needs to be implemented in java, so the switch cost is reduced for teams who you're trying to convince to adopt it. if you're building some saas thing, then you are probably going to end up using javascript for some of it. if the architecture will be used by teams with large numbers of recent grads / junior devs to deliver projects or ship features, maybe it's best if the language doesn't contain too many huge foot-guns.
If you wanted the modularity of microservices, but couldn't use microservices and were forced to use one process - what would you use.
We want to force programmers through the language to only go through services' public interface - and if possible prevent them from even knowing about the private data,classes, and methods of the module internally.
If Go is a feasible option [1], each service could be structured as its own module, with all of its private data and types placed in packages inside the module's special internal/ package, which has special support from the compiler:
> internal/ is a special directory name recognised by the go tool which will prevent one package from being imported by another unless both share a common ancestor. Packages within an internal/ directory are therefore said to be internal packages.
It'd be the responsibility of the owner of each service to opt-in and put private things in their internal/ package, and not expose them as part of the service's module's public interface to other modules.
More generally, if you have a lot of influence over how your services are written and maintained, even for languages and tooling that doesn't give you a built-in way to enforce that external dependencies on private details of other components are not introduced, it might not be that hard to write a custom linter script to analyse all the imports in your project and fail the build if any change introduces a forbidden dependency.
Re: running things in a single process. With Go, suppose you craft a number of services wrapped in their own packages with well-designed public interfaces, and all private details hidden via the compiler-enforced internal/ package mechanism. There are still scenarios where the modularity can be horribly broken, and the implementation details matter, especially if you're running everything on a single machine or in a single process: e.g. suppose the private internals of one service want to leak all of the TCP buffer memory or open all the file handles, that'll likely break the internals of some other service that is competing for the same shared resources. Similarly, suppose some internal code deep in the bowels of one service triggers a panic inside a goroutine that isn't trapped, then the go runtime will react by terminating the whole process, taking down all of your services. The language-level modularity doesn't give you a way to enforce that services running in the same process or in the same machine can't trash shared resources.
[1] Go is a pretty good fit for writing backend web services, and less of a good fit for, say, shipping mobile apps, or prototyping machine learning algorithms.
How do you define modularity? Written modular code in many languages.
Me experience: functional constructs and declarative problem solving helps in grand scheme, as does putting your time into building missing abstractions or using right library.
with big monolithic architecture not only modularity important. also good if language has good set of libraries. also easier to maintain with strong typing. no strong typing in large code base is deal breaker to me.
I was looking into java and kotlin - but even the people talking about modular monoliths weren't happy with the current module support in java.
The language should support separate teams writing modules to be used by other teams. The modules themselves would publish their public methods but really allow a group to turn up the encapsulation and hiding to the max. Users of a module would be forced to not even know about private variables or methods. They'd just get a compiled artifact and documentation.
As someone who has done a lot of development from Assembly, C, C++, Delphi, Java , Clojure, Javascript, Elixir etc, in my opinion Delphi's component/modular structure is completely unparalleled.
The Delphi component model - and the Visual Component Library (VCL) is such that:
a) It is trivial to create either visual or non-visual components for Delphi which can be installed at runtime into the IDE using its own registration process which is also trivial.
b) Once registered in the IDE, these components can be drag-dropped onto the forms and setup using "property sheets" which can also be heavily customised by the components. These property sheets can be simple screens or even very sophisticated screens with tabs etc. It is possible for components to even have run time property sheets.
c) The components can be installed as packs
d) These components can also be used at runtime without being registered into the IDE.
e) The components can be linked into the application staticly
Other than Delphi, I find the PHP's ability - especially in apps like Wordpress, to support a range of plugins that can be installed/uninstalled at runtime to be extremely powerful.
I mean they can all be. It's more where is it running..
Ruby has gem and rails has engine support, you could build most of the code in gems and mount them as engines so their url's and paths would be isolated.
Or you could just be super specific about how you build things from day 1.
A recent app I worked on had no logic in models or controllers, but everything was in name-spaced active_jobs. Super easy to find what you're looking for and DRY was not something this team loved. Simple and contained was way more important.
i'd argue that python is not one of the best languages at supporting modularity, because is is dynamically typed and duck typed, and by default there's no requirement for functions to say what type of thing they accept -- unlike languages that give you a way to declare "this function requires a thing A that implements interface B" -- and no mechanism to enforce it either.
i completely agree that one can certainly write modular code in python, but doing so is far more error prone than certain other languages, as by default the language doesn't give you a way to enforce that code cannot start depending on implementation details of random things. maybe the closest thing is the protocol stuff (https://peps.python.org/pep-0544/), but of course python doesn't enforce any of these optional type annotations.
in contrast if you bash out some go or java code using interfaces, the language gives you a lot more assistance in preventing you from bypassing the interface you're claiming to be depending upon.
> sql
i'd be very curious to see examples of modularity with sql. are there good ways of doing this in the sql standard, that are widely supported by implementations?
one example - (albeit the bigquery/sql manifestation of sql): suppose you have a database that contains many tables and views. some of the tables and views are internal, part of the implementation, you want to avoid users directly depending on them so you can change them without notice. some of the tables and views are external, and are meant to be exposed to certain groups of users. this defines the external interface to accessing data. if you implement this in bigquery and give your users read access to the database, then there's no way to give them read access to some tables and views, and block them from others. so the language and technology doesn't give you one of the basic building blocks you need to define and enforce modularity
It was designed for building large real-world applications and separates specification from implementation by using separately compilable specification and body files:
https://learn.adacore.com/courses/intro-to-ada/chapters/modu...
This allows the interface between modules to be defined concretely without relying on the implementations.