Search code examples
haskelldependenciescode-coveragehaskell-stackquickcheck

Where do I define Arbitrary instances?


I can't figure out where to define Arbitrary instances for my datatype. If I put it in the package, then the package unnecessarily has to have QuickCheck as a dependency. If I put it in the tests, then other packages can't make use of the instance. If I put it in a separate test-utils package then the tests also have to live in a separate package so it's an orphan instance and also stack test --coverage doesn't work.

What other options are there?


Solution

  • I'd usually pick the separate package option — but then I don't use stack test --coverage. Thanks for introducing me to it!

    (Edit: I'd probably do this, and then use the test flag option only for running stack test --coverage --flag thepackage:arbitrary so that nobody else has to deal with the flags.)

    It may also be worth raising the --coverage issue on the stack issue tracker, as it would be good for the coverage check to work in this case.

    You ask for other options — the best one is probably a test flag.

    A test flag

    It is possible to define a flag in your cabal file (defaulting to false) which will only build the modules with your QuickCheck dependency if the flag is selected.

    Place the required code in the directory arbitrary (for example). Then add the equivalent of the following to the relevant parts of your package.yaml (1st snippet) or the-library.cabal (2nd snippet) file:

    flags:
      arbitrary:
        description: Compile with arbitrary instances
        default: false
        manual: true
    
    library:
      ⁝
      when:
      - condition: flag(arbitrary)
        dependencies:
        - QuickCheck
        source-dirs:
        - arbitrary
    
    flag arbitrary
      description: Compile with arbitrary instances
      manual: True
      default: False
    
    library
      ⁝
      if flag(arbitrary)
        hs-source-dirs:
          arbitrary
        build-depends:
          QuickCheck
    

    Then, packages which want to use the instances should add the following in their stack.yaml (1st) or cabal.project (2nd) files:

    flag:
      the-library:
        arbitrary: true
    
    constraints: the-library +arbitrary
    

    But there's a slight problem… there is currently no way for that library to only depend on the +arbitrary version in only its test suite, unless it also defines such a flag. This may be a price worth paying.

    Note: I haven't tested the downstream packaging, yet.

    Ivan Milenovic's blog was useful as an initial resource.

    DerivingVia/Generic instances

    There may be another possibility, now that GHC 8.6 has been released, with DerivingVia. There's a case study in Blöndal, Löh & Scott (2018) for Arbitrary instances.

    You would create newtype wrappers, and implement Arbitrary for those newtypes.

    It doesn't quite avoid the problem, as is. But you may be able to implement Generic for those newtypes in such a way that the instances derivable using generic-arbitrary match what you'd want.


    There may be some other options. In particular, QuickCheck's dependencies aren't actually that heavy. And there are other testing libraries, too. Plus, note that there has been some discussion of separating an Arbitrary-like typeclass into a standalone library.

    I would also have suggested internal libraries, but that doesn't allow other packages to use your instances.