Thoughts on Backpack, modules, and records ========================================== January 31, 2021 In the previous post I have explored how Backpack might be used for the Handle pattern. It helped me to take a better look at Backpack and reflect a bit on modules and records in Haskell. What's wrong with Backpack? --------------------------- Backpack works as expected but there are downsides of the current implementation: 1. It's `cabal`-only. It means it's hard to integrate it in any big project because developers tend to choose different build tools. 2. It's not user-friendly — it may throw some mysterious errors without explaining what's going on. Or parse errors in mixins block https://github.com/haskell/cabal/issues/5150. 3. It's not maintained. `git blame` tells that there was not much activity in Backpack modules in `cabal` repository for three years. 4. It does not support mutual recursive modules, sealing, higher-order units, hierarchical modules, and other things. The functionality can be improved. 5. It lacks documentation. 6. It's not supported by Nix https://github.com/NixOS/nixpkgs/issues/40128. I don't think it's Backpack's problem but since Nix has become a very popular choice for Haskell's build infrastructure it's a strong blocker for Backpack's integration. 7. It's not used. Even if we close the eyes on problems with cabal or nix, the developers don't use Backpack because it's too heavy-weight to use and it's unidiomatic. These issues seem to be related: no users => bad support and bad support => no users. Backpack was an attempt to bring a subset of ML modules system to Haskell, the language with anti-modular features and a big amount of legacy code written in that style. In my opinion, that attempt failed. The ML modules system is about explicit manipulation of modules in the program by design — the style that most Haskell programmers seem to dislike. What's the point of having an unused feature which is unmaintained and unfinished then? I write this not to criticize Backpack, but to understand its future. It's a great project that explored the important design space. The aforementioned downsides can be fixed and improved. The real question is "Should the community invest resources in that?" What's in the proposals ----------------------- I looked at https://github.com/ghc-proposals/ghc-proposals/ to see if there are any improvements of modularity in the future and, in my opinion, most proposals are focused on concrete problems instead of reflecting on the whole language. It may work well but also it may create inconsistency in the language. - QualifiedDo `QualifiedDo` brings syntax sugar for overloading `do` notation: ``` {-# LANGUAGE LinearTypes #-} {-# LANGUAGE NoImplicitPrelude #-} module Control.Monad.Linear (Monad(..)) where class Monad m where return :: a #-> m a (>>=) :: m a #-> (a #-> m b) #-> mb ----------------- module M where import qualified Control.Monad.Linear as Linear f :: Linear.Monad m => a #-> m b f a = Linear.do b <- someLinearFunction a Linear.>>= someOtherLinearFunction c <- anotherLinearFunction b Linear.return c g :: Monad m => a -> m b g a = do b <- someNonLinearFunction a >>= someOtherNonLinearFunction c <- anotherNonLinearFunction b return c ``` `Linear.do` overloads `return` and `(>>=)` with functions from `Control.Monad.Linear`. Hey, isn't this is why Backpack was created? To replace the implementation? Also, it can be achieved with records. And the authors used that solution in linear-base https://github.com/tweag/linear-base/blob/0d6165fb d8ad84dd1574a36071f00a6137351637/src/System/IO/Resource.hs#L119-L120: ``` (<*>) :: forall a b. RIO (a ->. b) ->. RIO a ->. RIO b f <*> x = do f' <- f x' <- x Linear.pure $ f' x' where Linear.Builder { .. } = Linear.monadBuilder ``` The quote from the proposal: >There is a way to emulate ``-XQualifiedDo`` in current GHC using ``-XRecordWildcards``: have no ``(>>=)`` and such in scope, and import a builder with ``Builder {..} = builder``. It is used in `linear-base`. This is not a very good solution: it is rather a impenetrable idiom, and, if a single function uses several builders, it yields syntactic contortion (which is why shadowing warnings are deactivated here I don't mind about using records. It might be uncomfortable, but not worth to patch the language. Maybe I'm missing something important. At first glance, this syntactic extension may even look okay, but a more fundamental solution would be local modules support where you can open a module locally and bring its content into the scope: ``` (<*>) :: forall a b. RIO (a ->. b) ->. RIO a ->. RIO b f <*> x = let open Linear in do f' <- f x' <- x Linear.pure $ f' x' ``` ``` (<*>) :: forall a b. RIO (a ->. b) ->. RIO a ->. RIO b f <*> x = do f' <- f x' <- x Linear.pure $ f' x' where { .. } = open Linear ``` It's just like opening the records! It might be tedious to write these imports instead of `Linear.do`, but we wouldn't need to bring new functionality to the language if we had local imports. Maybe it means that "do notation" is really important to the Haskell community but to me, it feels like a temporary hack at the moment, not a fundamental part of the language. - Local modules `LocalModules` sounds like a good step in the right direction. It will be possible to create modules in modules and to open them locally! Just like in the example above with `Linear` module. Another good thing is that each `data` declaration implicitly creates a new local module, like in Agda. I'm not sure if it's possible to open them, I haven't found it in the proposal, but that would unify the opening of records and modules. Unfortunately, the proposal doesn't explore the interaction with Backpack: >This proposal does not appear to interact with Backpack. It does not address signatures, the key feature in Backpack. Perhaps the ideas here could be extended to work with signatures. Does it mean that there will be `LocalSignatures` in the future? Why not design the whole mechanism at once? Is there a risk of missing something important that would be hard to fix later? - First class modules This proposal is about making a module a first-class entity in the language. Its status is *dormant* because it's too much to change while the benefits seem to be the same as in `LocalModules`. While `LocalModules` is a technical proposal that goes into details, `First class modules` is more about language design proposal. `LocalModules` does not replace `First class modules`, but *a part of it*. This is exactly what I was looking for, the proposal that tries to build a vision for the language, what it might look in the future. The proposal mentions Backpack only once: > Interface files must be able to handle the possibility that an exported name refers to a module. This may have some interaction with Backpack. Unfortunately, the work on this proposal was stopped because most interesting things were described in `LocalModules` — a proposal that is about *local namespaces*, not *modules*. There is nothing about abstraction there. - Summing up It's important to remember that Haskell modules are just namespaces without any additional features. It's possible to import the contents of a module, everything or only specific things, and control what to export (except instances). While `LocalModules` will significantly improve developers' lives providing a flexible way to deal with scopes, it's unclear what the whole picture with modules will look like. And why Backpack is ignored by the proposals? What's wrong with it? The Backpack is ignored because it's not a proper part of the Haskell language. It's a mechanism that consists of *mixin linking* which is indifferent to Haskell source code, and *typechecking against interfaces*, which is purely the concern of the compiler. That's why it depends so much on Cabal — a frontend client for mixins description which can be replaced the compiler supports typechecking of interfaces. More details are available in Edward Z. Yang's thesis. Modules and records, and type classes ------------------------------------- We understood that Backpack is a tool that uses the compiler and the package manager to express the features that are not supported in the language internally. Is it possible for Haskell to go further and improve the modularity in the language internally? To fit together anti-modular type classes and some subset of modules with abstract types? I want to explore what's in the ML languages at first. - What's in the ML land Let's take a look at what happens in languages with the ML modules system. The users of these languages accept the cost of the explicitness. Although, they would be glad to reduce the boilerplate when it's necessary. The theoretical works demonstrate that type classes and modules are not that different. http://www.stefanwehr.de/publications/Wehr_ML_modules_and_Haskell_ type_classes.pdf showed that a subset of Haskell with type classes can be expressed with ML modules and vice versa with some limitations. https://www.cs.cmu.edu/~rwh/papers/mtc/full.pdf went further in expressing type classes with modules. It starts with modules as the fundamental concept and then recovers type classes as a particular mode of use of modularity. The example of bringing type classes to the language with modules can be found in https://arxiv.org/abs/1512.01895 — an extension to the OCaml language for ad-hoc polymorphism inspired by Scala implicits and modular type classes. It's not supported by mainstream OCaml yet because the proper implementation requires a big change in the language. But here is a small example of how it might look for the `Show` type class: ``` (* We express `Show` as a module type. It's a module signature that can be instantiated with different implementations. It has an abstract type `t` and a function `show` that takes `t` and returns `string`. class Show t where show :: t -> String *) module type Show = sig type t val show : t -> string end (* This is a global function `show` with an implicit argument `S : Show`. It means that it takes a module that implements the module type `Show`, an argument `x`, and calls `S.show x` using the `show` from `S`. show' :: Show a => a -> String show' = show *) let show {S : Show} x = S.show x (* An implementation of `Show` for `int`. instance Show Int where show = string_of_int *) implicit module Show_int = struct type t = int let show x = string_of_int x end (* An implementation of `Show` for `float`. instance Show Float where show = string_of_float *) implicit module Show_float = struct type t = float let show x = string_of_float x end (* An implementation of `Show` for `list`. Since `list` contains elements of some type `t` we require a module `S` to show them. instance Show a => Show [a] where show = string_of_list show *) implicit module Show_list {S : Show} = struct type t = S.t list let show x = string_of_list S.show x end let () = print_endline ("Show an int: " ^ show 5); print_endline ("Show a float: " ^ show 1.5); print_endline ("Show a list of ints: " ^ show [1; 2; 3]); ``` As we can see the languages with ML modules system can get ad-hoc programming support to some degree. - Why modularity? Haskell's culture relies heavily on type classes. It's a foundation for monads, do-notation, and libraries. All instances of type classes should be unique and it's a cultural belief because the *global uniqueness of instances* is just an expectation that isn't forced by GHC http://blog.ezyang.com/2014/07/type-classes -confluence-coherence-global-uniqueness/. What's the point of living in an anti-modular myth that forbids the integration of possible modularity features? >It is easy to dismiss this example as an implementation wart in GHC, and continue pretending that global uniqueness of instances holds. However, the problem with global uniqueness of instances is that they are inherently nonmodular: you might find yourself unable to compose two components because they accidentally defined the same type class instance, even though these instances are plumbed deep in the implementation details of the components. This is a big problem for Backpack, or really any module system, whose mantra of separate modular development seeks to guarantee that linking will succeed if the library writer and the application writer develop to a common signature. The example with `QualifiedDo` shows that even one of the key features of Haskell needs modularity — monads and do-notation. That required additional patching of the language because do-notation is based on type classes instead of modules. `LocalModules` may help to deal with scopes (it's really annoying sometimes), but they won't provide the abstraction mechanism — the key feature of a module system. Modules with abstract types allow replacing the implementations because of Reynolds’s abstraction theorem. Type classes allow to create interfaces and implement them for different types, but they are usually global and ad-hoc. They make the project, the libraries, and the whole Hackage into one global world. Not always in practice for now, but it can be very annoying to track the import statement that brings an instance into the scope especially in a big codebase. Type classes become too expensive at some point as an abstraction tool. - Local signatures I wrote `LocalSignatures` as a joke when was writing about `LocalModules`, but then later I understood that it might work since GHC already supports type checking against interfaces it can be used to implement signatures on the language level. It might require lots of syntax changes though. Something like that: ``` module type EQ where data T eq :: T -> T -> Bool module type MAP where data Key data Map a empty :: Map a lookup :: Key -> Map a -> Maybe a add :: Key -> a -> Map a -> Map a module Map (Key :: EQ) as MAP with (type Key as Key.T) where type Key = Key.T type Map a = Key -> Maybe a empty x = Nothing lookup x m = m x add x y m = \z -> if Key.eq z x then Just y else m z ``` We have created two signatures (module types) `EQ` and `MAP`. And a module `Map` that implements `MAP`. Writing `as MAP`, we seal the module `Map` and hide the implementation details behind the signature `MAP`, specifying that `Key.T` is the same as `Key`. This is just an example to demonstrate how it might look like. It introduces a module system to the language but on a different level. We can't pass a module to a function — modules and core terms are separated. - Modules and records The Handle pattern demonstrates that Haskell's modules and records are alike in some way — they implement the Handle interface provided to the user, both containing functions. Records are dynamic and can be replaced in the runtime while signatures are static and can be specialized during the compilation. What if we could merge them into one entity? https://people.mpi-sws.org/~rossberg/1ml/ does that — it merges two languages in one: *core* with types and expressions, and *modules*, with signatures, structures and functors. And requires only System Fω. The example for local signatures mentioned above is the code I adapted from 1ML. Here is the original version: ``` type EQ = { type t; eq : t -> t -> bool; }; type MAP = { type key; type map a; empty 'a : map a; lookup 'a : key -> map a -> opt a; add 'a : key -> a -> map a -> map a; }; Map (Key : EQ) :> MAP with (type key = Key.t) = { type key = Key.t; type map a = key -> opt a; empty = fun x => none; lookup x m = m x; add x y m = fun z => if Key.eq z x then some y else m z; }; datasize = 117; OrderedMap = Map {include Int; eq = curry (==)} :> MAP; HashMap = Map {include Int; eq = curry (==)} :> MAP; Map = if datasize <= 100 then OrderedMap else HashMap : MAP; ``` Looks similar, but now we can choose what module to use by analyzing the runtime value similar to what can be done in Haskell with records, vinyl, or something else. But they lack the abstract types support. Conclusions ----------- I haven't thought about how local signatures or first-class modules may interact with type classes to achieve the same experience as with implicits in Agda, Scala, or OCaml. According to the recent paper on new implicits calculus https://lirias.kuleuven.be/2343745, implicits don't have global uniqueness of instances, just GHC's type classes in practice, but have coherence and stability of type substitutions. It makes me wonder why not try to drop the global uniqueness property and improve the module system instead. The recently created Haskell Foundation states that it's going to address the need for *driving adoption*. It means more developers will write Haskell, more libraries, more projects, more type classes, and more instances that are *global* for the entire Hackage. I think it's important to decide what to do with Backpack, consider the improvement of the module system, and design the lianguage more carefully taking in mind the whole picture.