Proposal: Overloaded Quotes for Template Haskell

(This was originally posted to haskell-cafe, but I've since learned that haskell-libraries is a more appropriate venue. Apologies for the resulting cross-post.) == Description == I would like to propose a generalization of the types of quote forms (i.e. "[| ... |]") and nested splices (i.e. "[| ... $( ... ) ... |]"). Currently quote forms have type "Q Exp" and nested splices expect their contents to have type "Q Exp". I propose that quote forms have type "forall m. Quasi m => m Exp" and nested splices expect a body of type "m Exp" where the "m" of the nested splice is the same as the "m" of the quote form that it is inside of. Top-level splices (i.e. those not inside a "[| ... |]") continue to expect their body to have type "Q Exp", and do *not* have a generalized to the type. (Otherwise, the compiler wouldn't know how to evaluate the monad. Nested splices do not have this problem as it is the responsibility of the user code between the splice and its enclosing quote to determine how to evaluate the monad.) Note that generalizing the types of quotes in this way is very similar to the generization of the types of strings that happens with the "-XOverloadedStrings" flag. However, unlike with strings which require an extra class (i.e. "IsString"), I believe generalizing quotes does not require an extra class. The already existing "Quasi" class is sufficient. An implementation could treat each quote or nested splice as mere syntactic sugar for an equivalent do-block, then use the usual type inference. == Example Use Case == I discovered a need for this generalization when writing a Template Haskell program in which part of it operates in a "StateT S Q a" monad instead of the usual "Q a" monad. The "S" type stores the state of a memoization table which contains code fragments already generated. Without memoization, the program would loop infinitely when processing certain recursive structures. It is fairly easy to declare an instance of "Quasi" for "StateT S Q", so that is not a problem. In order to keep the code clean, I'd like to use quotations with splices in them (i.e. [| ... $( ... ) ... |] ) for expressing the generated code. However, as quotations and splices are tied to the "Q" monad, I have to manually write "LamE ... VarP ... VarE ... etc." instead of using the much nicer quotation syntax. Without access to the quote syntax, I've had a one-line quotes turn into a half-a-dozen-line do-blocks. In this particular program, I can simulate the state monad using an IORef and lifting IO operations into Q using runIO. However, this merely a workaround. == Open Questions == ~~ Flags ~~ Generalizing the type of quotes can either be an "always-on" feature or it could be enabled by a flag (e.g. "-XOverloadedQuotes"). Since "Q" is an instance of "Quasi", we would expect programs that are type correct without the generalization to also be type correct with the generalization. Thus "always-on" would seem to be a backwards compatible change. However, while I am not an expert on GHC's type system, I suspect there are pathological cases where generalizing the types of quotes would result in the inference system not being able to infer a type that it would be able to without the generalization. In addition, type incorrect programs may yield more confusing type errors. I don't have a particular position on this question so long as the generalization is available (even if only by a flag) to those who want it. I suspect that, in the interests of design conservatism, having it as a flag will be the preferred choice. ~~ Quasi-Quotes ~~ The type of quasi-quoters (i.e. "String -> Q Exp") can be generalized to "forall m. Quasi m => String -> m Exp". However, this poses a compatibility program for existing user-written quasi-quoters. I am uncertain about the best way to handle this. It may be best to leave quasi-quoters without a generalized type unless someone can come up with a good backwards compatibility story. == Deadline == I propose the usual two week deadline (January 13) for discussion. If the overall response is positive, I'll submit a GHC Track ticket.

| Currently quote forms have type "Q Exp" and nested splices expect | their contents to have type "Q Exp". I propose that quote forms have | type "forall m. Quasi m => m Exp" and nested splices expect a body of | type "m Exp" where the "m" of the nested splice is the same as the "m" | of the quote form that it is inside of. Happily they already have that type. Have a look in Language.Haskell.TH.Syntax: newtype Q a = Q { unQ :: forall m. Quasi m => m a } You want a function from "Q Exp" to "forall m. Quasi m => m Exp". It would have type Q Exp -> forall m. Quasi m => m Exp or, isomorphically forall m. Quasi m => Q Exp -> m Exp And indeed there is such a function: it's called runQ. So I think what you want is already available. Happy new year Simon

