This package has targets for the frontend of the code generation system, using an approach that generalizes arbitrary Haskell into a categorical representation that can then be translated to various targets (for example, C code).
For this plugin to work, you need to make some changes both to targets that use it directly, as well as ones that are depended on (transitively) by the ones that use it.
- enable the plugin with
-fplugin=Categorifier, - ensure inlining is available with
-fno-ignore-interface-pragmas(implied by-Oor-O2), and - import
Categorifier.Categorifyto makecategorifyavailable (you must import the entire module, but it may be qualified).
- use
-fno-omit-interface-pragmas(implied by-Oor-O2) to make sure inlinings are available and - use
-fexpose-all-unfoldingsto avoid having to add aninlinablepragma to every definition that may need to be inlined by the plugin. NB: This flag adds the optimized definition to the interface file, whereasinlinableadds the original definition. This distinction may be important during conversion.
- define instances for your target category using your preferred type class hierarchy (the default
is
base) - define
Categorifier.HasRepinstances for any types that you use in a converted function (the plugin will tell you if you are missing any when you try to convert)
It can be difficult to find a reasonable setting for the various inlining thresholds. This attempts to lay out an approach for identifying one.
There are two significant GHC flags for adjusting inlining, -funfolding-creation-threshold and
-funfolding-use-threshold. They allow you to set an upper bound on the "size" of unfoldings that
will be considered for inlining.
- set the
creation(globally) threshold high, say10000; - test to see if the inlining issue goes away (if so, skip to step 5);
- set the
use(incategorifymodules) threshold to match thecreationthreshold; - do a binary search on the
usethresholds to minimize them as much as possible; - do a binary search on the
creationthresholds to minimize them as much as possible (the lower bound here is probably the minimum of 750 (the default) and theusethreshold).
If either if these values is too small, you'll end up with errors complaining that some definition
couldn't be inlined. If they're too big, you'll get errors about "simplifier ticks exhausted" (in
which case, you can bump -fsimpl-tick-factor) and things will take a lot longer to compile.
You should use Categorifier.Client.deriveHasRep for all HasRep instances. However, for
some GADTs you'll currently have to write out a more direct instance.
The plugin attempts to convert a large part of Haskell to a category-agnostic form, however there are some features that aren't supported (or, not supported fully).
FFI- The plugin can not categorify arbitrary FFI calls. There is, however, support for parts oflibm.IO- The plugin can not categorify anything that operates on theRealWorldstate. If you only useMonadoperations (etc.) onIO, then it should categorify fine. But then you should also generalize the function to an arbitraryMonad.LinearTypes- We can categorify plugins with arbitrary multiplicities (for example, linear functions), but we don't yet take advantage of Evaluating Linear Functions to Symmetric Monoidal Categories to produce simpler categorical models.- mutual recursion - The plugin won’t work with any mutually-recursive definitions. Mutually-
recursive types are fine, but operations on them can't be mutually-recursive. Sometimes
(mutually-recursive
letorwherebindings), the plugin can identify it and will give a helpful error. However, in other cases (top-level bindings) the plugin can't identify the mutual recursion andcategorifywill get stuck in a loop during compilation. - polymorphic recursion - The plugin doesn’t currently work with polymorphically-recursive functions that need to be inlined (rather than directly interpreted).
- polymorphism - We can
categorifypolymorphic functions. However, the polymorphism may only be constrained by operations that we can interpret directly to the target category. For example, functions with types likeforall a. Foo a -> Intorforall a. Ord a => Foo a -> Intcan be categorified, butforall a. MyCustomClass a => Foo a -> Intmay not be able to. (IfMyCustomClasssimply aggregates operations from type classes inbase, then it will still categorify.)
A call to Categorifier.Categorify.expression must be fully saturated to trigger the rewrite
rule installed by the plugin. For example, expression . f $ x or
(if b then expression else somethingElse) (f x) doesn’t trigger the plugin, but
expression $ f x and expression (f x) do.
If you need to support a particular kind of partial application (such as expression . foo $ bar),
you may want to install an extra CoreToDo, which runs before the plugin and performs the
necessary rewriting (for example, rewrite expression . f $ x into expression (f x)).
BuildDictionary.hs is responsible for building type class dictionaries needed by the plugin to
satisfy constraints. It currently has a limitation that it can only find orphan instances in
exposed packages, not hidden packages. For example, suppose A.hs, B.hs and C.hs are in
target a, b and c, respectively. A.hs imports B.hs and B.hs imports C.hs, but A.hs doesn’t
directly import C.hs, and target a doesn’t directly depend on target c. Then, if C.hs
has an orphan instance, BuildDictionary.hs won't see it when compiling A.hs, because
C.hs is in a hidden package c. The workaround is to make target a directly depend
on target c, thereby exposing c to a (but you don't need to import C.hs in A.hs).
Conal Elliott's original implementation of his Compiling to Categories paper. It's the basis for this work, but has a lot of complexity and shortcomings that we've tried to address here. Some of the improvements we've made:
- require less monomorphization,
- support alternative type class hierarchies (including support for the
Arrowhierarchy inbase, - better error handling (both in terms of catching more things at compile time and having clearer messages), and
- support for sum types (in the paper, but not the current implementation of concat)
- some support for recursion via traced monoidal categories
- require fewer fiddly pragmas (for example,
inlineandrules) and other modifications to the client code, - simpler implementation with better modularity and more comments to hopefully make understanding the internals more accessible.
This is a more general package, but it does offer
Overloaded.Categories,
which is like Compiling to Categories applied to the Arrow language
extension.
This is ostensibly a more correct approach, given the way GHC is structured, but it relies on
Arrow (the extension, not the type class), and so the result is much less idiomatic Haskell.
There are a bunch of modules, this calls out the most important ones when diving in.
Categorifier- this is the entrypoint of the plugin, everything that hooks into GHC starts from here;Categorifier.Core.Categorify- the high-level logic of the categorical transformation as described in Conal's paper, it tries to define as clearly as possible the mapping from Hask to abstract categories;Categorifier.Hierarchy- the mappings from abstract categories to specific type class hierarchies.Categorifier.Test.Hask,Categorifier.Test.Term,Categorifier.Test.TotOrd- various categories defined (often against many type class hierarchies) for testing the plugin.
Writing a plugin is mostly the same as vanilla Haskell. The primary distinctions are
- you have an entrypoint other than
mainand - you have to use the GHC API.
The first means that there is a lot of stuff happening outside your code, and your code may be called zero or many times, the expressions that your code is applied to may be different than what you were expecting, or than what you created in a previous step, etc.
The second means dealing with decades of evolutionary code. We try our best to code defensively
(for example, call foo_maybe instead of foo), but things like panics and other surprising
behavior still surface.
As we work through the implementation, the types of debugging we use may change, so this section is expected to churn a bit, as new approaches are added and old ones are obsolesced.
We use a flag, Categorifier:defer-failures, to keep conversion failures from crashing GHC. However,
for the time being, all deferred failures are identical (SW-) -- they don't carry any
information about what failed. This makes them harder to debug. What you should do is constrain your
testing to exactly the failed test. That means
- comment out the line in plugins.bzl that mentions
Categorifier:defer-failures, - in TH.hs, comment out all the
testTermsother than the failing one, - in the Main.hs for the appropriate hierarchy, comment out the other
*TopLevelentries in the list, then - run a specific hierarchy test, for example,
concat-class-hierarchy.
Not all these steps are always necessary, but it can be hard to know when you can omit one.
This is a bit tedious, for sure. But it does often make the loop faster, and it ensures no other errors confuse issues. In future, we should preserve the actual failure for the test so it's easier to inspect what's happening less invasively.
The last case of findMaker tries to inline the identifier, which can be useful to track but it's
possible for this to cause a cycle with categorifyLambda (Plugins.App ...). To discover if you're
running into this, replace the Nothing with error [fmt|missing: {modu}.{var} {length args}|] and
see if you're trying to match the correct application.
We compile all the hierarchy tests with -dcore-lint, which catches cases where we’re generating
at least slightly off (and possibly bogus) Core.
The Core output (from either -dcore-lint or our own tracing) can be pretty noisy, so
-dsuppress-all substantially shortens the output, but it can also hide some critical
information. You can control the suppression a bit more with options like -dsuppress-coercions and
-dsuppress-type-applications (and their -dno- inverses).
If you need to inspect Core when there isn't a linting issue, you can add -dverbose-core2core to
see every transformation that happens.
Sometimes the plugin doesn't terminate (hopefully we can completely eliminate this possibility by treating conversion as an inductive fold in future). When this happens, there are a few ways you can recover what's happening.
If your GHC was built with libdw support (ours currently isn't) then you can send
SIGQUIT
(or Ctrl-\) to the process to get a stack trace of where it exited.
Or, sending SIGUSR2 will cause any accumulated IO actions to run before the program
exits. This can be useful for recovering any tracing output.