Compatibility Modules
Motivation[edit]
It can be difficult to write code that maintains compatibility with multiple versions of dependencies. In many cases, a library author can decide to require users to upgrade to a more recent version of dependencies. However, in the case of libraries bundled with GHC, this means upgrading the compiler, which in many cases is not an option.
One possible response would be to try and lock down the APIs for these libraries to avoid any migration headaches. However, such an approach would ultimately lead to quite convoluted APIs at the core of our ecosystem.
Instead, the core libraries committee has recommended an approach of releasing compatibility packages for each version of the base package. This document will lay out the design decisions we recommend, and why we decided on them.
Problem statement[edit]
Consider a very simple (and obviously fake) example. Suppose base for some terrible reason decided to provide a function which attempts to parse an Int
and, on failure, uses a default value of 0:
module Parse where parseInt :: String -> Int parseInt s = case reads s of (i,""):_ -> i _ -> 0 -- Some other functions as well
This module makes it into base version 4.6 but then, when releasing base 4.7, we decide that the default value should really be an argument to the function instead of hard-coded to 0. Thus, our type signature changes to:
parseInt :: Int -> String -> Int
Making this change in base itself is trivial. The problem is for all the user code out there using parseInt
. In order to allow user code to be compatible with both version 4.6 and 4.7 of base, the user must rely on techniques such as Cabal CPP macros, e.g.:
myFunc s = 5 + #if MIN_VERSION_base(4, 7, 0) parseInt 0 s #else parseInt s #endif
Such conditionals are tedious and error-prone. The goal of this proposal is to simplify the life of users trying to write code that will compile against multiple GHC versions, while allowing GHC-bundled libraries to be updated in reasonable manner.
Compatibility modules[edit]
We want to provide compatibility modules to continue providing the old API for newer base versions. Some important points about these modules:
- They will have a consistent naming, simply appending
.Compat
to the original module name. - We want to make these modules available before the GHC release, so that user code can begin migrating over to these modules immediately and therefore have a smoother transition to the new GHC release. We will therefore release the code to Hackage in compatibility packages. This strategy also means that the burden of maintenance can be moved from GHC HQ to the community in general.
- These packages will have a similarly consistent naming scheme, original-package-compat-targeted-version, where targeted version will be major_minor. For example, base-compat-4_6 will have modules exposing the same API as base version 4.6.
- When a breaking change in a specific API has been identified and determined to be worth a compatibility module, such a module and package can be created and released to Hackage immediately, and will simply re-export the current module. Once the new version of GHC is released (or about to be released), a new version of the compatibility package must be released which will use conditional compilation to emulate the old API using the newer API.
Worked example[edit]
Let's work out how this will play out for our fictitious parseInt
example above. base 4.6 contains the Parse module which is about to change, so we must create a package base-compat-4_6 containing a single module, defined as such:
module Parse.Compat (module Parse) where import Parse
This can be uploaded to Hackage, and users can immediately transition to using Parse.Compat anywhere they would have used Parse. Eventually, it comes time for the new GHC release, so we need to update base-compat-4_6 to support the new Parse API using conditional compilation. This would look something like:
{-# LANGUAGE CPP #-} #if MIN_VERSION_base(4, 7, 0) module Parse.Compat (module Parse, parseInt) where import Parse hiding (parseInt) import qualified Parse parseInt :: String -> Int parseInt = Parse.parseInt 0 #else module Parse.Compat (module Parse) where import Parse #endif
This can be uploaded to Hackage before the GHC release is made, and then users of the compatibility package will be completely insulated from the GHC upgrade.
In sum:
- Initial release of base-compat-4_6 version 1.0.0 would only depend on base 4.6, and be a pass-through shim.
- When we're about to make the GHC 7.8 release, we at that point release base-compat-4_6 version 1.0.1, which includes base 4.7 support.
- The only change here would be if someone in the community volunteers to add the base 4.7 support earlier in order to ease GHC HEAD testing.
- The only changes to be made to this package in the future would be:
- Bug fixes.
- Support for future base versions.
- At no point would there ever be a breaking API change of this package. In other words, the versions released will always be <= 1.1.
Goals[edit]
The primary stated goal is to simplify the process of supporting multiple GHC releases. The purpose in doing so is to reduce tension in the library ecosystem caused by new GHC releases. If some packages only work with GHC version X, and some packages only work with version X+1, it can be difficult for users to compile their applications.
The compatibility package approach eases this process. But we also have a secondary goal: make compatibility packages the easiest way to transition to newer GHC versions. The reasoning here is that, if we make compatibility the most expedient option for developers, they are more likely to embrace it.
Questions/questionable choices[edit]
- The package naming scheme may seem a bit strange. It would seem more reasonable to simply call the package base-compat, and use the package version to indicate the API we're emulating. However, by encoding the emulated API version in the name itself, we can import modules from multiple compatibility packages simultaneously. This would allow, for example, usage of Foo.Compat for base 4.3 and Bar.Compat for base 4.5.
- Note that, in such a circumstance, users would likely need to use package imports to disambiguate which Foo.Compat module they want to use.
- We need to determine a general guideline for which API changes warrant compatibility modules.
- We also need to determine how long compatibility modules will be maintained. This can likely be community driven, e.g. if someone cares enough about extended backwards compatibility, he/she can send a pull request.