On Fri, Dec 30, 2011 at 5:05 PM, Simon Peyton-Jones
| Currently quote forms have type "Q Exp" and nested splices expect | their contents to have type "Q Exp". I propose that quote forms have | type "forall m. Quasi m => m Exp" and nested splices expect a body of | type "m Exp" where the "m" of the nested splice is the same as the "m" | of the quote form that it is inside of.
Happily they already have that type. Have a look in Language.Haskell.TH.Syntax: newtype Q a = Q { unQ :: forall m. Quasi m => m a }
You want a function from "Q Exp" to "forall m. Quasi m => m Exp". It would have type
Q Exp -> forall m. Quasi m => m Exp
or, isomorphically
forall m. Quasi m => Q Exp -> m Exp
And indeed there is such a function: it's called runQ.
So I think what you want is already available.
If there are no splices in the quote then, yes, that is sufficient. However, as there is no function of type "forall m. Quasi m => m Exp -> Q Exp", the contents of all splices must be of type "Q Exp". Thus in the expression "[| ... $( foo ) ... |]" there is no way for foo to modify the state of the memoization table.

On Fri, Dec 30, 2011 at 05:50:18PM -0800, Michael D. Adams wrote:
On Fri, Dec 30, 2011 at 5:05 PM, Simon Peyton-Jones
wrote: | Currently quote forms have type "Q Exp" and nested splices expect | their contents to have type "Q Exp". I propose that quote forms have | type "forall m. Quasi m => m Exp" and nested splices expect a body of | type "m Exp" where the "m" of the nested splice is the same as the "m" | of the quote form that it is inside of.
Happily they already have that type. Have a look in Language.Haskell.TH.Syntax: newtype Q a = Q { unQ :: forall m. Quasi m => m a }
You want a function from "Q Exp" to "forall m. Quasi m => m Exp". It would have type
Q Exp -> forall m. Quasi m => m Exp
or, isomorphically
forall m. Quasi m => Q Exp -> m Exp
And indeed there is such a function: it's called runQ.
So I think what you want is already available.
If there are no splices in the quote then, yes, that is sufficient. However, as there is no function of type "forall m. Quasi m => m Exp -> Q Exp", the contents of all splices must be of type "Q Exp". Thus in the expression "[| ... $( foo ) ... |]" there is no way for foo to modify the state of the memoization table.
Given Simon's description of Q above, there is such a function: namely, the constructor Q. Unfortunately it seems that it is not exported. -Brent

| > And indeed there is such a function: it's called runQ. | > | > So I think what you want is already available. | | If there are no splices in the quote then, yes, that is sufficient. | However, as there is no function of type "forall m. Quasi m => m Exp | -> Q Exp", the contents of all splices must be of type "Q Exp". Thus | in the expression "[| ... $( foo ) ... |]" there is no way for foo to | modify the state of the memoization table. I'm sorry, I don't understand what you say here. You started your post by saying that you want quote to be of type forall m. Quasi m => m Exp But they already are! (With a newtype wrapper.) Simon

