-
Notifications
You must be signed in to change notification settings - Fork 1
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
Provide APIs for viewing and editing kinds of type parameters #1095
Conversation
78e5e4d
to
161af8a
Compare
6151ff8
to
896d9c8
Compare
4798b9b
to
5bbf4d0
Compare
primer/src/Primer/Action.hs
Outdated
@@ -1290,7 +1309,7 @@ toProgActionInput def0 sel0 opt0 = \case | |||
opt <- optNoCxt | |||
sel <- typeSel | |||
index <- length . astTypeDefParameters <$> typeDef -- for now, we always add on to the end | |||
pure [AddTypeParam sel.def index opt C.KType] | |||
pure [AddTypeParam sel.def index opt $ C.KHole ()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually want this last commit? Starting with a hole seems like the obvious thing to do now that we can actually construct kinds, and it's consistent with terms and types, but it may be more annoying to use.
There's a wider conversation to be had about the extent to which we want to hide kinds from beginners. If we do, this is potentially a step in the wrong direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I'm not sure.
There is one sense in which kinds are different to terms/types: since they are so simple, there is a canonical thing to put in the leaves of a non-holey tree: a KType
. Since our kind actions will all push the current kind down (i.e. k ~> k -> ?
), one will always end up filling in this hole with a KType
. (This is not true in types/terms, since there are more options for leaves.).
Indeed, if we are going the direction of "leave as KType
because of the above justification", I would perhaps argue that inserting an arrow should not create k -> ?
, but k -> KType
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brprice's proposal sounds good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. One could argue that we should go a step further and not allow constructing KHole
at all. In practice I suppose this would mean, in addition to reverting the change above, replacing the "delete kind" action with a "raise" action, offered only on KFun
nodes.
Now that I think about it, the purpose of KHole
is somewhat unclear, given that we have such a simple kind system. Is it just used to give a kind to TEmptyHole
? And would it be reasonable to just say that that has the kind KType
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. One could argue that we should go a step further and not allow constructing
KHole
at all. In practice I suppose this would mean, in addition to reverting the change above, replacing the "delete kind" action with a "raise" action, offered only onKFun
nodes.
Potentially, but see below about why KHole
exists (and is not easily going away). Thus if we forbade constructing KHole
at all, it would be somewhat inconsistent.
Being able to give hole kinds does enable students to write more "programs" (i.e. things the system accepts, but are obvious nonsense). This may be good or bad depending on whether this is the sort of confusion/freedom that is good for learning(!), and on whether these states actually have utility as intermediate states in interactive use (I have not got any intuition on this as yet). In particular, one could write something like data T (p : ?) = C (p p) p
, where p
is being used at multiple kinds: (roughly speaking) k -> *
, k
and *
(for some unknown k
). It is not possible to fill in the KHole
here to get a valid complete program. (As an aside, it is possible to make sense of this in a richer type system, by giving p
a polymorphic kind; e.g. ghc will accept data D (p :: forall k . k) = C p (p p)
. I don't know whether things of this ilk are practically useful.)
Now that I think about it, the purpose of
KHole
is somewhat unclear, given that we have such a simple kind system. Is it just used to give a kind toTEmptyHole
? And would it be reasonable to just say that that has the kindKType
instead?
It is indeed used to kind TEmptyHole
(iirc). In the past we did say TEmptyHole
has kind KType
, but there was an issue with that, and I changed it https://github.com/hackworthltd/vonnegut/commit/0f291a3db0904e1d05b1af60201358565089ac9a . In particular, we really want to be able to create type applications via the sequence (e.g.) ? ; ? ? ; List ? ; List Bool
, but the intermediate type ? ?
is checked by synthesising a kind for the first hole, ensuring it is (consistent with) an arrow kind and the second hole matches the domain -- this would fail if we gave the kind KType
. (This is not the only way to solve this problem, but is the one we currently use, and would need to revisit this if we wanted to give the kind KType
to type holes.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Being able to give hole kinds does enable students to write more "programs" (i.e. things the system accepts, but are obvious nonsense).
This is interesting, and I'd never really considered that our type holes work in this way. And it is unique to type-level holes, because of the lack of kind-polymorphism: we allow terms like λx. x x
(which GHC doesn't accept without further annotations), but this can be completed with appropriate annotations and type applications.
Anyway, the point about kindchecking ? ?
(EDIT: also made at #108 (comment)) is a good one (although, just to put it out there, it would go away if we were able to treat type constructors as always-saturated, like we do for value constructors). So
I'll implement your original proposal.
primer-api/src/Primer/API.hs
Outdated
maybe (throw' $ ParamNotFound p) (pure . Kind . viewTreeKind . snd) $ | ||
find ((== p) . fst) (astTypeDefParameters def) | ||
maybe (throw' $ ParamNotFound p.param) (pure . Kind . viewTreeKind . snd) $ | ||
find ((== p.param) . fst) (astTypeDefParameters def) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means we also display the whole kind when a node in the kind is selected. This is a bit weird, but I don't really know what else we could do - just say "kind"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of the current behaviour. I think it would be better to (as you suggest) just say "kind".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be easy enough to implement, changing data TypeOrKind = Type Tree | Kind Tree
to data TypeOrKind = Type Tree | Kind Tree | SortKind
(naming?). But I don't know how we would want to communicate this last case in our primer-app
UI.
@dhess Any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record:
ghci> :k *
* :: *
But I think mimicking this would be less technically correct for Primer (in modern GHC Haskell, kinds really are just types).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand the question, but let's make sure. Is this about what type (aka "kind") to display when a kind is, say, * -> * -> *
, and the student selects a non-root node in that tree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prior to this PR, we have a function Selection -> (Type | Kind)
. When any value-level node is selected it returns its type, and when any type-level node is selected we return its kind. The question is essentially: now that a selection can be of a kind-level node, what should be returned in that case?
(which I think is almost what you were saying, except that it includes root nodes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's discuss in the upcoming developer meeting. (This item is now in the agenda.)
} | ||
} | ||
} | ||
) | ||
ParamKindAction tyName paramName id actions -> editModuleOfCrossType (Just tyName) prog $ \(mod, mods) defName def -> do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commit is, as mentioned in the commit message, a little bit of a hack, but not one that I'm concerned about merging, since it will be made more robust as part of #1098. The current approach is not ideal since we traverse both branches even though only one will have a matching ID, and we don't fail if the ID isn't found anywhere. See "type edits currently only allowed in typedefs"
in applyAction'
: the main logic should in principle be implemented there, but can't be until Loc
(part of our zipper interface) is generalised to account for kinds.
We also end up performing a full typechecking pass (checkEverything
) for now because manually fixing up typecaches, as we do with many of the other typedef actions, would be more difficult to implement. Another option would be to further restrict the rules for when the action is offered, such that not only can the type not be in use, but also the parameter could not be in use within the body of any of the type's constructors' fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current approach is not ideal since we traverse both branches even though only one will have a matching ID, and we don't fail if the ID isn't found anywhere
Do you think it is worth fixing this in this PR? I think it would be somewhat easy to do, but perhaps not worth it if you expect to revisit Loc
soon.
Three ways spring to mind, all roughly implementing a modifyKindNode :: ID -> Kind -> (Kind -> Kind) -> Either Error Kind
(or some variety of focusKind
) function (where error could be "id not found" or "multiple id"):
- just do it in the "findNode" style -- we may already have the ability to use a generic uniplate zipper on things which
HasID
to find a particular node - since kinds are simple, we could use some uniplate magic: something like
contexts
to get a focus on each node, thenfilter
this by ID, then do the transform - some small applicative-state hack on this current code where we work not in the current monad
m
, but in something likeStateT Int m
(or maybeAccumT
orWriterT
may be better) where every time themodifyKind
actually runs the function, it also emits a+1
. We then check that we counted to exactly1
(i.e. did exactly one edit). (It may be nicer to work in the "0-1-many" monoid, rather than the integers, as that more clearly expresses what we care about).
We also end up performing a full typechecking pass
This seems fine to me (at least, it is consistent with other actions)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hadn't considered using uniplate. That seems like a good idea. But I'd rather wait until #1098 anyway. I'm sure we'll get to that very soon.
else case k of | ||
KHole _ -> Nothing | ||
KType _ -> Nothing | ||
KFun _ k1 k2 -> findKind i k1 <|> findKind i k2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another thing which can be simplified once we have zippers etc. for kinds.
let allKindIDs = \case | ||
KHole m -> [getID m] | ||
KType m -> [getID m] | ||
KFun m k1 k2 -> [getID m] <> allKindIDs k1 <> allKindIDs k2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another thing which can be simplified once we have zippers etc. for kinds.
@@ -70,7 +72,7 @@ data NodeType = BodyNode | SigNode | |||
-- | Describes which element of a (type or term) definition the student has selected. | |||
-- We have the following invariant: when this contains a `NodeSelection` with @nodeType = SigNode@, | |||
-- or any `TypeDefConsFieldSelection`, then they will always have @meta = Right _@. | |||
type Selection = Selection' (Either ExprMeta TypeMeta) | |||
type Selection = Selection' (Either ExprMeta (Either TypeMeta KindMeta)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This nested Either
is maybe a bit ugly, but it's still in some ways simpler than defining a new type, whilst still being fairly self-explanatory.
Thanks, this was obviously a lot of work. For posterity, can you provide here in the comments an example of a program/typedef that this PR allows us to create using the frontend API that we could not previously? (e.g., we can write |
| TForall a TyVarName Kind (Type' a) | ||
| TForall a TyVarName (Kind' ()) (Type' a) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For clarity: we intentionally don't put metadata on the kind here as is out of scope for this PR. See FR #1098. Here we are only interested in type parameters, and not foralls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since I already need to rebase, I'll add this to the commit message when doing so.
primer/src/Primer/Module.hs
Outdated
primitiveModule :: MonadFresh ID m => m Module | ||
primitiveModule = do | ||
allPrimTypeDefs' <- traverse (traverseOf (#primTypeDefParameters % traversed % _2) generateKindIDs) allPrimTypeDefs | ||
-- allPrimTypeDefs' <- traverse _ allPrimTypeDefs | ||
-- boolDef' <- traverseOf _typedefFields generateTypeIDs $ TypeDefAST boolDef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean to commit these comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, thanks, will remove.
primer/src/Primer/Action.hs
Outdated
ConstructKType -> const $ throwError $ CustomFailure ConstructKType "type edits currently only allowed in typedefs" | ||
ConstructKFun -> const $ throwError $ CustomFailure ConstructKFun "type edits currently only allowed in typedefs" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these errors read "kind edits"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, will edit.
"DeleteTypeParam" | ||
"DeleteTypeParam", | ||
"MakeKType", | ||
"MakeKFun", | ||
"DeleteKind" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commit should maybe be a breaking change? We may emit new stuff from the api
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left a few minor things above, but this generally looks good to me. Thanks!
We take advantage of this to output proper universally-unique IDs in `API.viewTreeKind`. We still use `Kind' ()` in the definition of `TForall`, since adding the ability to edit such kinds is a complicated change which is out of scope for now. We're initially focused on kinds in typedefs. NB. Even though we are considering getting rid of IDs entirely, we'd almost certainly still want some sort of metadata on nodes, even just for caching, and thus we'd benefit from the significant majority of the changes in this commit, since it's mostly concerned with giving the option to attach metadata to kind nodes. In other words, the exact nature of the metadata on kinds is irrelevant to most of the lines which are changed here. Signed-off-by: George Thomas <[email protected]>
This is analogous to the existing synonym for types, and we use it in roughly the same places. Signed-off-by: George Thomas <[email protected]>
Signed-off-by: George Thomas <[email protected]>
NB. We never actually construct a `Right (Right _)` case yet, but we will after the next commit. Signed-off-by: George Thomas <[email protected]>
Signed-off-by: George Thomas <[email protected]>
5bbf4d0
to
161fc4e
Compare
The implementation of this is rather ad-hoc (arguably hacky), because some of the machinery used for other actions, in particular `Primer.Zipper`, only supports expressions and types, but not kinds. This is something which should be revisited imminently. Signed-off-by: George Thomas <[email protected]>
Signed-off-by: George Thomas <[email protected]>
161fc4e
to
f8b1ea1
Compare
We can now finally construct any typedef supported by the backend.
There are a number of things which I intend to punt to #1098. This PR is already big enough.