Personal tools

Module signature

From HaskellWiki

Jump to: navigation, search

A module signature is a type signature for a module. Similar to hs-boot files, module signatures contain only type definitions and type signatures, and do not have any value bindings:

signature Str where
 
data Str
empty :: Str
append :: Str -> Str -> Str

A package may define signatures in lieu of modules (e.g., as in str-sig); clients of these packages must provide implementations of the modules specified by these signatures before being able to compile and run these packages.

Signatures are part of Backpack, a new facility for modular programming in GHC; here, they are used to defer the choice of an implementation of a module to a user. These features are only compatible with GHC 8.2, and currently are only allowed in packages uploaded to next.hackage, an experimental staging ground for packages that use these new facilities.

In the rest of this page, we'll give some guidance for how to make use of these features if you are on GHC 8.2.

Contents

1 For the impatient

There's a package on Hackage, you want to use it, but it has some signatures and you're not sure what to do (we call this an indefinite package). Here is what you should do:

  1. Does the package define any modules? Pure signature packages like str-sig don't define any modules, meaning that they are purely intended for developers who want to use this signature to parametrize their own packages. If you are actually interested in using signatures to parametrize your package, skip to #How to use a signature package; the quick start won't help you.
  2. Find out if the package author has defined any pre-instantiated packages which have the implementations at the types you want; usually, they should be linked from the package description in question. For example, if you are looking at the simple-regex-indef package, and you want to use the regular expressions on strings, check and see if there is a simple-regex-string package. (TODO: link the example) If so, you can directly depend on that package and ignore the existence of Backpack (just remember that you need to be running GHC 8.2 or later.)
  3. If the author has not defined a pre-instantiated package, you may be able to instantiate it yourself. First, you have to find a package which provides implementations of the signatures in question; if the package depends on a signature package, you can probably click to that package to find out what valid implementations of that signature are available. Then, you will need to instantiate the package. This is done by depending on both the indefinite package and the implementing package, e.g., build-depends: simple-regex-indef, str-string: this will instantiate simple-regex-indef with whatever implementing module str-string brings into scope under the name Str; if you want to use a differently named module, use the mixins field to rename the module you want to the signature name, e.g., mixins: str-bytestring (Str.ByteString.Lazy as Str)
  4. If you do all that, but GHC complains that the signature requires a function/type which is not defined by the module, this means that the implementing module doesn't implement enough functionality to support the requirements of the package in question. You might be able to extend module with the missing functionality, see #FAQ.

2 The high-level picture

In a package ecosystem with Backpack, there are various types of packages which serve different purposes for enabling modularity:

  • A signature package (e.g., str-sig) defines only signatures (no modules). They follow the naming convention "foo-sig". Signature packages serve two purposes: first, they define the full namespace of operations which may be supported by modules, helping coordinate implementations of the signatures so that they don't define two functions with different types/semantics under the same name. Second, they provide a set of reusable type signatures, which other packages can use to specify their requirements.
  • An implementation package is a package whose express purpose is to implement a signature defined by a signature package. They follow the naming convention "foo-implname". An implementation package may not implement *all* of the signatures defined by a signature package, but there should be some subset of the signature which they do implement fully. Often these packages are thin wrappers around existing Haskell libraries, renaming functions so that they match the naming convention of the signature package.
  • An indefinite package is a package which has been parametrized over a signature; in ML-parlance, we'd refer to these as functors. They follow the naming convention "bar-indef". An indefinite package may specify its requirements as a one-off local source signature, or they define the requirement by taking a subset of the signatures from a signature package. Users of indefinite packages can either instantiate these requirements with implementation packages, or pass on the the requirement to their clients (making them, in turn, an indefinite package.)
  • A pre-instantiated package is a package which instantiates an indefinite package with an implementation package. Pre-instantiated packages are helpful because they let users make use of code which is implemented using Backpack under the hood without needing to know anything about Backpack themselves.

The overall life cycle is that, when a desirable API for modularizing over is identified, a signature package and several implementation packages are written in tandem, to flesh out the new namespace. When these are stable, users who want to parametrize over this API can base their requirements off the signature package (e.g., by using signature thinning), and then let users instantiate their library and/or provide pre-instantiated packages for ease of use.

3 How to use a signature package

Let's suppose you want to use the str-sig signature package. The general recipe is this:

  1. Add str-sig to your build-depends.
  2. Write your library, using functions from Str that were specified by str-sig.
  3. Run ghc-usage (cabal new-repl -w ghc-usage), and copy paste the export lists of the signatures into a Foo.hsig file, so you end up with something like:
      signature Foo( Str, empty, append ) where
          import Prelude () -- don't omit this!
  4. Add the signature to your Cabal file with signatures: Str. If you add or remove functions you are using, modify the export list.

In more detail:

First, go read this blog post, which explains how to get and use Backpack on a simple example where we write all the signatures by hand. The rest of this tutorial will assume a basic working knowledge on how to use Backpack.

The point of a signature package is to avoid having to write signatures by hand; by simply adding build-depends: str-sig, we inherit all of the requirements of foo-sig. There is one catch, however: str-sig is a very big signature, which contains every function you could possibly imagine on strings; in practice, an actual library is never going to use all of these functions. If we directly depend on str-sig, we are claiming to depend on everything that str-sig defines, and that's a problem, because none of the implementation packages actually defines every function!

To solve this problem, after we include str-sig, we are going to use signature thinning to reduce the set of entities which we are going to require in the end. A signature that thins its exports looks like this:

signature Str( Str, Chr, Position, empty, concat ) where
    import Prelude () -- don't omit this!

Here, the export list of the Str signature refers to declarations which are not locally defined; in fact, they refer to the entities from Str in str-sig. You can think of this as a centralized import list: just write all of the functions which you actually want to bring into scope, and only those functions will be brought into scope when you import Str. The import Prelude () ensures that an identifier like concat doesn't get resolved to the concat from Prelude.

You can write this export list by hand, or you can generate it automatically using ghc-usage; just run cabal new-repl -w ghc-usage --with-ghc-pkg=ghc-pkg-8.2 in the library your running, and it will print out the export lists of all your signatures (and modules too); copy paste them into Str.hsig.

4 FAQ

I'm not using GHC 8.2, is there anything to see here? Implementation packages like str-string generally don't make use of any Backpack features, so you may decide to use them for extra uniformity of interfaces.

How do I instantiate a library with X? The general recipe is to add mixins: str-IMPL (Str.IMPL as Str) to the Cabal file that wants to instantiate another one of its dependencies. Implemenation packages will also often have a "default" implementation (e.g., strict, binary ByteStrings for str-bytestring and strict Text for str-text) which can be used without a mixin field at all.

The signature doesn't contain functions I need. Just add your own functions to your signature file. For example:

signature Str( Str, append, splits ) where
    import Prelude () -- don't omit this!
    data Str
    splits :: Str -> [(Str, Str)]
    -- append is still inherited from str-sig

Note that you will need to declare data Str, etc., for any abstract type you want to make reference to.

If your method is of general interest, consider submitting it for inclusion in str-sig.

The implementation module doesn't implement enough functions. In this case, you should define your own implementation module (it will need to be in a library or package separate from the one you're parametrizing over), importing and reexporting the existing implementations to avoid having to repeat yourself:

module Str.String.Splits (
    module Str.String, -- we get Str and append from here
    splits
) where
    import Str.String
    splits = ...

If you implemented a function that is in the canonical str-sig, consider submitting it upstream to improve implementation coverage.