Consider a network of nodes running ML programs that exchange data. How can data which has an abstract type on one node be accepted on another node? A safe approach is to treat abstract types as distinct whenever they are defined on different nodes. However this is too restrictive in practice, for example in the common case where an abstract type enforces a semantic invariant. The main contributions of this thesis are threefold: I define a notion of hash of an abstract type, whereby abstract types that have the same hash are deemed compatible; I give an operational semantics for a module system that preserves types, including abstract types; I also propose a new, more general module system that is well-suited to distributed applications. The hash of an abstract type must reflect its intended semantics, which is often not apparent from the program's code. In practice, two modules have the same hash if they have the same code. Compound modules are compatible when they are built from compatible components. Existing operational semantics for ML modules lose information as they erase abstraction boundaries. I use coloured brackets to track the visibility of abstract types. I study two calculi equipped with brackets, a simply-typed lambda-calculus and a rich ML module calculus. I use singleton signatures to keep track of not only type but also code sharing, so that module equivalence is defined at arbitrary signatures. A simple effect system limits type constraint to a statically checkable fragment, while permitting both applicative and generative functors. I discuss static and dynamic forms of module sealing.