Template Haskell is unhygienic. Can we change it?
This is just a discussion ticket for now; following through on it would need a GHC proposal.
Consider
{-# LANGUAGE TemplateHaskell #-}
module Lib where
f :: Maybe a -> Maybe a
f y = case y of
$([p| Just x |]) -> y
This compiles and type-checks. Now I supposedly alpha-convert f
:
{-# LANGUAGE TemplateHaskell #-}
module Lib where
f :: Maybe a -> Maybe a
f x = case x of
$([p| Just x |]) -> x
... and get a type error, because the Just x
pattern quote has captured x
, which is of type a
now.
This capturing of variables renders Template Haskell unhygienic. Macro hygiene is quite like referential transparency for macro systems/splices: If I can't rename y
consistently to x
without altering program semantics, then the macro system is not hygienic.
The Scheme, Rust and Lean community have put enormous efforts into their macro systems, and all of them are (mostly) hygienic.
Why is Template Haskell unhygienic? The reason is not that quotes may refer to names bound outside; it rather is that splices may produce binding constructs using names that capture local variables.
Lack of hygiene is problematic:
- In general, the splice
$([p| Just x |])
might not capturex
in such an obvious way through a pattern quote; we could have$(conP "Just" (varP "x"))
for example, and thex
is just aString
there or could well be read from another file throughIO
. - The result is completely unpredictable (i.e. dynamic) scoping behavior! We cannot do name resolution unless we run all the splices first. One consequence of this is the dreaded stage restriction: Since a custom splice function
foo
cannot be renamed until all the splices have been run in the module definingfoo
, we cannot usefoo
in the same module it is defined. Related: #21051.
So a net gain of this proposal would be that we would no longer need the stage restriction.
This is what we need to fix in order to make TH hygienic: Whenever a binding construct is spliced in, we must rename any binders to use fresh names, and do the same to its use sites. For example, f x = $([| \x -> x |])
must expand to f x1 = \x2 -> x2
, whereas f x = $((\e -> [| \x -> $(e) |]) [| x |])
must expand to f x1 = \x2 -> x1
.