On Sat, Dec 31, 2011 at 7:55 AM, Simon Peyton-Jones
| > And indeed there is such a function: it's called runQ. | > | > So I think what you want is already available. | | If there are no splices in the quote then, yes, that is sufficient. | However, as there is no function of type "forall m. Quasi m => m Exp | -> Q Exp", the contents of all splices must be of type "Q Exp". Thus | in the expression "[| ... $( foo ) ... |]" there is no way for foo to | modify the state of the memoization table.
I'm sorry, I don't understand what you say here.
You started your post by saying that you want quote to be of type forall m. Quasi m => m Exp But they already are! (With a newtype wrapper.)
The constructor of that newtype wrapper is *not* exported by the Template Haskell libraries. The destructor *is* exported in the form of "runQ" (which is equivalent to "unQ" (and I don't know why the two names)), but the constructor "Q" is not. However, while it may look like exporting "Q" is sufficient to get what I want, that doesn't quite work. To see why it doesn't work, suppose we have: - "c :: StateT S Q Exp -> StateT S Q Exp" and - "e :: StateT S Q Exp" and - "instance Quasi (StateT s Q)". Now, consider "c (runQ [| ... $(Q (e)) ... |])". There will be a type error in the application of "Q" to "e" since "forall m. Quasi m => m Exp" is not an instantiation of "State S Q Exp". However, if the types of quotes and splices are generalized as I proposed, then "c [| ... $(e) ... |]" will type check just fine and achieve the effect that I'm after. Another way to think of this distinction is to the quotes as functions. The function "\e = [| ... $e ... |]" has type "Q Exp -> Q Exp". On the other hand the function "\e -> runQ [| ... $(Q e) ... |]" has type "(forall m. Quasi m => m Exp) -> (forall m. Quasi m => m Exp)". Note that the two "m" do not unify. Under my proposal "\e -> [| ... $e ... |]" would have type "forall m. Quasi m => m Exp -> m Exp".

I'm sorry Michael, I just don't get it. Maybe I'm just being slow.
One difficulty is that top-level splices (and hence, recursively) nested splices, must be run in the typechecker monad; and TH knows nothing of that monad. That's why the Quasi class exists in the first place; it says exactly what the "host monad" (the typehcecker in this case) must support. By moving to ST you are fixing the monad, and I just don't see how to make that work.
Perhaps the thing to do is to write some typing judgements, as in the TH paper, or to implement a prototype, or to write up a design in more detail. Template Haskell always does my head in.
Sorry not to be more helpful
Simon
| -----Original Message-----
| From: Michael D. Adams [mailto:mdmkolbe@gmail.com]
| Sent: 31 December 2011 19:59
| To: Simon Peyton-Jones
| Cc: libraries@haskell.org
| Subject: Re: Proposal: Overloaded Quotes for Template Haskell
|
| On Sat, Dec 31, 2011 at 7:55 AM, Simon Peyton-Jones
|

As suggested, I've written up the typing judgement that have to change
relative to the TH paper. (Only two judgments have to change.) I've
attached those in a PDF to avoid having to write typing judgments in
ASCII art. If the PDF causes any one trouble, let me know and I can
transcribe it to ASCII art.
I'm not opposed to writing an implementation myself, but I'm not
terribly familiar with the internals of GHC so it would be difficult.
(I really don't understand the treatment of "pending splices" or
HsSpliceOut.)
I agree that top-level splices must run in the type-checker monad, but
I don't see why nested splices would need to. For example, consider
"$( runStateT [| 1 + $(quux) |] 0)" where "quux :: StateT Int Q Exp"
and an appropriate "Quasi" instance is declared for "StateT". I'm
thinking of quotes like [| 1 + $(quux) |] as equivalent to "do e1 <-
quux; return (InfixE (Just (LitE (IntegerL 1))) (VarE "GHC.Num.+")
(Just e1)". What I'm proposing is simply that the quote form along
with it's immediately nested splices have the same types as in the
do-block form.
I do want to be sure you fully understand what I'm proposing. Let me
know if the type judgments don't make it clear, and I'll try to figure
out how to better explain it.
On Thu, Jan 5, 2012 at 10:08 AM, Simon Peyton-Jones
I'm sorry Michael, I just don't get it. Maybe I'm just being slow.
One difficulty is that top-level splices (and hence, recursively) nested splices, must be run in the typechecker monad; and TH knows nothing of that monad. That's why the Quasi class exists in the first place; it says exactly what the "host monad" (the typehcecker in this case) must support. By moving to ST you are fixing the monad, and I just don't see how to make that work.
Perhaps the thing to do is to write some typing judgements, as in the TH paper, or to implement a prototype, or to write up a design in more detail. Template Haskell always does my head in.
Sorry not to be more helpful
Simon
| -----Original Message----- | From: Michael D. Adams [mailto:mdmkolbe@gmail.com] | Sent: 31 December 2011 19:59 | To: Simon Peyton-Jones | Cc: libraries@haskell.org | Subject: Re: Proposal: Overloaded Quotes for Template Haskell | | On Sat, Dec 31, 2011 at 7:55 AM, Simon Peyton-Jones |
wrote: | > | > And indeed there is such a function: it's called runQ. | > | > | > | > So I think what you want is already available. | > | | > | If there are no splices in the quote then, yes, that is sufficient. | > | However, as there is no function of type "forall m. Quasi m => m Exp | > | -> Q Exp", the contents of all splices must be of type "Q Exp". Thus | > | in the expression "[| ... $( foo ) ... |]" there is no way for foo to | > | modify the state of the memoization table. | > | > I'm sorry, I don't understand what you say here. | > | > You started your post by saying that you want quote to be of type | > forall m. Quasi m => m Exp | > But they already are! (With a newtype wrapper.) | | The constructor of that newtype wrapper is *not* exported by the | Template Haskell libraries. The destructor *is* exported in the form | of "runQ" (which is equivalent to "unQ" (and I don't know why the two | names)), but the constructor "Q" is not. | | However, while it may look like exporting "Q" is sufficient to get | what I want, that doesn't quite work. | | To see why it doesn't work, suppose we have: | - "c :: StateT S Q Exp -> StateT S Q Exp" and | - "e :: StateT S Q Exp" and | - "instance Quasi (StateT s Q)". | | Now, consider "c (runQ [| ... $(Q (e)) ... |])". There will be a type | error in the application of "Q" to "e" since "forall m. Quasi m => m | Exp" is not an instantiation of "State S Q Exp". | | However, if the types of quotes and splices are generalized as I | proposed, then "c [| ... $(e) ... |]" will type check just fine and | achieve the effect that I'm after. | | Another way to think of this distinction is to the quotes as | functions. The function "\e = [| ... $e ... |]" has type "Q Exp -> Q | Exp". On the other hand the function "\e -> runQ [| ... $(Q e) ... | |]" has type "(forall m. Quasi m => m Exp) -> (forall m. Quasi m => m | Exp)". Note that the two "m" do not unify. Under my proposal "\e -> | [| ... $e ... |]" would have type "forall m. Quasi m => m Exp -> m | Exp".

Simon, the two-week deadline has passed but I'm hesitant to transfer
this to the bug tracker unless you're on board (GHC HQ would likely be
the ones to implement it afterall). Does what I'm suggesting make
sense, or do we need more discussion on/explanation of this?
On Thu, Jan 5, 2012 at 1:07 PM, Michael D. Adams
As suggested, I've written up the typing judgement that have to change relative to the TH paper. (Only two judgments have to change.) I've attached those in a PDF to avoid having to write typing judgments in ASCII art. If the PDF causes any one trouble, let me know and I can transcribe it to ASCII art.
I'm not opposed to writing an implementation myself, but I'm not terribly familiar with the internals of GHC so it would be difficult. (I really don't understand the treatment of "pending splices" or HsSpliceOut.)
I agree that top-level splices must run in the type-checker monad, but I don't see why nested splices would need to. For example, consider "$( runStateT [| 1 + $(quux) |] 0)" where "quux :: StateT Int Q Exp" and an appropriate "Quasi" instance is declared for "StateT". I'm thinking of quotes like [| 1 + $(quux) |] as equivalent to "do e1 <- quux; return (InfixE (Just (LitE (IntegerL 1))) (VarE "GHC.Num.+") (Just e1)". What I'm proposing is simply that the quote form along with it's immediately nested splices have the same types as in the do-block form.
I do want to be sure you fully understand what I'm proposing. Let me know if the type judgments don't make it clear, and I'll try to figure out how to better explain it.
On Thu, Jan 5, 2012 at 10:08 AM, Simon Peyton-Jones
wrote: I'm sorry Michael, I just don't get it. Maybe I'm just being slow.
One difficulty is that top-level splices (and hence, recursively) nested splices, must be run in the typechecker monad; and TH knows nothing of that monad. That's why the Quasi class exists in the first place; it says exactly what the "host monad" (the typehcecker in this case) must support. By moving to ST you are fixing the monad, and I just don't see how to make that work.
Perhaps the thing to do is to write some typing judgements, as in the TH paper, or to implement a prototype, or to write up a design in more detail. Template Haskell always does my head in.
Sorry not to be more helpful
Simon
| -----Original Message----- | From: Michael D. Adams [mailto:mdmkolbe@gmail.com] | Sent: 31 December 2011 19:59 | To: Simon Peyton-Jones | Cc: libraries@haskell.org | Subject: Re: Proposal: Overloaded Quotes for Template Haskell | | On Sat, Dec 31, 2011 at 7:55 AM, Simon Peyton-Jones |
wrote: | > | > And indeed there is such a function: it's called runQ. | > | > | > | > So I think what you want is already available. | > | | > | If there are no splices in the quote then, yes, that is sufficient. | > | However, as there is no function of type "forall m. Quasi m => m Exp | > | -> Q Exp", the contents of all splices must be of type "Q Exp". Thus | > | in the expression "[| ... $( foo ) ... |]" there is no way for foo to | > | modify the state of the memoization table. | > | > I'm sorry, I don't understand what you say here. | > | > You started your post by saying that you want quote to be of type | > forall m. Quasi m => m Exp | > But they already are! (With a newtype wrapper.) | | The constructor of that newtype wrapper is *not* exported by the | Template Haskell libraries. The destructor *is* exported in the form | of "runQ" (which is equivalent to "unQ" (and I don't know why the two | names)), but the constructor "Q" is not. | | However, while it may look like exporting "Q" is sufficient to get | what I want, that doesn't quite work. | | To see why it doesn't work, suppose we have: | - "c :: StateT S Q Exp -> StateT S Q Exp" and | - "e :: StateT S Q Exp" and | - "instance Quasi (StateT s Q)". | | Now, consider "c (runQ [| ... $(Q (e)) ... |])". There will be a type | error in the application of "Q" to "e" since "forall m. Quasi m => m | Exp" is not an instantiation of "State S Q Exp". | | However, if the types of quotes and splices are generalized as I | proposed, then "c [| ... $(e) ... |]" will type check just fine and | achieve the effect that I'm after. | | Another way to think of this distinction is to the quotes as | functions. The function "\e = [| ... $e ... |]" has type "Q Exp -> Q | Exp". On the other hand the function "\e -> runQ [| ... $(Q e) ... | |]" has type "(forall m. Quasi m => m Exp) -> (forall m. Quasi m => m | Exp)". Note that the two "m" do not unify. Under my proposal "\e -> | [| ... $e ... |]" would have type "forall m. Quasi m => m Exp -> m | Exp".

| Simon, the two-week deadline has passed but I'm hesitant to transfer
| this to the bug tracker unless you're on board (GHC HQ would likely be
| the ones to implement it afterall). Does what I'm suggesting make
| sense, or do we need more discussion on/explanation of this?
Um. What 2-week deadline? That's for library changes, whereas you are proposing a language extension, which is a much bigger deal.
You have done what I asked, and done a more careful description, but have fallen down on my end: I have not had time to give it the attention it deserves. I am just _utterly_ snowed under at the moment (www.computingatschool.org.uk).
Best thing is to
* start a GHC wiki page (like http://hackage.haskell.org/trac/ghc/wiki/Records)
* explain the problem you are trying to solve
* describe the solution, with examples
* link to your pdf
* do not rely on the reader also reading the email thread
I am interested to know whether anyone else is interested in your proposal. The more support, the more likely.
I do apologise for dragging my feet, but my bandwidth is limited.
Simon
|
| On Thu, Jan 5, 2012 at 1:07 PM, Michael D. Adams
participants (3)
-
Brent Yorgey
-
Michael D. Adams
-
Simon Peyton-Jones