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 this 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.
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
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.
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
.Compatto 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.
Let's work out how this will play out for our fictitious
parseInt example above.