Supporting wide (version) ranges of dependencies is a common problem in software engineering. In particular, supporting many major GHC versions is sometimes tricky. In my opinion it's not because Haskell-the-language changes, very few extensions are essential for library-writing1. A tricky part is the changes in the so called boot libraries: base
, transformers
... Luckily, there is a collection of compatibility packages, which smooth the cross-GHC development experience.
Compatibility packages are symptom of a greater problems: non-reinstallable ghc
(and base
). That will hopefully change soon: reinstallable lib:ghc
is mentioned on GHC-8.8.1 page in a planned section.
Another, somewhat related problem is orphan instances. Sometimes instances are just forgotten, and as upgrading a proper home package is tricky one, a module with orphan instances is the best one can do. Or maybe the reason is as simple as that neither type-class nor data-type defining package can2 depend on other.
As some of these compatibility packages define orphan instances, it would be good that people used these packages instead of defining their own orphans. They are easy to update, if something is still missing.3
In this post I'll discuss some compatibility packages, I'm aware of. I'll also mention few extras.
Acknowledgments: Ryan Scott maintains a lot of packages listed below. I cannot appreciate enough all the time and work put into their maintenance.
base
is a core package. Along the years, there were bigger changes like Applicative-Monad-Proposal, Semigroup-Monoid-Proposal, MonadFail-Proposal, but also small changes, like addition of new instances and functions.
The scope of base-compat
is to provide functions available in later versions of base to a wider (older) range of compilers.
One common at the time problem was <$>
not in scope, due AMP. This kind of small problems are easily fixed with
import Prelude ()
import Prelude.Compat
base-compat
provides "an alternative" prelude, which I can recommend to use.
Similarly to <$>
, base-compat
provides Semigroup
right out of Prelude.Compat
. However due the fact that base-compat
does not add any orphan instances (base-orphans
does). neither backport any types, Semigroup
is provided only with GHC-8.0 / base-4.9
. Latter is solved by base-compat-batteries
.
Also base-compat
(and base-compat-batteries
) provide a lot of .Compat
modules
import Data.Either.Compat (fromLeft) -- is always there
base-compat-batteries
provides the same API as the base-compat
library, but depends on compatibility packages (such as semigroups
) to offer a wider (or/and more complete) support than base-compat
.
The most is understood by looking at build-depends
definitions of base-compat-batteries
:
if !impl(ghc >= 7.8)
build-depends:
tagged >= 0.8.5 && < 0.9
if !impl(ghc >= 7.10)
build-depends:
nats >= 1.1.2 && < 1.2,
void >= 0.7.2 && < 0.8
if !impl(ghc >= 8.0)
build-depends:
fail >= 4.9.0.0 && < 4.10,
semigroups >= 0.18.4 && < 0.20,
transformers >= 0.2 && < 0.6,
transformers-compat >= 0.6 && < 0.7
if !impl(ghc >= 8.2)
build-depends:
bifunctors >= 5.5.2 && < 5.6
if !impl(ghc >= 8.6)
build-depends:
contravariant >= 1.5 && < 1.6
In some cases (like application development), base-compat-batteries
is a good choice to build your own prelude on. Even some projects are often built with single GHC only, base-compat-batteries
can help smooth GHC migrations story. You can adapt to the newer base
without updating the compiler (and all other bundled dependencies).
However when developing libraries, you might want be more precise. Then you can use the same conditional build-depends
definitions to incur dependencies only when base
is lacking some modules. Note that we use GHC version as a proxy for base
version4
bifunctors
provides Data.Bifunctor
, Data.Bifoldable
and Data.Bitraversable
contravariant
provides Data.Functor.Contravariant
.fail
provides Control.Monad.Fail
nats
provides Numeric.Natural
semigroups
provides Data.Semigroup
tagged
provides Data.Proxy
void
provides Data.Void
module.Note, that some of this packages provide additional functionality, for example semigroups
have Data.Semigroup.Generic
and contravariant
Data.Functor.Contravariant.Divisible
modules.
base-orphans
defines orphan instances that mimic instances available in later versions of base to a wider (older) range of compilers.
import Data.Orphans ()
My personal favourite is
instance Monoid a => Monoid (IO a) where
mempty = pure mempty
mappend = liftA2 mappend
instance, which is in base
only since 4.7.0.0
.
A word of warning: sometimes the instance definition changes, and that cannot be adopted in a library.
generic-deriving
is the package providing GHC.Generics
things.
Notably it provides missing Generic
instances for things in base
. If you ever will need Generic (Down a)
, it's there.
transformers
is a well known library for monad transformers (and functors!).
transformers-compat
backports versions of types from newer transformers. For example an ExceptT
transformer.
Note that for example Data.Functor.Identity
may be in base
, transformers
or transformers-compat
, so when you do
import Data.Functor.Identity
it may come from different packages.
writer-cps-transformers
have become a compatibility package, as since 0.5.6.0
transformers
itself provide "stricter" Writer
monad in Control.Monad.Trans.Writer.CPS
.
There is also a writer-cps-mtl
package which provides mtl
instances.
deepseq
provides methods for fully evaluating data structures.
Since deepseq-1.4.0.0
(bundled with GHC-7.10) the default implementation of rnf
uses Generics. Before that
instance NFData Foo
worked for any type and did force only to WHNF: rnf x = x
seq()
. If your library define types, and support older than GHC-7.10 compilers, the correct variant is to define rnf
explicitly, using deepseq-generics
import Control.DeepSeq.Generics (genericRnf)
instance NFData Foo where rnf = genericRnf
Template Haskell (TH) is the standard framework for doing type-safe, compile-time meta programming in the GHC. There are at least two compat issues with Template Haskell: defining Lift
instances, missing Lift
instances, and changes in template-haskell
library itself.
They are solved by three (or four) packages:
th-lift
provides TemplateHaskell
based deriving code for Lift
type-class (and defines Lift Name
);th-lift-instances
provides instances for small set of core packages, and acts as a instance compat module (e.g. provides Lift ()
, which wasn't in template-haskell
from the beginning);th-orphans
provides instances for the types in template-haskell
th-abstraction
which helps write Template Haskell code.Note: th-lift
and th-lift-instances
only use TemplateHaskellQuotes
, therefore they don't need interpreter support. That means that your library can provide Template Haskell functionality without itself requiring it. This is important e.g. for GHCJS (template haskell is very slow), or in cross-compilation (tricky issues), or the simple fact the system doesn't have dynamic loading (See Dyn Libs in GHC supported platforms)5.
Lift
type-class provides lift
method, which let's you lift expressions in Template Haskell quotations. It's useful to embed data into final library
myType :: MyType
myType = $(readMyTypeInQ "mytype.txt" >>= lift)
th-lift
provides TemplateHaskell
based deriving code for Lift
type-class.
With GHC-8.0 and later, you can write
{-# LANGUAGE DeriveLift #-}
data MyType = ...
deriving (Lift)
however, for older GHCs you can use th-lift
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH.Lift (deriveLift)
data MyType = ...
deriveLift ''MyType
th-lift-instances
provides instances for small set of core packages.
import Instances.TH.Lift ()
to get e.g. Lift Text
instance.
th-orphans
provides instances for template-haskell
types, in particular Ord
and Lift
. This package is useful when you write Template Haskell code, not so when you use it.
import Language.Haskell.TH.Instances ()
th-abstraction
is not precisely a compat package, but it normalizes variations in the interface for inspecting datatype information via Template Haskell so that packages can use a single, easier to use informational datatype while supporting many versions of Template Haskell.
If you can write your TH code using th-abstraction
interface, it's way simpler than all CPP involved with raw template-haskell
usage.
binary
provides binary serialisation. There are various alternatives6, but binary
is bundled with GHC, so if you don't have special requirements it's good enough default choice. The benefit of binary
is that there is a lot of support, many packages provide Binary
instances for their types.
binary-orphans
provides instances defined in later versions of binary
package. For example it provides MonadFail Get
instance, which was the main motivation for the creation of the package.
import Data.Binary.Orphans ()
binary-instances
scope is broader, it provides Binary
instances for types in some popular packages: time
, vector
, aeson
et cetera.
import Data.Binary.Instances ()
bytestring
provides an immutable byte string type (pinned memory).
bytestring-builder
provides Data.ByteString.Builder
and Data.ByteString.Short
modules for old bytestring
.
The commonly needed missing piece is Data.ByteString.Lazy.toStrict
and fromStrict
. They are not needed that often though, so I have written it inline when needed. Compat toStrict
is actually is more efficient than it looks7:
import qualified Data.ByteString as BS
import qualified Data.ByteString as LBS
fromStrict :: BS.ByteString -> LBS.ByteString
toStrict :: LBS.ByteString -> BS.ByteString
#if MIN_VERSION_bytestring(0,10,0)
fromStrict = LBS.fromStrict
toStrict = LBS.toStrict
#else
fromStrict bs = LBS.fromChunks [bs]
toStrict lbs = BS.concat LBS.toChunks lbs -- good enough.
#endif
time
provides most time related functionality you need. In 1.9
version it got a lot of nice things, including CalendarDiffDays
to represent "calendar" day difference.
time-compat
shims the time
package to it's current latest version. This is my recent experiment. All time
modules have a .Compat
version. This means that you can change to dependency definition
- build-depends: time >=... && <...
+ build-depends: time-compat ^>=1.9.2
and imports
- import Data.Time
+ import Data.Time.Compat
and get reasonably8 compatible behaviour across different GHC (7.0 ... 8.8) and time (1.2 ... 1.9.2) versions.
We already mention th-lift
which provide Template Haskell based functionality to derive Lift
type-class instances. There are other classes to be derived.
deriving-compat
provides Template Haskell functions that mimic deriving extensions that were introduced or modified in recent versions of GHC.
Particularly, it provides a way to mimic DerivingVia
which is only in GHC-8.6+. The deriving-compat
is ugly, but if you are stuck with GHC-8.2 or GHC-8.4, that's an improvement: you can prepare code to be DerivingVia
ready.
So if real DerivingVia
looks like
{-# LANGUAGE DerivingVia #-}
data V2 a = V2 a a
deriving (Functor)
deriving (Semigroup, Monoid) via (Ap V2 a)
instance Applicative V2 where ...
the deriving-compat
way looks like
{-# LANGUAGE TemplateHaskell, ... #-}
import Data.Deriving.Via
data V2 a = V2 a a
deriving (Functor)
instance Applicative V2 where ...
deriveVia [t| forall a. Semigroup a => Semigroup (V2 a) `Via` (Ap V2 a) |]
deriveVia [t| forall a. Monoid a => Monoid (V2 a) `Via` (Ap V2 a) |]
It feels like StandaloneDeriving
with all pros and cons of it.
QuickCheck
is a well known library for random testing of program properties. It's usage relies on Arbitrary
instances for various types.
The goal of quickcheck-instances
is to supply QuickCheck
instances for types provided by the Haskell Platform. Also to keep QuickCheck
dependency light, and also CPP free, quickcheck-instances
provides instances for types like Natural
or NonEmpty
.
import Test.QuickCheck.Instances ()
[hashable](https://hackage.haskell.org/package/hashable) defines a class,
Hashable`, for types that can be converted to a hash value.
hashable-time
is a small package providing Hashable
instances for types from time
.
If you want to use unordered-containers
with time
types
import Data.Hashable.Time ()
to get missing instances.
I have to mention one personal mistake: aeson-compat
. aeson
is not a GHC bundled boot package, and upgrading its version shouldn't be a problem. Creating aeson-compat
felt like a good idea back then, but currently I'd recommend to just use as recent aeson
as you need. I'd also advice against creating any compatibility packages for any other non-boot libraries, it's quite pointless.
In general, I don't see point sticking to the too old versions of non-boot dependencies. Virtually no-one uses the low ends of dependency ranges, at least based by amount of incorrect lower-bounds I find during my own experiments9. Stackage and nixpkgs add some friction into the equation. Allowing versions of dependencies in a current Stackage LTS is a kind thing to do, but if you need newer version, then at least I don't feel bad myself about putting higher lower bound.
It's quite likely that text
will adopt UTF-8 as the internal representation of Text
. My gut feeling says that the change won't be huge issue for the most of the users. But for example for the aeson
it will be: it would be silly not to exploit UTF-8 representation. attoparsec
should need adaptation as well. However, text
is bundled with GHC, and though it shouldn't be a problem to upgrade (as only mtl
, parsec
and Cabal
depend on it, not ghc
itself), it's hard to predict what subtle problems there might be. text-utf8
has an old fork of aeson
, but it pretends there's only new text-utf8
. So there is a lot of (compatibility) work to do for someone who cares about the details!
Library features are rarely the problem preventing wide GHC support windows. Unfortunately the compatibility story grew up organically, so the naming is inconsistent, which hurts discoverability of these compatibility packages. However, I think it's very great, given it's somewhat uncoordinated and voluntary effort. It's possible to write code which works for at least GHC 7.4 ... GHC-8.6. GHC-7.4.1 was released in 2012, seven years ago. We should measure support windows in years, not major versions. And from this point-of-view Haskell is definitely suitable for an enterprise development!
Libraries will keep changing, and it's good to think about compatibility a bit too. Luckily, there is prior art in Haskell eco-system to learn from!
Using BlockArguments
or similar syntax-only-extension, if completely fine. But I don't think using them is alone a good reason to not supporting older GHCs. Also, I don't think that dropping support for older GHC support only because it's old, is not a good enough reason.↩︎
Of course they can, but in addition to being social problem, it's also technical challenge: Completely linearising dependency graph into a chain is non desirable, though data-type package depend on type-class packages may still offer enough slack in the dependency graph.↩︎
There is an opinion that "lawless" classes like Arbitrary
, Binary
, FromJSON
should not exist, but rather we should use explicit values. I think that opinion is wrong, at the very least too extreme. Having explicit values and combinators is good, we can have both. These kind of type-classes enable generic programming and things like servant
: they let compiler write code for you. But this topic is better discussed in a separate blog post.↩︎
Semantically using GHC version as proxy for base
is imprecise, but it's simpler than currently available alternatives.↩︎
In my opinion Template Haskell, build-type: Custom
Setup.hs
scripts, unsafeCoerce
or unsafePerformIO
are all in the same group. Every time you use any, you have to write 200 words essay. Any consequtive use will require a 100 words longer one. Please send me those essays, I promise to publish the best parts.↩︎
Check serialise
which is also fast, but also uses CBOR format. That is useful, as the binary representation is self-descriptive, at least to some degree.↩︎
Compare toStrict
from 0.10.8.2 with concat
from 0.9.2.1.↩︎
For example changing instance implementation is impossible, therefore parsing and formatting behaves as in underlying time
versions. In any case, more compatible then bare Data.Time
.↩︎
I actually compile a lot packages sweeping their entire dependency-range. My ~/cabal/.store
have grown to 175G in a past month, and it contains e.g. 20 variants of text-1.2.3.1
for a single compiler version. Often lower bounds have bit-rotten, but sometimes there are also "interesting" interactions in the middle of dependency ranges.↩︎