I am not an expert but there do seem to be a lot of subtle concerns that ML-style modules introduce, both in the theory/metatheory and pragmatic sides of language design: generativity, full vs partial functorized styles, translucency, etc. Plus the desire to reduce the extent to which the module system duplicates a large subset of the base language; see 1ML.
(Mostly for the sake of other readers)... IME there absolutely are a lot of subtle concerns, but that's also the case of just about anything that combines interfaces and generics in any way. (Just look at the design morass you can get out of Java/C#/C++-style OO code.)
I also believe that the theoretical concerns are mostly overblown, and the pragmatic sides are hampered more by poor UI and inconsistent design than anything else. There's been papers written on the design choices between generative and applicative functors for example, when the solution to me is obviously "you want to be able to write both". Translucency is a large and complicated idea that large and complicated Python programs implement as "private fields are prefixed by _, if you touch them you'll deserve what you get". Stuff like that.
Haskell's traits were in many ways originally devised as "ML modules: the good parts", so "why is this so hard" is very much not a new thought on the subject. :D
Oh yeah ML Modules have a ton of complexity in theory. They also have, in my opinion annoying, quality that theoretically there's this tantalizing implementation of modules that very cleanly divides into a static module language that assembles all your code and then compiles away, leaving you with fast tightly linked code at runtime. In theory this is reachable, but in practice most module systems have some amount of runtime functionality they have to assume due to how tricky the semantics get.
I cut a whooooole a lot about "hey how do you tell when abstract types are equal" because it was gonna dominate the post, but I'd like to talk about some of the more advanced module stuff. Especially around Fing Modules (a bit of precursor to 1ML) and mixin modules, and 1ML ofc, and backpack style modules...😅. There's so much ground to cover around modules
A class Foo introduces both a particular type (we can have instances x : Cat) and a set of methods & associated types (a.k.a. type members in Scala).
To get from classes to ML modules, we can notice that the job of introducing a new type x : Cat is kind of redundant, since that could just have been another associated type x : Cat.cat. (In OCaml/SML, this associated type is conventionally just called t, so we would write x : Cat.t).
If we do this, we lose the idea that the methods in Cat "belong" exclusively and inalienably to the receiver type Cat.t. The downside is we lose out on some type-directed name resolution; the upside is that I can define my own set of functionality Cat2 for the same underlying type (Cat2.t = Cat.t), and we don't have any of the weirdness of "extension methods", downstream code can just choose to use my module with no trouble.
eschew | 8 hours ago
The Fox project (FoxNet) used SML modules to organize their networking stack in the mid 90s.
More recently, OCaml modules have been used in the MirageOS unikernel, for its network stack and more broadly, and in the VPNKit networking subsystem of Docker (see section 4.1.1).
I am not an expert but there do seem to be a lot of subtle concerns that ML-style modules introduce, both in the theory/metatheory and pragmatic sides of language design: generativity, full vs partial functorized styles, translucency, etc. Plus the desire to reduce the extent to which the module system duplicates a large subset of the base language; see 1ML.
icefox | 7 hours ago
(Mostly for the sake of other readers)... IME there absolutely are a lot of subtle concerns, but that's also the case of just about anything that combines interfaces and generics in any way. (Just look at the design morass you can get out of Java/C#/C++-style OO code.)
I also believe that the theoretical concerns are mostly overblown, and the pragmatic sides are hampered more by poor UI and inconsistent design than anything else. There's been papers written on the design choices between generative and applicative functors for example, when the solution to me is obviously "you want to be able to write both". Translucency is a large and complicated idea that large and complicated Python programs implement as "private fields are prefixed by
_, if you touch them you'll deserve what you get". Stuff like that.Haskell's traits were in many ways originally devised as "ML modules: the good parts", so "why is this so hard" is very much not a new thought on the subject. :D
thunderseethe | 6 hours ago
Oh yeah ML Modules have a ton of complexity in theory. They also have, in my opinion annoying, quality that theoretically there's this tantalizing implementation of modules that very cleanly divides into a static module language that assembles all your code and then compiles away, leaving you with fast tightly linked code at runtime. In theory this is reachable, but in practice most module systems have some amount of runtime functionality they have to assume due to how tricky the semantics get.
I cut a whooooole a lot about "hey how do you tell when abstract types are equal" because it was gonna dominate the post, but I'd like to talk about some of the more advanced module stuff. Especially around Fing Modules (a bit of precursor to 1ML) and mixin modules, and 1ML ofc, and backpack style modules...😅. There's so much ground to cover around modules
jyounker | 6 hours ago
So, ML modules are classes with structural typing.
christine | 4 hours ago
A class
Foointroduces both a particular type (we can have instancesx : Cat) and a set of methods & associated types (a.k.a. type members in Scala).To get from classes to ML modules, we can notice that the job of introducing a new type
x : Catis kind of redundant, since that could just have been another associated typex : Cat.cat. (In OCaml/SML, this associated type is conventionally just calledt, so we would writex : Cat.t).If we do this, we lose the idea that the methods in
Cat"belong" exclusively and inalienably to the receiver typeCat.t. The downside is we lose out on some type-directed name resolution; the upside is that I can define my own set of functionalityCat2for the same underlying type (Cat2.t = Cat.t), and we don't have any of the weirdness of "extension methods", downstream code can just choose to use my module with no trouble.