Are ‘orphan’ rewrite rules a bad idea?

As far as I can tell from the manual, nothing’s stopping me from writing some fusion-y rewrite rules for some base functions in one of my own modules, and any consumers of that module get my rewrite rules (which is what I want; it enables more inlining). This intuitively seems like shaky ground, but I haven’t found any advice for or against the technique. Who’s got opinions?

(The rules in question, if anyone has any feedback on them for me:)

{-# RULES
"foldrMap1/build" 
  forall f k x (g :: forall b. (a -> b -> b) -> b -> b).
  foldrMap1 f k (x :| build g) =
    g (\y f' z -> k z (f' y)) f x

"foldrMap1/augment"
  forall f k x xs (g :: forall b. (a -> b -> b) -> b -> b).
  foldrMap1 f k (x :| augment g xs) =
    g (\y f' z -> k z (f' y)) (foldr (\y f' z -> k z (f' y)) f xs) x
#-}
3 Likes

From the GHC User Guide, we find two relevant snippets of documentation:

A rule does not need to be in the same module as (any of) the variables it mentions, though of course they need to be in scope.

The first snippet explicitly tells us that an orphan rewrite rules are an expected and supported case.

All rules are implicitly exported from the module, and are therefore in force in any module that imports the module that defined the rule, directly or indirectly.

The second snippet tells us that rewrite rules are controlled / scoped through imports, and so the user may safely choose whether to import a module that includes rewrite rules, without affecting other modules that do not import any such modules.

This doesn’t really answer whether it is a ‘bad’ idea, but it does tell us that we can (somewhat) safely control their effect similarly class instances.

The main problem with orphan instances is that you can get in the situation where there are two matching instances, where the choice of instance could affect the semantics of your program. But rewrite rules should always preserve semantics, so I don’t think orphan rules are directly problematic. Still, I’d start a CLC proposal to get these rules added to base such that everyone can profit.

1 Like

Wait…so I can release a Haskell library that rewrites arbitrary code in base in potentially incorrect ways?

You can release a Haskell library that runs Template Haskell or a cabal setup script which erases all data on the computers of anyone who adds that library as a dependency.

Hah yeah I’m aware of those. But this is a fun and original way Haskell libraries can misbehave :laughing: Imagine a rule that makes id call exitFailure once in a while. That would be wild to hunt down.

Right, this is more or less my concern: what if my rewrite rules are a bad idea for some reason (maybe they pessimize some case that a consumer-of-a-consumer tried particularly hard to optimize)? There’s no way to opt out of a rule once it appears anywhere in the transitive dependencies of your code, right? That seems like the sort of thing that would not make me a popular library author.

OTOH, things that can be tried and tested in non-core libraries before they’re absorbed into core libraries should be, right? And if rewrite rules are technically in that class…

1 Like

I don’t believe this is quite the case. As I understand the documentation, you can release a Haskell library with rewrite rules for functions in base, but it doesn’t go back and apply to base itself because base does not import the rewrite rule. Only new code written in the new module with the rewrite rules in scope should have the rewrite rule applied to it.

Please correct me if I am wrong because if I am, I have grossly misunderstood something.

With a sufficiently liberal definition of ‘new code’, this is true. But you have to watch out for things getting inlined into the new module—for example.

4 Likes

That is a most excellent observation, exactly the sort of detail I was hoping for. I wish that I could heart that a second time for using the Haskell playground to demonstrate :slight_smile:

1 Like