Skip to content
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

Merged
merged 7 commits into from
Aug 8, 2023

Conversation

georgefst
Copy link
Contributor

@georgefst georgefst commented Jul 13, 2023

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.

@georgefst georgefst force-pushed the georgefst/typedef-param-kinds-api branch 4 times, most recently from 78e5e4d to 161af8a Compare July 20, 2023 13:04
@georgefst georgefst force-pushed the georgefst/typedef-param-kinds-api branch from 6151ff8 to 896d9c8 Compare July 24, 2023 12:55
@georgefst georgefst changed the title Provide API for viewing and editing kinds Provide APIs for viewing and editing kinds of type parameters Jul 24, 2023
@georgefst georgefst force-pushed the georgefst/typedef-param-kinds-api branch 4 times, most recently from 4798b9b to 5bbf4d0 Compare July 26, 2023 18:45
@@ -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 ()]
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

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 to TEmptyHole? And would it be reasonable to just say that that has the kind KType 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.)

Copy link
Contributor Author

@georgefst georgefst Aug 1, 2023

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.

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)
Copy link
Contributor Author

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"?

Copy link
Contributor

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".

Copy link
Contributor Author

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?

Copy link
Contributor Author

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).

Copy link
Member

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?

Copy link
Contributor Author

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)

Copy link
Member

@dhess dhess Jul 31, 2023

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
Copy link
Contributor Author

@georgefst georgefst Jul 26, 2023

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.

Copy link
Contributor

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, then filter 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 like StateT Int m (or maybe AccumT or WriterT may be better) where every time the modifyKind actually runs the function, it also emits a +1. We then check that we counted to exactly 1 (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)

Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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))
Copy link
Contributor Author

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.

@dhess
Copy link
Member

dhess commented Jul 27, 2023

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 Fix now, yes?) And can you also provide an example of a program that we still cannot write and will need #1098 for?

@georgefst
Copy link
Contributor Author

Sure. We can now construct types like Fix or MaybeT but we're limited in being able to use them since all foralls implicitly quantify over variables with the kind "type". For example, we can't remove the mismatch here:

image

Comment on lines -47 to +50
| TForall a TyVarName Kind (Type' a)
| TForall a TyVarName (Kind' ()) (Type' a)
Copy link
Contributor

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

Copy link
Contributor Author

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.

Comment on lines 133 to 137
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
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, thanks, will remove.

Comment on lines 474 to 475
ConstructKType -> const $ throwError $ CustomFailure ConstructKType "type edits currently only allowed in typedefs"
ConstructKFun -> const $ throwError $ CustomFailure ConstructKFun "type edits currently only allowed in typedefs"
Copy link
Contributor

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"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will edit.

Comment on lines -321 to +324
"DeleteTypeParam"
"DeleteTypeParam",
"MakeKType",
"MakeKFun",
"DeleteKind"
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right.

Copy link
Contributor

@brprice brprice left a 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]>
NB. We never actually construct a `Right (Right _)` case yet, but we will after the next commit.

Signed-off-by: George Thomas <[email protected]>
@georgefst georgefst force-pushed the georgefst/typedef-param-kinds-api branch from 5bbf4d0 to 161fc4e Compare August 7, 2023 21:53
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]>
@georgefst georgefst force-pushed the georgefst/typedef-param-kinds-api branch from 161fc4e to f8b1ea1 Compare August 7, 2023 22:21
@georgefst georgefst added this pull request to the merge queue Aug 8, 2023
Merged via the queue into main with commit e603e61 Aug 8, 2023
2 checks passed
@georgefst georgefst deleted the georgefst/typedef-param-kinds-api branch August 8, 2023 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants