-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Haskell's arrow notation #1
Comments
The trouble with making commands less second class is that they fundamentally involve a complex notion of lexical closure. Given a command For a command to stand on its own, it needs to include information about which variables are free in its body within its type. This is complicated, as it requires a kind of extra dimension of information than types normally contain. For a way to encode this, one could imagine an entirely new kind of type, an Using this new Command :: IdBag -> [Type] -> Type -> Type where the |
I think your |
Yes, they are related, though I think the command type given by Definition 3 lacks the information about the free variables, which is the biggest obstacle to a command standing on its own. Take a look at Type and Translation Rules for Arrow Notation in GHC, and note how the context of arrow-local variables (named Δ) is interwoven with both typechecking and desugaring rules. My If you want commands to be real values, it becomes possible to move them out of their current context into some other context entirely, and the assumptions made by the current rules for |
Isn't your |
Oh, I suppose you are right. But Paterson’s |
I would love an arrow syntax that allowed the types to look like the curried function types that we're used to in Haskell. For example, instead of keyed :: Rule m (e, k, a) b -> Rule m (e, Map k a) (Map k b) to have something like (following Paterson's notation) keyed :: (k ⇀ a ⇀ Rule m \ b) -> (Map k a ⇀ Rule m \ Map k b) This looks a lot like the monadic version would keyed :: (k -> a -> Rule m b) -> (Map k a -> Rule m (Map k b)) But even if we could get keyed :: (k ⇀ a ⇀ e ⇀ Rule m \ b) -> (Map k a ⇀ e ⇀ Rule m \ Map k b) This looks less pleasant, but it's still workable. |
I think we probably have to put the environment into the terminal type constructor. Can we have something like data Command arr env =
A arr env Type
| (⇀) Type (Command arr) |
The issue I was alluding to above is that it isn’t clear what |
Oh, I assumed that keeping it polymorphic would always be fine. |
If you can take the command out of its context and pass it around, where does the quantifier go? |
Hmm, it feels like keeping it polymorphic a la ST should be sufficient, but I may not be imagining the same sorts of things that you are. |
To be a little less cryptic, let me put it another way. When GHC checks a control operator against a polymorphic type, that’s okay, because a control operator isn’t a command, but a command maker. All that polymorphic type is saying is that the command maker needs to work on commands with any environment, and it needs to treat them all equivalently. That’s fine. But GHC never assigns a polymorphic type to the environment of an actual command itself. That would be a big problem! If a command is given the type So after GHC checks a control operator against the polymorphic type, it actually immediately instantiates it to a concrete type before continuing with typechecking. It only uses the polymorphism to ensure the control operator can’t observe the concrete type, since it’s an implementation detail. Put yet another way, the polymorphism in a control operator is simply a way of ensuring two environments are exactly the same (specifically, the input environment to the resulting command and the input environment to its arguments). It pins a relationship between two commands. If you want a command to be able to stand on its own, there’s no relationship you can take advantage of, there’s just an environment, so you can’t get away with the polymorphism trick. |
This is an "embedded" version of the typing rules that I'm imagining. I've used GADTs rather than TypeFamilies as I prefer to keep things explicit. That implies a certain amount of syntactic noise though. It wouldn't leak through to the user though. {-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
import GHC.Types
import Control.Arrow
import Control.Category
data CG type_ = A type_ | (:->) type_ (CG type_)
infixr :->
infixr :->.
type (:->.) (a :: k) (b :: k) = a :-> A b
data Command arr env c where
I :: arr env b -> Command arr env (A b)
K :: Command arr (env, a) b
-> Command arr env (a :-> b)
data CO f c where
J :: f b -> CO f (A b)
L :: (f b -> CO f c) -> CO f (b :-> c)
type ControlOperator arr env = CO (Command arr env)
originalKeyed :: (Eq k, Applicative m)
=> Rule m (e, k, a) b
-> Rule m (e, Map k a) (Map k b)
originalKeyed = undefined
keyed :: (Eq k, Applicative m)
=> ControlOperator (Rule m) env
((k :-> a :->. b) :->. (Map k a :->. Map k b))
-- It's the type that's important. The implementation is too messy to
-- care about looking it.
keyed = L (\x ->
let K (K (I x')) = x
mungeArguments = (arr (\(x, y, z) -> ((x, y), z)) >>>)
in J (K (I (originalKeyed (mungeArguments x')))))
-- Irrelevant cruft:
data Map k v
-- Only doing this proxy business to avoid turning on KindSignatures
data Rule m a b = Rule (Proxy (m a))
data Proxy a = Proxy
instance Category (Rule m)
instance Arrow (Rule m) [EDIT: Improved names] [EDIT2: Generalised to a jaw-dropping degree] |
Naively, I expect the
|
I think you might be focusing too much on control operators (e.g. Consider a simple use of proc (foo, bar) -> do
baz <- f -< foo
(g -< bar) `handle` \e -> h -< (e, baz) Now, imagine we’d like to pull the proc (foo, bar) -> do
baz <- f -< foo
let h' = \e -> h -< (e, baz)
(g -< bar) `handle` h' The key question is: what is the type of Given we’ve already established a command has three components—environment, argument stack, and return type—two thirds of the question is easy to answer. If we assume To reiterate:
|
Let's see if I can catch up with you a bit. It seems to me that we would like to be able to go one step further and write proc (foo, bar) -> do
baz <- f -< foo
let h' = \e -> h -< (e, baz)
let g' = (g -< bar)
g' `handle` h' Assume |
In that particular case, yes… but depending on whether or not you believe Let Should Not Be Generalized, I think it’s not necessarily clear that their inferred types should be identical. Consider this slightly modified example: proc (foo, bar) -> do
let h' = \e -> h -< (e, foo)
let g' = g -< bar
baz <- g' `handle` h'
let i' = i -< baz
i' `handle` h' In this example, |
Intuition question: is the |
Yes, exactly. Sorry if I hadn’t made that clear already; that’s sort of essential. The thing that makes it complicated is you don’t actually have arrow closures, since arrows only have inputs and outputs. So when you desugar something like proc (x, y) -> do
z <- f -< x
(g -< z) `handle` (h -< y) you have to turn it into something like first f >>> handle (first g) (second h) and for more elaborate kinds of composition it gets ugly really quick. One thing I guess you could maybe consider, which I haven’t really thought about, is if it would be possible to extend the notion of closure to arrows. That is, could you somehow introduce an operation that encodes closure more directly? I’m not sure. I’m not really sure what that would mean, since closure implies a lexical scope “outside” the arrow that it can close over, but you have to be “trapped inside” the arrow for it to work, since otherwise you have to thread arrows through arrows and that needs |
Ah great. What confused me is that the type of a control operator is supposed to be forall b. (b ~> t1) -> ... -> (b ~> tn) -> (b ~> t) but now I realise (I think) that this will effectively only be ever used at type forall a1 ... an. (a1 ~> t1) -> ... -> (an ~> tn) -> ((a1, ..., an) ~> t) by choosing |
Specifically, where Paterson has proc p -> form e c1 ... cn = e (proc p -> c1 ) ... (proc p -> cn ) couldn't we replace it with proc (p1, ..., pn) -> form e c1 ... cn = e (proc p1 -> c1) ... (proc pn -> cn) |
Essentially yes, but the type you’ve written isn’t quite right, since all the forall a1 ... an. ((a1, ..., an) ~> t1) -> ... -> ((a1, ..., an) ~> tn) -> ((a1, ..., an) ~> t) and then each argument projects what it needs out of that tuple.
No, because all the arguments have to have the same type. Remember: you don’t control |
OK, I'll have to mull that over a bit more. Another question: is this basically just doing closure conversion? |
Yes, it is pretty much precisely closure conversion. The problem is the implementation details of our conversion algorithm leak out into the type system. |
Great. Until future notice I will think of this as performing closure conversion with only a restricted set of operations to hand. |
I observed two things that interested me. Firstly, if we can make abstractions of the form we've been considering then we can package up things that only exist in the "arrow argument scope" as commands and then later extract them, e.g. ...
let bAsACommand = returnA -< b
...
b <- bAsACommand -< () There's nothing new in this except that this particular way of looking at things was new to me. Secondly, any " proc (foo, bar) -> do
baz <- f -< foo
let h' = \e -> h -< (e, baz)
baz <- somethingElse -< quux
(g -< bar) `handle` h' then if the |
Yes, that’s what I was talking about in #1 (comment) (see points number 2 and 3). It’s tricky! I can’t think of a solution other than my complicated |
Ah yes, I see. I'm glad I'm finally catching up :) |
I'm pleased to report that in the intervening four weeks I've managed to improve my understanding to the point where all your comments above make sense to me. Previously I didn't really have a solid grasp of the issues under discussion. This summarises my current beliefs:
|
|
As a regular user of arrows, I believe I could contribute to advancements in this area in GHC, but I am behind on my understanding of the current state of arrows, and the problem that this issue is trying to solve. As time passes, papers are outdated, and it becomes harder and harder to catch up. This was originally my problem: syntax that I thought was supported wasn't, and so I learned by using them, but saw no path from papers to code. I do not understand all the syntax that is available. For example, I never used banana brackets. I also normally limit my use of arrows to Arrow, ArrowLoop, and ArrowChoice. What is the smallest set of up-to-date references that one would have to read to understand arrows as they are implemented today? |
As far as I can tell, if you demand they be both complete and fully up to date, the minimal set currently includes the GHC source code. Alas. Type and Translation Rules for Arrow Notation in GHC is by far the closest thing we have, though it includes no explanatory prose, so it’s best viewed as a revision of the information in Paterson’s original paper. However, even those two do not form a full picture, as the changes made in GHC 7.10 are not represented in either of them. The relevant section of the GHC User’s Guide gets most of the rest of the way there, but it is imprecise and misses a few subtleties. That said, I think understanding the above three resources will basically have you up to speed, and it’s certainly enough to be able to have an informed discussion. Some of the added context explained in my proposal can also help to understand a few of the subtleties absent from the GHC User’s Guide. If that proposal is accepted and implemented, I’ll try to update the Types and Translation Rules document to reflect the status quo as part of those changes, but for now, that’s the situation. |
This answer expands a little bit on this idea |
No description provided.
The text was updated successfully, but these errors were encountered: