Posted on 2019-06-03 by Oleg Grenrus engineering

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-writing . 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 can 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.

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 version

bifunctors provides Data.Bifunctor , Data.Bifoldable and Data.Bitraversable

provides , and contravariant provides Data.Functor.Contravariant .

provides . fail provides Control.Monad.Fail

provides nats provides Numeric.Natural

provides semigroups provides Data.Semigroup

provides tagged provides Data.Proxy

provides 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 );

provides based deriving code for type-class (and defines ); 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);

provides instances for small set of core packages, and acts as a instance compat module (e.g. provides , which wasn't in from the beginning); th-orphans provides instances for the types in template-haskell

provides instances for the types in and 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) .

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 alternatives , 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 looks :

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 reasonably compatible behaviour across different GHC (7.0 ... 8.8) and time (1.2 ... 1.9.2) versions.

#GHC functionality

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.

#My mistake

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 experiments . 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.

#Future thoughts: text

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!

#Final words

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!