| CARVIEW |
Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 275
First-class existential types #473
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
base: master
Are you sure you want to change the base?
Conversation
|
I'm really looking forward to it, thank you. Just this week I wrote some code where I believe I could have made use of something like If we, at some point, have
(I'm not suggesting that this works for |
|
Sounds great! I have a question: How does this compare with the "traditional" encoding of sigma types via pi types, that is very often found in e.g. Agda and Idris: record Sigma (A : Set) (P : A -> Set) : Set where
constructor _,_
field
fst : A
snd : P fstFrom a quick skim, it seems like the main benefits of making these "first class" are
Is there anything else I'm missing? |
|
I don't like the EDIT: In fact, In Core, GHC already makes Constraint, Type pairs. (In weird cases of |
|
Just a few quick responses (I will return to @int-index's points later): @JakobBruenker Yes, very good point. The new keyword (I propose: @googleson78 I agree with your two bullet points, both of which are significant (in my opinion) -- and undersold in the proposal, as @int-index rightly points out. The "first class" nature also gives us custom syntax. @phadej I'm open to a new name instead of |
|
@goldfirere alternative keyword suggestion for I agree with not doing much about that in this proposal, though. |
proposals/0000-existentials.rst
Outdated
| The ``TransformEC`` form allows us to create coercions witnessing the equality of | ||
| any two expressions. The idea is that we use this when we need to prove that an | ||
| optimization GHC performs is sound. We put the name of the optimization in the | ||
| carried string. Perhaps in the future, this will become more principled. |
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 still limiting what GHC can do because for example if we have a transformation that removes an unused argument in a function call (e.g. due to absent usage annotation) and then would float out an expression out of a lambda binding that parameter, then now it still needs to record the old expression in the coercion, so the parameter stays used and floating is limited.
Not a show stopper, just saying that I expect existential to sometimes get in the way of optimizations even with this very general coercion.
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, I see your point here. I have added a Drawback about this. I think it can be worked around (as I write in the proposal).
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 workaround is interesting, thanks!
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.
Hmm, given the workaround, do we really need this very unsafe coercion? Or could we say that any core2core transformation either needs to be consistent (i.e. translate the same expression the same way in all contexts, including in the types, and this way the typing is preserved), or it need to let-bind these expressions first?
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 work-around is this, right:
Because of the ability to substitute in the types of a let expression, the appearance of a locally bound variable in a TransformEC need not stop floating. Just float out and replace the variable with its right-hand side.
For recursive lets that’s possible but tedious – you’d have to substitute something like let x = … in x.
But what about floating out of lambdas? There is nothing to substitute there?
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 seem in the general case floating things out will result in parameters that must partially coincide?
foo :: forall x. foreach (a :: _) (b :: _). (a ~ (...x...), b ~ (...x...)) => ...
Wouldn't take a fancy combination of floating and common subexpression elimination avoid this?
|
It shouldn't have On a different note, I want to be able to do "open"/witness -like projecting fields from GADTs too, and also "nominal existentials" directly without coercing to a newly underlying existential type first. If the witnesses are forced to leak the contents in the latter case, we can't use such projection with abstract newtypes, and that sounds bad. Finally, while this is very exciting and would be great to have, I'm a bit concerned we're starting a big core language and type theory project when we already have one in flight — Dependent Haskell. If this delays that further, I rather just wait and do it after. |
|
To fill out the quantifier matrix further - we have erasable @Ericson2314 funnily enough f :: forall a . Show a => Stringas "given some type |
|
@JakobBruenker Yeah I thought it about some more and I agree |
|
Another potential issue I noticed is that it seems trying to recursively Assuming expr : exists (a :: k) (b :: k1). inner_tywe have Open expr : inner_ty[Witness @k (exists (a :: k) (b :: k1). inner_ty) expr / a]Open (Open expr) : inner_ty[Witness @k (exists (a :: k) (b :: k1). inner_ty) expr / a]
[Witness @k1 (exists (b :: k1). inner_ty[Witness @k (exists (a :: k) (b :: k1). inner_ty) expr / a]) expr / b]Perhaps we better get type-level Open (Open expr) : let
a_witness = Witness @k (exists (a :: k) (b :: k1). inner_ty) expr
a_elim = inner_ty[a_witness/ a]
b_witness = Witness @k1 (b :: k1) (c :: k2). a_elim) expr
b_elim = a_elim[b_witness / b]
in b_elimand further with a 3 variable existential: expr : exists (a :: k) (b :: k1) (c :: k2). inner_tyOpen (Open expr) : let
a_witness = Witness @k (exists (a :: k) (b :: k1) (c :: k2). inner_ty) expr
a_elim = inner_ty[a_witness / a]
b_witness = Witness @k1 (b :: k1) (c :: k2) (c :: k2). a_elim) expr
b_elim = a_elim[b_witness / b]
c_witness = Witness @k2 (c :: k2). b_elim) expr
c_elim = b_elim[c_witness / c]
in c_elim |
|
Relatedly, we've long had a principle in Haskell that if you can do it, you can abstract over it. (This is different than, say, Rust, where there is sorts of borrow-checker magic within functions that cannot be expressed formally at function boundaries.) However, with this feature we run into these issues:
This makes me further suspect we should wait on implementing this proposal until these "dependencies" are implemented. I both think that principle of abstracting over anything is very important to instill in beginners about what Haskell is all about, and it is also simply darn useful for more advanced users using extensions in anger, where it's very frustrating to fight the type checker for an hour and then end finding the would-be solution is something that cannot be expressed. |
|
Good point about datatype-based existentials. I've added a comment in the proposal (an "interaction"). Also good point about dependent types. I've added a comment in the proposal (an "unresolved question"). About big substitutions: What's the type of About lack of ability to abstract: Could you illustrate with examples? I don't catch what you're describing here. I think a visible existential is really just a user-written sigma type. Maybe a visible existential has some magical type syntax? Because it wouldn't impact type inference, etc., I'd like to leave this as a future extension. |
I edited my comment to provide. I think the source of confusion was I had a different
The problem just seems to be in the types, and I am not exactly sure when in Core the type is mandatory so I am not sure how bad the problem is. Still, if there is indeed a big blow up in the Core wrt the user-written original, we would need to introduce type-level
Sure: etaOpen
:: forall (rep :: RuntimeRep)
. forall k
. forall (f :: k %U -> TYPE rep)
. foreach (expr :: exists (a :: k). f a)
-> f (Witness @k (exists (a :: k). f a) expr)
etaOpen expr = exprI think is the "principle" eta-expansion of existential opening, but we are very far away from it being legal Haskell. |
Darn it! Are you saying this proposal is apart from Dependent Haskell? Because I was excited to see something that I thought used dependent types, and I could see a use for, and I couldn't see how to do any other way (I tried). Getting a type-safe
isn't the index type
(Or does this not deliver a safe |
|
@Ericson2314 You say your @AntC2 This proposal is definitely in sympathy with dependent types, in that a language with both dependent types and existentials is more than the sum of its parts. But this proposal is distinct from dependent types. In particular, the features in this proposal are new -- they are not already in Idris or Agda, for example. So we could have dependent types, or first-class existentials, or both. I agree with the definition for dependent types you quoted from Wikipedia. But the type for filter I'm proposing is As for |
|
I also don't like the I do think it should be an operator and not a name, since Haskellers are used to using operators when dealing with constraints. To match up with |
|
@goldfirere oops. That was just a cut and paste error, the open paren was also not balanced! I deleted the |
|
@AntC2's "darn it" is useful to me. To me, the idea that this feature can be properly done before dependent types is a bit of a siren's call. I understand the appeal: get something done which seems less fancy and broadly popular across skill levels, and builds a bridge with Liquid Haskell. But it will require what to me feels like a very nearly ad-hoc subset of Dependent Types to make opening work, and to provide expression coercions. In line with what @nomeata said, it feels like we are opening a can of worms trying to rush that stuff in to do this, and that is in addition to my point above about sacrificing the ability to eta-expand. As far as the research goes, it is great that we can have papers and proposals for each occurring independently --- in particular I am not saying the formalization in the paper has gaps or any slander like that. But for development the order for implementation to me is very important, and we must constantly fight the urge to rush to popular new features without first doing the behind the scenes infrastructure/ tech debt work to have a good. (I am not even sure Dependent Haskell is even less exciting in vaccum, but rather this is the new kid on the block, and DH isn't, and that has an effect.) Bottom line is, it is very clear to me how we can do DH without this, and add this later, and is very sketchy to me that we can do this without much of DH, without running the risk of opening and |
|
@Ericson2314 Getting closer, but still not quite there. Yet I think I can now fill it out myself: Assuming we have That's not really so bad, but I do see how a nested dependent existential might grow. I think this can be fixed. I will take another pass through the proposal, as I think that second argument of |
|
OK thanks for working through that and simplifying. Per what @nomeata and you were talking about, if |
Emm (apologies if this is a dumb question/I'll delete it) isn't it more general than that? We could unload the And in general we cannot load to a type-length-indexed |
|
@AntC2 I think it's a good question, and the answer is somewhat counterintuitive at first. This is exactly what we need existential types for, but we can already encode existential types in Haskell. However, it does get a bit unwieldy (as well as having some other disadvantages) without the first-class existential types introduced in this proposal. For example, right now, we can write data SomeVec a = forall n . MkSomeVec (Vec n a)
filter :: (a -> Bool) -> Vec n a -> SomeVec a
filter p VNil = MkSomeVec VNil
filter p (x :> xs) = case filter p xs of
MkSomeVec v | p x -> MkSomeVec (x :> v)
| otherwise -> MkSomeVec v
fromList :: [a] -> SomeVec a
fromList [] = MkSomeVec VNil
fromList (x:xs) = case fromList xs of
MkSomeVec v -> MkSomeVec (x :> v)So if we can produce a list from IO, we can also produce a But with this proposal, we could instead write filter :: (a -> Bool) -> Vec n a -> exists m. Vec m a
filter p VNil = VNil
filter p (x :> xs)
| p x = x :> filter p xs
| otherwise = filter p xs
fromList :: [a] -> exists n. Vec n a
fromList [] = VNil
fromList (x:xs) = x :> fromList xs |
|
Thanks @JakobBruenker, you're right about "counterintuitive".
So the guard is applying to something outside the scrutinee of the |
The one with the proposed |
|
My 2 lovelace:
|
|
@VitWW I'm looking forward to your ICFP paper :P More seriously, I don't think that pseudo-type approach generalizes to other situations where existential types are used. For example if we want to convert a list into a length-indexed vector: |
|
@noughtmare Good point. But yes, I agree, this also requires some additional calculations |
|
#473 (comment) data Box = forall a . Box a
unbox (Box a) = a
-- unbox :: Box -> exists a. aWith dependent types you wouldn't need existentials for branching either. You can do this with type families and singletons in Haskell. If you don't want to use singletons, then you would start bringing in existentials (losing the information that the existential is simply the parameter). If anything, the existentials are here because you don't have dependent types and aren't using singletons, so you are forced to combine two different types by losing information where they differ. I think (though am not certain) having the existential before the bool is unsound in your first example. let f = fn @Int @Char :: exists c . Bool -> Int -> Char -> cI could at this point unpack f to get c - the fixed, specific return type - and then apply the function with both False and True, which should give the same type (but really they won't - in one case c = Int in the other c = Char). I think what you are imagining is a type nearly isomorphic to the existential that does not make use of existentials. |
|
My update @Jashweii @noughtmare :
data exists a. Box = forall a. Box a
unbox :: exists a. Box -> a
unbox (Box a) = a
box :: a -> exists a. Box
box a = Box a
-- more flexible working with existential types
doublebox :: exists a. Box -> (exists a. Box, exists a. Box)
doublebox a = (a, a)
constbox :: exists a. Box -> exists b. Box -> exists a. Box
constbox = const
-- append :: [exists a. Box] -> exists a. Box -> [exists a. Box]
append :: [exists a. Box] -> exists b. Box -> [exists c. Box]
append list b = b : list
-- F(default, auto) c = Id c
-- c ~ a | b
fn :: forall a b. Bool -> a -> b -> exists c. F c
fn c t f = if c then t else f
-- class (Monad m) => MonadState s m | m -> s where ...
class (Monad m) => MonadState (exists s. s) m where ...
-- class Foo a b c | a b -> c where ...
class Foo a b (exists c. c) where ... |
|
how does this relate to rank N types and current existentially quantified types? are the representation equal or would we need to explicitly or implicitly convert between them? |
|
@eldritch-cookie My posts were unrelated to current existentially quantified types. Which are de facto So I've just create an alternative proposal #642 |
|
@VitWW my question was about the proposal in general not your comment but i see how it could be misunderstood. |
|
I see ))) |
|
tomjaguarpaw commented on Sep 8, 2023:
Exactly how I felt when reading the proposal. Note, I am far from understanding most of the technical details here. My position is that of a user of existentially quantified types as they are available now i.e. with GADTs; and suffering from the limitations that this proposal seems to be addressing, including but not limited to https://gitlab.haskell.org/ghc/ghc/-/issues/17130. I find it sad that this proposal seems to have been abandoned. |
Me too. I'm currently heavily using singleton style and returning existentials from functions is a very common pattern. It's sad to have to either CPS or wrap everything in |
The very purpose of this proposal is to repair the discrepancy that, in the following table, four of the cells are first-class while two of them have to be expressed as encodings.
Adding |
|
That's interesting, because that's not my understanding of the intention of the proposal. I would say that the intention, as with most of the proposals I've read recently, is to create something of use to Haskellers. Indeed the motivation section does not mention anything like your table. Furthermore, as far as I can tell, half baking this cake would be useful to Haskellers. (It's possible that I've missed something, of course. Perhaps abstract consistency is more important than practical utility, or perhaps half baking the cake wouldn't be practically useful, for reasons not yet clear to me.) |
|
You are creating a false dichotomy between "abstract consistency" and "practical utility". Abstract concepts are useful in practice, and Haskell has historically been one of the main languages to embrace this viewpoint. Anyway, this discussion is pointless. As the proposal says:
|
|
I'm not trying to create a dichotomy. I agree that many (not all) abstract concepts are useful in practice, and I would say that Haskell is the language that benefits most from this viewpoint. That's a major reason why I love Haskell. What I am trying to suggest is that if this proposal naturally splits into two then it would be useful to make the split, because the first part provides incremental practical benefit even if it is lacking in some other regard. I'm grateful that you pointed out to me that the proposal already touches on my suggestion. I didn't notice that! However, I disagree that further discussion is pointless. I think the proposal is mistaken in that regard. I would really, really, really love Ideally I would love both, but if adding just |
My understanding is that the blockers have more to do with existentials than with implicit constraint packing (the former, as currently formulating, requiring significant changes to Core). That being said, my reading is that the design of |
|
Great to know, thanks @aspiwack! So it looks like my suggestion is moot, at least for the time being. |
|
For what it's worth, I still have quite a bit of love for this proposal / direction of travel. What I don't have is time to really push this through. If someone else wants to become a champion here, I would happily support that person with e.g. weekly meetings, code review, etc. |
Let me add that the proposal involves a very significant change to Core, with lots of follow-on consequences. I'd not just a matter of implementation. We have not changed Core in such a significant way for over two decades. Implementing it would give a clearer sense of what the costs would be. But it'd take quite a bit of effort, and then we might end up rejecting the proposal anyway. Yet, deciding whether to accept the proposal depends quite a bit on how hard it is to adjust Core. I would love to have first class existentials too, but the price is fairly high, and I'm genuinely not sure if the pain justifies the gain. Not against, but rather cautious. |
|
I just want to say that I agree with Simon here -- it's one of the reasons I haven't really pushed this forward. I think it would be great to have existentials. But it's a lot of work! And I agree that it's not clear the gain will be worth the implementation and specification effort. I don't want to dissuade folks from volunteering here. But instead I would want people to come in eyes wide open about the potential challenges here. |
|
Alternative algorithm of existential quantifier (without |
|
I was thinking about this recently and user datatypes, and got to re-thinking about @VitWW's idea, which I think I now get (but I might be wrong). I am curious if it is a real alternative to this, or if it plays well with type inference. Essentially instead of a witness type, you would infer refinement types of existential data types (for a variant you would need to know which constructor as well) that lift the existential type variable(s) to additional parameters of the refined type, thus also acting as In Haskell today Edit: data Exists' f a = MkExists' (f a)
-- Exists f ^a = Exists' f a
-- Exists f = exists a . Exists' f aWhich incidentally is what I imagine people will do more often were first class existentials (strong or weak) supported. |
|
The main thing that has been dragging this proposal is the needs for changes to core. I've been spending a bit of time thinking about Andreas Rossberg's 1ML line of work. My understanding is that one component of 1ML is that you can elaborate of a language with So it seems to me that we can probably get away with a minimal modification to Core (as exists with unpack is basically a magic GADT). Could it help unblock this PR? |
|
I don't think the insights from 1ML really apply here. 1ML contains a phase separation, where all module expressions can be resolved to values at compile time. This indeed avoids the troublesome need to inject terms into types, but it is not expressive enough to handle the cases, like |
I don't think that's quite right. 1ML's modules are ordinary values. They can be function arguments, and they can be computed dynamically. The differences with ordinary records is that: 1ML modules can contain types, and 1ML module types' fields can depend on a type defined by another field. The (critical!) phase distinction story is that, despite appearances static types cannot actually depend on dynamic values (you can have dynamic type values, but for a type to be able to branch on a such a value, the value must be known statically). This is the secret sauce. And I think it applies to your existentials. Probably. I hope? Maybe there's a subtlety in your |
|
The length in the |
|
I don't think it's too dynamic for 1ML, at least. Let's try. We give ourselves (I'm modelling the integers at sort type o
type s a
val vec : {
type t n a;
nil : t o a;
cons : a -> t n a -> t (s n) a;
}Now, I think the following will typecheck in 1ML: let list = {
type t a = {
type ln; (* a nat really *)
b : vec.t ln a
};
nil : t a = {
type ln = o;
b = vec.nil;
};
cons : a -> t a -> t a = fun x xs -> {
type ln = s (xs.ln);
b = vec.cons x xs.b
};
}And from there, building |
|
That looks reasonable... but I also think that the mini slice of dependent types as needed here is pretty fundamental (sadly). (Exception: we might plausibly be able to store just variables in types, not whole expressions.) Something has to give in 1ML -- maybe we can't achieve laziness the right way? I'm afraid I don't have the time to do an exploration of this. |
|
I don't claim to understand the magic in 1ML's elaboration. But somehow, this is translated to just Fω + unpacking-existentials. The catch seems to be that translating types to such a Core requires peppering them generously with existential quantifications. Which may not be such a good trade-off in that it may go against the grain of GHC's implementation. But I'm honestly not sure either way. |
|
@aspiwack @goldfirere I working right now on try to add Existentials into system F and Core language with explicit types at Discourse: |
Following on from the recent ICFP paper, I propose first-class existentials for GHC.
Rendered