Skip to content

Instantly share code, notes, and snippets.

@lehins
Last active June 27, 2018 13:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lehins/3498c5eb3bd5ca82af47c740c1c37256 to your computer and use it in GitHub Desktop.
Save lehins/3498c5eb3bd5ca82af47c740c1c37256 to your computer and use it in GitHub Desktop.
Make QuickCheck instances optional, but part of the library.
name: arbitrary
version: 0.1.0.0
description: Please see the README on GitHub at <https://github.com/lehins/arbitrary#readme>
homepage: https://github.com/lehins
bug-reports: https://github.com/lehins
author: Alexey Kuleshevich
maintainer: alexey@kuleshevi.ch
copyright: 2018 Alexey Kuleshevich
license: BSD3
license-file: LICENSE
build-type: Simple
cabal-version: >= 1.10
extra-source-files: ChangeLog.md
, README.md
flag quickcheck
description: Include Arbitraray and CoArbitrary instances for datatypes
default: False
library
exposed-modules: Lib
hs-source-dirs: src
build-depends: base >=4.7 && <5
default-language: Haskell2010
if flag(quickcheck)
cpp-options: -DQuickCheck_Instances
build-depends: QuickCheck
-- exposed-modules: Lib.QuickCheckInstances
test-suite arbitrary-test
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: arbitrary
, QuickCheck
, base >=4.7 && <5
default-language: Haskell2010
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Lib
( someFunc
, Foo(..)
) where
-- In practice instead of CPP possibly use a separate module for optional instances
#ifdef QuickCheck_Instances
import Test.QuickCheck
instance Arbitrary Foo where
arbitrary = Foo <$> arbitrary
#endif
newtype Foo = Foo Int deriving (Show, Num, Eq)
someFunc :: IO ()
someFunc = putStrLn "someFunc"

Motivation

When developing a Haskell library we often have a test-suite as part of the package. That test-suite will often have some orphan instances of Arbitrary and CoArbitrary instances from QuickCheck for custom data types declared within the library. Which is OK, if no one else will ever need those instances, but in case when the library in question is highly reusable by others, it will be very likely that others could benefit from having those instances available to them as well. Unfortunately it is impossible to import those instances from the test-suite, but including them in the library itself isn't practical either, since that would include an unnecessary dependency on QuickCheck.

The common solution to this problem is to have a separate package that includes just those orphan instances and depends both on the QuickCheck and the library, which is in itself becomes a dependency of the test-suite. This works, but produces an unnecessary, in my opinion, overhead of maintaining an extra package and clutters hackage with packages with orphan instances, that are useful only for testing. Same pattern seems to emerge for validity, genvalidity packages and possibly others that are used for testing.

Solution

I think, there is no reason to have those instances be included as orphans in a separate package and they can be placed right where they belong, next to the declaration of data types. All we have to do is use a custom cabal flag, quickcheck in the example above, but it could be anything, eg. test-mode could be a logical name as well.

Once it's implemented in the way presented above we will have main library depending on QuickCheck only when it is being used from within a test suite:

$ stack build
arbitrary-0.1.0.0: configure (lib)
Configuring arbitrary-0.1.0.0...
arbitrary-0.1.0.0: build (lib)
Preprocessing library for arbitrary-0.1.0.0..
Building library for arbitrary-0.1.0.0..
[1 of 1] Compiling Lib              ( src/Lib.hs, .stack-work/dist/x86_64-linux/Cabal-2.0.1.0/build/Lib.o )
arbitrary-0.1.0.0: copy/register
Installing library in /home/user/arbitrary/.stack-work/install/x86_64-linux/lts-11.14/8.2.2/lib/x86_64-linux-ghc-8.2.2/arbitrary-0.1.0.0-ImHjisNdlc5EoXoKrW90uk
Registering library for arbitrary-0.1.0.0..
$ 
$ stack test --flag='arbitrary:quickcheck'
arbitrary-0.1.0.0: unregistering (missing dependencies: QuickCheck)
primitive-0.6.4.0: using precompiled package
random-1.1: using precompiled package
Progress 0/6ghc-pkg: cannot find package primitive-0.6.4.0
ghc-pkg: cannot find package random-1.1
tf-random-0.5: using precompiled package
Progress 2/6: tf-random-0.5ghc-pkg: cannot find package tf-random-0.5
QuickCheck-2.10.1: using precompiled package
Progress 3/6: QuickCheck-2.10.1ghc-pkg: cannot find package QuickCheck-2.10.1
arbitrary-0.1.0.0: configure (lib + test)
Configuring arbitrary-0.1.0.0...
arbitrary-0.1.0.0: build (lib + test)
Preprocessing library for arbitrary-0.1.0.0..
Building library for arbitrary-0.1.0.0..
[1 of 1] Compiling Lib              ( src/Lib.hs, .stack-work/dist/x86_64-linux/Cabal-2.0.1.0/build/Lib.o )
Preprocessing test suite 'arbitrary-test' for arbitrary-0.1.0.0..
Building test suite 'arbitrary-test' for arbitrary-0.1.0.0..
[1 of 1] Compiling Main             ( test/Spec.hs, .stack-work/dist/x86_64-linux/Cabal-2.0.1.0/build/arbitrary-test/arbitrary-test-tmp/Main.o )
Linking .stack-work/dist/x86_64-linux/Cabal-2.0.1.0/build/arbitrary-test/arbitrary-test ...
arbitrary-0.1.0.0: copy/register
Installing library in /home/user/arbitrary/.stack-work/install/x86_64-linux/lts-11.14/8.2.2/lib/x86_64-linux-ghc-8.2.2/arbitrary-0.1.0.0-FnF56kbnAsXLPpyQ6XsKsW
Registering library for arbitrary-0.1.0.0..
arbitrary-0.1.0.0: test (suite: arbitrary-test)
                               
Progress 5/6: arbitrary-0.1.0.0+++ OK, passed 100 tests.
                               
arbitrary-0.1.0.0: Test suite arbitrary-test passed
Completed 6 action(s).         
import Test.QuickCheck
import Lib
propPlus :: Foo -> Foo -> Property
propPlus fx@(Foo x) fy@(Foo y) = Foo (x + y) === fx + fy
main :: IO ()
main = quickCheck propPlus
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment