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

Unsaturated type families #242

Open
wants to merge 6 commits into
base: master
from

Conversation

@kcsongor
Copy link

kcsongor commented Jun 16, 2019

This is a proposal to allow partial application of type families, as
described in the paper Higher-Order Type-level Programming in Haskell

Rendered

@goldfirere

This comment has been minimized.

Copy link
Contributor

goldfirere commented Jun 16, 2019

Hooray! I've been hoping to see this proposal for some time. Unsurprisingly, I think this is a great leap forward. However, I have some quibbles with the details:

  1. As highlighted in Section 3.1, there is an inconsistency around the naming of the arrows. A comment there says (quite rightly) that types of terms will be unmatchable (because we have no matchable functions in terms other than data constructors) and that "the arrow is rendered as -> in terms". I think you mean "in the types of terms" there... but how will that be implemented? It's unclear to me that GHC knows the difference between types of terms and types of types in a way that means it will print arrows consistently.

    Furthermore, I worry with this naming scheme that any future move toward dependent types -- and using ordinary term-level functions in types -- with be hamstrung.

    So might I humbly suggest that we swap the names? Then, -> will be unmatchable and ~> will be matchable. Sadly, this will be a breaking change (but could be easily guarded by the extension), changing the meaning of, e.g., forall (m :: Type -> Type). .... Yet the vast vast majority of arrows in Haskell code are in types of terms, which will continue to be spelled correctly.

  2. It is worth considering a different spelling of the new arrow. Many fonts render ~> too similarly to ->.

  3. The proposal uses ->{m} notation but does not explain it.

  4. Formatting trouble under "Inference".

  5. Why have "Arity of type families"? I would think that type family A :: Type ~> Type and type family B (x :: Type) :: Type would be fully identical. Is there a reason they can't be?

  6. Though I'm responsible for using the word "matchable" in this way, I wonder if we shouldn't rethink. Here's a problem: Suppose I write type family F :: Type -> Type where F Int = Bool. That is rightly rejected. I ask "Why?" You say "You can't match on the argument to F because the argument to F is matchable. You can match only on unmatchable arguments." I say "😫".

  7. The proposal considers interaction with #111: Linear Types: "Happily, multiplicities are only used in types, whereas matchabilities only appear in kinds. As a result, these features are completely orthogonal." However, types and kinds are the same in GHC. How can we tell these apart? I don't think sorting this out will be this easy.

  8. I'm in full support of matchability polymorphism over the subtyping approach in my thesis.

  9. "type inference with the matchability defaulting scheme is incomplete": Very helpful example! I'm not sure you mean "dependency order" there, but I agree that this problem exists, may be soluble, but is likely not worth solving.

  10. We don't need to steal syntax for this (I think): the new operator can always just be imported.

Please don't take these comments as a lack of support for this -- I think it's great. I just want to refine the edges a bit. Thanks for posting.

@RyanGlScott

This comment has been minimized.

Copy link
Contributor

RyanGlScott commented Jun 17, 2019

I've also been eagerly awaiting this proposal. I apologize in advance for dumping another wall of questions :)

  • I don't have a particular favorite choice of syntax for unmatchable/matchable arrows, but I agree with @goldfirere's point (10) and wonder if we should avoid baking new syntax directly into the parser and instead require users to import the new arrow themselves (perhaps from Data.Arrow).

    This would pose some complications in the implementation, since the fixity of -> is essentially negative infinity due to the fact that it is hard-coded in the parser, and as a consequence, it is impossible to give user-defined type operators the same precedence as ->. This results is oddities like the one observed in this GHC issue, where a ~> b -> c associates as (a ~> b) -> c, not as a ~> (b -> c) like you would intuitively expect.

  • The special ->{m} notation makes me a little nervous, especially since I think that the Linear Haskell prototype is using something similar. (Or perhaps I'm not remembering that correctly).

    Either way, I'm not terribly fond of blessing matchability as the one property of arrows that gets special bracket syntax, as there are other type parameters in (->) that could also be specified (e.g., the RuntimeRep arguments). I think it would be worthwhile to figure out a syntax that lets us specify all of these properties (or a subset of these properties), especially since arrows are only going to be getting even more type parameters in a Linear Haskell future.

  • Speaking of which, what is the full definition of (->) (the matchability polymorphic one) under this proposal? Should we consider having some primitive ARROW type constructor (à la TYPE) now that it has three arguments (the matchability argument and two RuntimeRep arguments)?

  • As far as I can tell, this only proposes to add syntax for A ~> B (i.e., unmatchable visible, non-dependent quantification), and not any of the following (terminology taken from here):

    • forall a ~> B (unmatchable visible, dependent quantification)
    • forall a '. B (unmatchable invisible, dependent quantification)
    • C '=> B (unmatchable invisible, non-dependent quantification)

    Is this intended? I worry that without those, we will run into limitations such as this one.

  • As I observed here, there are potential backwards-compatibility issues with respect to Haskell constructs where the distinction between types and kinds isn't clear. For example, in the following program:

    data T = MkT (Type -> Type)
    type S = MkT Maybe

    If one compiles this program with UnsaturatedTypeFamilies enabled, then according to the specification in this proposal, this will fail to typecheck. This is because Section 3.1 dictates that term-level arrows are assigned to be unmatchable, so MkT (Type -> Type) (a term-level data constructor) is really sugar for MkT (Type ~> Type). However, in the type synonym S, it's not clear whether the type synonym will be used for a type or a kind (as type synonyms can denote both), so the kind of Maybe in type S = MkT Maybe is Type -> Type, not Type ~> Type. Yikes!

    You proposed a more nuanced matchability inference story here which would solve this problem. Should this be mentioned in the proposal?

  • What is the specification regarding definitional equality between partially applied type families? For instance, consider the following two type family definitions:

    type family Id1 x where Id1 x = x
    type family Id2 x where Id2 x = x

    Is Id1 equal to Id2? Can you write type class instances on top of Id1 and Id2? If so, will the instance for Id1 overlap with the instance for Id2?

Fix link to pull request
@Icelandjack

This comment has been minimized.

Copy link
Contributor

Icelandjack commented Jun 17, 2019

I have no technical comments but I take (~>) to mean a polymorphic function, this could lead to confusion. The paper uses the barbed arrow , (->>) would make a good choice

@AntC2

This comment has been minimized.

Copy link
Contributor

AntC2 commented Jun 18, 2019

@goldfirere Though I'm responsible for using the word "matchable" in this way, I wonder if we shouldn't rethink. ... "You can't match on the argument to F because the argument to F is matchable. You can match only on unmatchable arguments." I say "tired_face".

Seconded. I'm afraid "(un)matchable" means nothing to me. (Then I have no intuition which flavour of arrow I should be using. Or what the flavour of arrow means if I see it in a signature.) And there's no crisp definition in the proposal -- which there should be, even if there's already definitions in the papers. If we had a definition, perhaps it'd be easier to find a more specific word. The paper section 2.2 says "both injective and generative".

Haskell already uses the word in the sense of 'pattern matching'. And at least one compiler gives you pattern match failure exceptions. So I agree with @goldfirere's intuition that "unmatchable" should give a rejection. But presumably "matchable" here has nothing to do with patterns(?)

The key concept for writing code is saturating the arguments/partial application. For a type family, you must supply the first n arguments to obtain a injective and generative type; for a datatype constructor n is zero.

Seeing as it's in the title of the proposal: "(un)saturated"; or invent a word "(un)injenerative".

Many fonts render ~> too similarly to ->.

Seconded. I often find myself squinting to see whether I'm looking at a tilde or a dash. I am unconvinced we need more than one arrow/decorating the arrow seems to me putting the abstraction in the wrong place, see below.

[proposal] Then equalities of the shape f a ~ g b are only solved by decomposition when f :: k -> j and g :: k -> j.

Equalities using tilde are only solved when the arrow is non-tilde. Seems backwards to me.

[proposal] The proposed change is to distinguish between type constructors and type functions in the kind system.

I don't want to nit-pick over wording, but I often see claims that type functions are type constructors. (For example in the discussion about Row Polymorphism #180 ). It is still confusing the heck out of me. But here you want to distinguish type constructors that are not type functions. Can I call those 'datatype constructors', or is that another blue?

Being a datatype constructor vs a type function is a property of the whole name (which might take several arguments). Then it doesn't seem right to make it a property that falls in between successive arguments (as with differently-flavoured arrows). Could there be something of either of these signatures

k1 ~> k2 -> k3 ~> k4
k1 -> k2 ~> k3 -> k4

If not, why not; and why (apparently) does the proposal allow me to write signatures like that?

(Whichever way round we choose to decorate the arrows.) That seems to me not to show the idea that the first n arguments must be saturated. I'd rather stick with just the one familiar arrow and put some kind of marker at the point we reach saturation:

k1 -> k2 -> k3 | -> k4   -- must supply 3 args to be saturated -- a typical type family
k1 | -> k2 -> k3 -> k4   -- must supply 1 arg to be saturated -- return a higher-kind from a type family
k1 -> k2 -> k3 -> k4     -- no saturation marker, so assume at zero/is a datatype constructor

Or put the marker just after the ->, as you have for the {m} decoration. For a matchability polymorphic TF, use a different marker. (Using a decoration with what looks like a variable name is downright confusing. Could I have k1 ->{m1} k2 ->{m2} k3 -> k4? And do I now have type (kind) variables m1, m2 in scope?) Again I think the required abstraction is to mark n arguments up to a point in the signature.

type family Map (f :: a -> ? b) (xs :: [a]) :: [b] where
  Map _ '[]       = '[]
  Map f (x ': xs) = f x ': Map f xs

Argument f to type function Map can have from zero up to a maximum of 1 argument to obtain a saturated type. (? I've pulled out of thin air/is eminently bikesheddable. It's potentially a user-defined type operator, unlike |; but as such couldn't legitimately appear immediately after ->. I suppose ->? or ?-> (no space) could be a user-defined operator. But we're becoming more relaxed about space-significance #229.)

@AntC2

This comment has been minimized.

Copy link
Contributor

AntC2 commented Jun 18, 2019

While I'm on the topic, do we actually need all that business about matchability polymorphism? Couldn't we just say all type families are potentially matchable-polymorphic: in the sense the n arguments is a maximum/you can always supply a type argument whose saturation point is less than n? Then where that happens, type inference/injectivity/generativity can be more eager.)

IOW is there a use case where you'd want to accept Map Id but reject Map Maybe?

kcsongor added 2 commits Jun 18, 2019
Fix formatting issues
@kcsongor

This comment has been minimized.

Copy link
Author

kcsongor commented Jun 19, 2019

Thank you all for taking the time to read the proposal! Let me address some of the questions here, in order. I will defer discussing the syntax for now, but I'm in general agreement with your suggestions @goldfirere.

The proposal uses ->{m} notation but does not explain it.

I've fixed this omission.

Why have "Arity of type families"? I would think that type family A :: Type ~> Type and type family B (x :: Type) :: Type would be fully identical. Is there a reason they can't be?

The arity of a type family still very much matters. Firstly, the generated axioms still have a fixed arity, and they only reduce when fully saturated. In other words, Const Int is perfectly fine as an unsaturated type family application, but there is no corresponding axiom to reduce it.
Related to this, the equational theory doesn't include eta-rules, which means that

type family Id1 (a :: Type) :: Type where
  Id1 x = x

type family Id2 (a :: Type) :: Type where
  Id2 x = Id1 x

type family Id3 :: Type ~> Type where
  Id3 = Id1

here, Id1 ~ Id3 is derivable, but Id1 ~ Id2 is not.

I should add that it would be possible to compute the arity of a type family from the equations, and
not their signature (as is done for terms), which would mean that

type family Id2' :: Type ~> Type where
  Id2' x = Id1 x

could work too. The issue here is with open type families, whose arity would then need to be computed from the declarations which can be in different modules. Or maybe it wouldn't be an issue? The equations are already checked against each other to ensure they don't overlap, so perhaps this check would be relatively straightforward to implement.

The proposal considers interaction with #111: Linear Types: "Happily, multiplicities are only used in types, whereas matchabilities only appear in kinds. As a result, these features are completely orthogonal." However, types and kinds are the same in GHC. How can we tell these apart? I don't think sorting this out will be this easy.

What I meant is that all type arrows are unmatchable, and all kind arrows have omega multiplicity, so there aren't any interesting interactions to consider. The code paths are entirely separate.

"type inference with the matchability defaulting scheme is incomplete": Very helpful example! I'm not sure you mean "dependency order" there, but I agree that this problem exists, may be soluble, but is likely not worth solving.

Here's a sketch of the algorithm:
Consider the constraint a b ~ c Id, with all a, b, c having unsolved matchabilities m_a, m_b and m_c respectively. We can emit an implication constraint which observes that if a and c are matchable, then the equality can be decomposed:
(m_a ~ Matchable, m_c ~ Matchable) => b ~ Id. Every equality constraint between applications would give rise to such implication constraints. Then, we only default the matchabilities that do not appear on the right hand side of implications (here, m_b does, m_b ~ Unmatchable is derivable from the RHS). Then keep doing this until all matchabilities are either derived or defaulted.

@RyanGlScott:

As far as I can tell, this only proposes to add syntax for A ~> B (i.e., unmatchable visible, non-dependent quantification), and not any of the following (terminology taken from here):

Good point, I agree these should be included too.

What is the specification regarding definitional equality between partially applied type families?
Is Id1 equal to Id2? Can you write type class instances on top of Id1 and Id2? If so, will the instance for Id1 overlap with the instance for Id2?

See above. But to answer, they are not equal, though nor are they apart. Type class instances for type families is not part of this proposal. I would like to have that feature, probably as a separate proposal once this one is settled. The trouble is that for instance matching, we really want a decidable apartness relation (otherwise instance selection would be undecidable even between instances that don't have contexts). For this, we would ideally like to determine that Id1 and Id2 are apart. But doing that would not be forward compatible with extensions to the type system such as eta. The tension could be resolved by introducing a new role that has restricted computation rules compared to the existing nominal one, which would then be used in instance selection. Anyway, this is outside of the scope of the current proposal.

@AntC2:

Equalities using tilde are only solved when the arrow is non-tilde. Seems backwards to me.

This is a really good point. I have not considered the relation, and the potential for confusion here.

Could there be something of either of these signatures [...] If not, why not; and why (apparently) does the proposal allow me to write signatures like that?

In Haskell, every kind is inhabited, and you can write a type family with either of those kinds. Granted, this type family will have no instances, but I don't see that as a reason for disallowing the kind.

That seems to me not to show the idea that the first n arguments must be saturated. I'd rather stick with just the one familiar arrow and put some kind of marker at the point we reach saturation:

This is an interesting idea, however, I think it wouldn't be very compositional, as it would essentially mean that the whole kind is now one big unit, and a lot of additional bookkeping is required to keep things together. How would you abstract over such a thing?

Using a decoration with what looks like a variable name is downright confusing. Could I have k1 ->{m1} k2 ->{m2} k3 -> k4? And do I now have type (kind) variables m1, m2 in scope?

Indeed, you can! They are truly variables, and we get m1 and m2 in scope. That said, I'm happy to change the syntax if we find something that looks better.

While I'm on the topic, do we actually need all that business about matchability polymorphism?

I think we do. Having higher-order type functions and staying with first-order unification poses a number of challenges, and I believe that such a first-class solution is the most predictable, even if there is some mental overhead.


The outstanding question seems to be what to do with the arrow. Make -> the unmatchable one, and introduce a different arrow for matchables? What would be a good migration path for that?

@RyanGlScott

This comment has been minimized.

Copy link
Contributor

RyanGlScott commented Jun 19, 2019

Type class instances for type families is not part of this proposal.

Gotcha. It might be worth mentioning in the proposal that attempting to define a class instance (or type family instance) on top of an unsaturated type family should be an error, then.

I might seem fixated on this point, but that's only because the singletons library currently defines type class instances for defunctionalization symbols, so the inability to define instances for unsaturated type families might pose a problem if singletons abandons defunctionalization in favor of UnsaturatedTypeFamilies. Of course, the needs of singletons shouldn't drive the specification of this proposal, but given that your paper draws inspiration from singletons' notation, I couldn't help but feel a tad bit disappointed that UnsaturatedTypeFamilies couldn't be a drop-in replacement for defunctionalization in it current state :)

To be clear, though, I agree that this is a challenging problem that is best left for a different proposal.

@jberryman

This comment has been minimized.

Copy link

jberryman commented Jun 19, 2019

Is there a commonly used library that would make use of this proposal? If so would love to see an example of what Joe User would see in the docs, what from this proposal they'd need to understand to be productive, etc.

As a library writer I'd be happy to be subjected to the complexity in this proposal in order to express the API I want. As a user I don't want to learn about anything here (the ideal is I never have to consider the issue being addressed here, right?; the language just becomes simpler).

If the proposal means common libs go the way of undefined where no one is quite sure what the type means anymore that's a Drawback that should be discussed I think.

Thanks for your work on this!

@simonpj

This comment has been minimized.

Copy link
Contributor

simonpj commented Jun 19, 2019

I like the proposal in general.

Syntax is the hardest bit.!

  • In "Specification" the proposal badly needs clear statement of the syntactic changes, by way of a BNF delta. You say you've fixed the lack of explanation of ->{m} but I don't see such an explanation.

  • It's hard to avoid the connection with linearity polymorphism. Both decorate the arrow, in a possibly-polymorphic way. But, currently, I think linear arrows will appear only in the types of term-level functions, while matchabilty arrows will appear only in the kinds of type-level functions.

  • I think linearity polymorphism is moving in the direciton of t1 # lin -> t2 meaning an arrow from t1 to t2 decorated with linerity lin. It would be odd not to use a related syntax for both. You propose t1 ->{m} t2; but if we use that, we should arguably use it (or some related form) for linearity polymorphism too.

  • Linearity polymorphism has operators that connect linearity, so you can have say

    f :: forall l m. (Int # l -> Int) -> (Int # m -> Int) -> Int #(l && m) -> Int
    

    (I'm sure I have the details wrong.) Do you need any functions of type Matchability -> Matchabilty -> Matchability or are the annotations always a constant or a variable?

@simonpj

This comment has been minimized.

Copy link
Contributor

simonpj commented Jun 19, 2019

type inference with the matchability defaulting scheme is incomplete

That suggest that you have a "matchability defaulting scheme" in mind. What is it?

I think this issue arises only when kind-checking type signatures. So in your example we have

nested :: a b ~ c Id => b Bool

I think you are suggesting

  • Kind-check the signature
  • Simplify the constraints that arise
  • Default any unconstraint matchability variables to Matchable
  • Based on that, further simplify the constraints
  • And quantify over unconstrained kind variables

The worry is that defaulting them all at once might be wrong. Perhaps defaulting would fix another. Or vice versa.

I don't understand your suggestion about implication constraints I'm afraid.

@AntC2

This comment has been minimized.

Copy link
Contributor

AntC2 commented Jun 19, 2019

Could there be something of either of these signatures [...]

In Haskell, every kind is inhabited, and you can write a type family with either of those kinds. Granted, this type family will have no instances, ...

Thanks @kcsongor for all the replies. Let me see if I can work what this kind would mean. I need to put the parens into the signatures, I think:

k1 ~> (k2 -> (k3 ~> k4))          -- was k1 ~> k2 -> k3 ~> k4

takes first argument a type family; second argument a datatype constructor; third argument a type family(?)

k1 -> (k2 ~> (k3 -> k4))          -- was k1 -> k2 ~> k3 -> k4

takes first argument a datatype constructor; second argument a type family; third argument a datatype constructor(?) Does that mean that if we already have the first argument, type inference can take advantage of it being generative?

So the decoration on the arrow tells us about the argument to its left?

  1. I'm still feeling that putting the decoration on the arrow is the wrong place: it applies for the argument.
  2. ref @simonpj's point on the {m} decoration, the matchability also applies for the argument to the left, so that should appear left of the arrow -- which conveniently coincides with the position of the linearity decoration.

The outstanding question seems to be what to do with the arrow. Make -> the unmatchable one ... ?

I'm going to repeat: I see no need for different flavour of arrows. If we need to decorate the arrow anyway for matchability polymorphism (and I'll put that to the left)

k1 {U} -> k2 {M} -> k3 {U} -> k4   -- upper case so not a tyvar

Now to my eye that could be decorating the argument, just as much as decorating the arrow.

Yes that's verbose; but I take it the defaulting rules will mean you seldom actually write it in code. Readers of displayed matchability-marked signatures will see it.

Except I'm not happy with U, M because they stand for (Un)Matchable and that means nothing to me. (Un)Saturated -- as in 'the argument to the left can be Unsaturated/must be Saturated to make it injective&generative'?

The { } braces indicate a required explicit type (kind) argument if they surround a tyvar; and there I'm putting a type (which therefore doesn't get explicitly applied). Is that going to upset anybody's aesthetics? Then we could borrow from linear types and put #; or put the explicit type application in the signature

k1 # U -> k2 # S -> k3 # U -> k4 

k1 @U -> k2 @S -> k3 @U -> k4 
@AntC2

This comment has been minimized.

Copy link
Contributor

AntC2 commented Jun 19, 2019

While I'm on the topic, do we actually need all that business about matchability polymorphism?

I think we do. Having higher-order type functions and staying with first-order unification poses a number of challenges, and I believe that such a first-class solution is the most predictable, even if there is some mental overhead.

I should have been clearer: I'm not trying to interfere with the implementation under the hood. I'm wondering why we need to expose it so "tiresomely" to the programmer:

When supplying type arguments to matchability-polymorphic functions such as ...
the user needs to provide either a concrete matchability or a wildcard before supplying the instantiation for f, as in qux @_ @Id. This is tiresome, because m can always be inferred from the kind of f, ...

  1. OK, so just go ahead and infer it, and don't bother me with putting useless @_. And if that means the polymorphism decoration can't be { }, then all the better. Use # _ -- in coordination with the linearity stuff, of course.
  2. I see no use case for a "concrete matchability". Again: why would you want to accept Map Id but reject Map Maybe?

At term level we don't say: here you must supply an expression with a function, you're not allowed to put a data constructor.

@isovector

This comment has been minimized.

Copy link
Contributor

isovector commented Jun 19, 2019

@jberryman I'm particularly excited about this proposal as a library writer. It makes using the higher-kinded data pattern feasible. It would allow us to write this:

type family HKD f a where
  HKD Identity a = a
  HKD f a = f a

data Person (f :: Type ~> Type) = Person
  { pName :: f String
  , pAge :: f Int
  } deriving (Eq, Ord, Show)

The unmatchable arrow on f lets GHC derive the instances, which it currently cannot do. Instead we are resigned to one of the two following solutions today, both of which are terrible:

deriving instance Eq (Person Identity)
deriving instance Ord (Person Identity)
deriving instance Show (Person Identity)

deriving instance Eq (Person Maybe)
deriving instance Ord (Person Maybe)
deriving instance Show (Person Maybe)
-- and also for every other type at which you instantiate this

-- O(distinct types used) number of instances

or

deriving instance Eq (Person Identity)
deriving instance Ord (Person Identity)
deriving instance Show (Person Identity)

deriving instance (Eq (f String), Eq (f Int)) => Eq (Person f)
deriving instance (Ord (f String), Ord (f Int)) => Ord (Person f)
deriving instance (Show (f String), Show (f Int)) => Show (Person f)

-- O(number of fields) size of instance constraints

OK, but when would such a thing be useful, you might wonder? Both beam and ecstasy both aggressively use this pattern. Beam rejects the HKD-ness --- requiring a bunch of boilerplate to pattern match out your Identitys --- while ecstasy requires the instances above.

@monoidal

This comment has been minimized.

Copy link
Contributor

monoidal commented Jun 19, 2019

Does this proposal affect type synonyms? Given type T a = a -> a we can give T the same type as if we had type instance T a = a -> a. Currently in ghci there's a hack to make :kind T work (even though T is not a legal type in Haskell) and we could get rid of it.

@jberryman

This comment has been minimized.

Copy link

jberryman commented Jun 20, 2019

@isovector You mean that HKD becomes unnecessary under this proposal, right? Because you can now write ... :: Person Id, where we have something like type family Id a where Id a = a.

Or do you mean we still have pName :: HKD f String ... etc? And the arrow would need to be matchability polymorphic right?

I don't totally follow how this allows deriving instances in the way we'd want but agree that looks like it changes the game for this pattern! So I guess what I'm looking for is an accurate view of how e.g. beam's API might change under this proposal (just one or two types and functions would be fine).

I'm also curious if you think parameterizing an HKD like Person by a traditional open type family would actually be useful (I don't have a good intuition for this pattern yet). I got the sense some of the challenge/complications in this proposal are due to non-injectivity of type synonym families (I may be totally wrong), and so I wonder if a simpler solution could satisfy 80% of use cases, say.

@Ericson2314

This comment has been minimized.

Copy link
Contributor

Ericson2314 commented Jun 21, 2019

@AntC2

I'm still feeling that putting the decoration on the arrow is the wrong place: it applies for the argument.

That and a few other of your comments leads me to think you think "matchability" applies to the function parameter. It doesn't; it really is about the function itself. @goldfirere in his these for example allows

case x :: a -> Either a b of
  Left -> ...
  Right -> ...

and

case x :: Type -> Type of
  Maybe -> ...
  List -> ...
  _ -> ...

This proposal doesn't go as far as the thesis, but the idea is that type constructor equality and variant equality are decidable in the same way.

[@goldfirere mentioned regretting some of the terminology, I don't quite know whether/how that applies to the above.]

@AntC2

This comment has been minimized.

Copy link
Contributor

AntC2 commented Jun 21, 2019

Thanks @Ericson2314 then all I can say is that the current state of the proposal is making a terrible job of explaining what's going on. Typically the text from these proposals goes forward to the Users Guide. (And with other proposals I've had cause to get the Users Guide dramatically rewritten, precisely because the Guide followed the proposal text, not what the extension actually does.) Then I suggest this proposal is not ready. I don't expect (nor do I expect the audience of the Users Guide) to have to read @goldfirere's thesis to be able to use this feature -- not even to read the paper.

I'm all for making the type language more consistent with the term language. Then explain this: Haskell doesn't at term level distinguish between functions that take (partially applied) functions as arguments vs functions that take (partially applied) data constructors as arguments. Indeed Haskell at term level doesn't distinguish whether anything is partially applied or saturated. It just has to be the right type. Then where's this claim coming from that "The aim of this proposal is to bring the type language closer to the term language ..."?

I get it that type functions are not like term-level functions, because the types (kinds) of type functions are not generative nor injective (usually). Then it seems to me that "bring[ing] the type language closer to the term language" is impossible, and the claim is bogus. And this proposal should just say it's to support partially-applying type functions (a good thing), and stop making overblown promises.

Specifically

The proposed change is to distinguish between type constructors and type functions in the kind system.
This new version of Map can now only be applied to type families, but not type constructors, ...

How is that not "appl[ying] to the function parameter."? (parameter as type constructor vs as type function)

... it really is about the function itself.

My first line of enquiry was to suggest it's about the function as a whole; and @kcsongor corrected that by saying a function-as-a-whole might have a mix of ->, ~> arrows in its kind. Now you seem to be contradicting that: "... applies to the function parameter. It doesn't;". What wording/explanation is going into the Users Guide?

This proposal doesn't go as far as the thesis, ...

Then pointing me to the thesis is no sort of explanation of this proposal. And I don't understand your example quoted from the thesis (those are (promoted) type constructors, so generative/injective, so can appear unsaturated OK, so have nothing to do with the case); and it doesn't appear in the proposal; so it just seems a distraction.

[ mentioned regretting some of the terminology, ...]

The specific term to be possibly regretted is "matchability", on grounds it seems to be counter-intuitive to what can be matched. "allowed to appear unsaturated" might be a more felicitous term, but hardly trips off the tongue.

" [doesn't ] appl[y] to the function parameter. ...; it really is about the function itself." isn't helping my intuition either.

@Ericson2314

This comment has been minimized.

Copy link
Contributor

Ericson2314 commented Jun 21, 2019

@AntC2

The simpler way to combine the type and term languages is just to remove this axiom at all levels. And that's what Agda and Idris do! But people complain about type inference being bad and those languages, and we absolutely don't want to create a regression! The alternative is differentiating our fs and gs on both levels so we a) unify the languages b) keep the axiom over it's original domain b) don't regress.

It's certainly possible. Finding the commonality between two fixed things and faithfully recreating them at the library, rather than language, level is an ancient FP trick. That's all that's being done here.

How is that not "appl[ying] to the function parameter."? (parameter as type constructor vs as type function)

Map is a higher order type family. The matchability-polymorphic function is not Map itself (the outer arrow in the kind) but it's parameter (the inner arrow in the kind).

My first line of enquiry was to suggest it's about the function as a whole; and @kcsongor corrected that by saying a function-as-a-whole might have a mix of ->, ~> arrows in its kind. Now you seem to be contradicting that: "... applies to the function parameter. It doesn't;".

I agree with @kcsongor. I think the confusion was that there merely being a mix of arrows in the kind doesn't say which one, why.

"allowed to appear unsaturated" might be a more felicitous term, but hardly trips off the tongue.

But with this proposal everything is allowed to be unsaturated. You might not get fan automatic equality reasoning from GHC when you do something really fancy, but you certainly won't be directly forced to add more arguments.

@monoidal

This comment has been minimized.

Copy link
Contributor

monoidal commented Jun 25, 2019

Is there an interaction with promotion? For example, can I declare data A where MkA :: X ~> A and use MkA on the type level?

Will there be an equivalent of -fprint-explicit-runtime-reps (that is, not showing Matchability variables by default and zapping them to Matchable)?

Is there interaction with top-level kind signatures?

@isovector

This comment has been minimized.

Copy link
Contributor

isovector commented Aug 11, 2019

I'm really excited about this proposal. @kcsongor is there anything in the way of submitting this to the committee?

@kcsongor

This comment has been minimized.

Copy link
Author

kcsongor commented Aug 13, 2019

@isovector I will update the proposal in light of the comments here, but most likely that will be after ICFP (which is next week).

@tydeu

This comment has been minimized.

Copy link

tydeu commented Sep 15, 2019

I am a bit confused as to why this proposal is so complex given the fact that we can already simulate unsaturated type families in current GHC.

For instance, we can implement the Map example in the proposal like so:

infixl 9 :$:
data f :$: a

type family Map (f :: k) (xs :: [a]) :: [b] where
    Map _ '[]       = '[]
    Map f (x ': xs) = BR (f :$: x) ': Map f xs

type family Id a where
  Id a = a

-- Type-level beta reduction
type family BR (a :: k1) :: k2

data Map'
type instance BR Map' = Map'
type instance BR (Map' :$: f) = (Map' :$: f)
type instance BR (Map' :$: f :$: a) = Map f a

data Id'
type instance BR Id' = Id'
type instance BR (Id' :$: a) = Id a 

type instance BR Maybe = Maybe
type instance BR (Maybe :$: a) = Maybe a

type MaybeMap' = Map' :$: Maybe
type DoMaybeMap = BR (MaybeMap' :$: '[Int, Bool])
-- DoMaybeMap == '[Maybe Int, Maybe Bool]

type DoIdMap = BR (Map' :$: Id' :$: '[Int, Bool])
-- DoIdMap == '[Int, Bool]

Thus, it seems like we could alternatively implement unsaturated types (and type-level lambdas in general) by doing something like the following:

  • Desugaring type-level application f a to BR (f :$: a)
  • Reducing completely unsaturated type family names (and possibly type synonyms) to some unique type (i.e. like the prime types in the above example)
  • Creating a magic type family BR that maps to the given type family (or type synonym) when given fully saturated application of its name. Otherwise it is the identity function.

My point with all this is that this proposal seems to be adding a lot of complexity to the type system when that might not be necessary. This seems especially true since the end goal is currently essentially achievable, just not in a particularly pretty way.

@LeanderK

This comment has been minimized.

Copy link

LeanderK commented Oct 2, 2019

this proposal introduces the matchability polymorphism through the mixfix syntax: ->{m}. But, as a side effect, it doesn't allow one to use the mixfix syntax for other stuff, does it?

Example:
type a ~~>{cat} b = cat a b

@goldfirere

This comment has been minimized.

Copy link
Contributor

goldfirere commented Oct 11, 2019

On Aug 13, @kcsongor wrote:

I will update the proposal in light of the comments here, but most likely that will be after ICFP (which is next week).

I don't see any commits since then. As someone very keen to get this into GHC, may I prod you to take action? Thanks!

@kcsongor

This comment has been minimized.

Copy link
Author

kcsongor commented Oct 13, 2019

I have updated the proposal. The remaining question seems to be around syntax, which I have elaborated more on in the unresolved questions section. As @RyanGlScott mentioned, due to the magic fixity of ->, not wiring the other arrow (whatever it might look like) would mean that they would have inconsistent fixities. On that note, I would be happy to hear suggestions on what the other arrow should look like, as ~> is already a widely used symbol.

Similarly, whether to rename matchability, as pointed out by @goldfirere and @AntC2, there is potential for confusion about what it exactly means.

@JakobBruenker

This comment has been minimized.

Copy link

JakobBruenker commented Oct 13, 2019

I would be happy to hear suggestions on what the other arrow should look like

I believe this same question was mentioned in the discussion of this proposal: #102

Which, like in @goldfirere's thesis, used '-> for matchable and -> for unmatchable quantification. This has the advantage of being easily extendable to the other quantifiers (some of which aren't actually in ghc at this point), either as

'forall a .
'forall a ->
'foreach a .
'foreach a ->
'=>

more or less as they appear in the thesis, or as

forall a '.
forall a '->
foreach a '.
foreach a '->
'=>

as was proposed in the linked pull-request discussion. Though there is some ambiguity over how something like forall a'. would be resolved with that syntax, i.e. whether it would be forall a' . or forall a '..

@goldfirere

This comment has been minimized.

Copy link
Contributor

goldfirere commented Oct 14, 2019

After some thought, I have an idea about names: what about constructive (instead of matchable) and flexible (instead of unmatchable)? The name constructive comes from the fact that generative, injective functions are like constructors. (I also considered constructor-like, but that was too much of a mouthful.) I'm not as enamored of flexible, but it seemed a nice counterpoint. I will use these terms in this post to try them out. Of course, the terms will be adopted only by consensus.

I'm still quite stuck on the arrows point. I agree that the new arrows need to be built-in syntax (enabled by extension) because of the fixities problem. As a stop-gap, we could just say that -> is constructive-polymorphic (flex-polymorphic?), and we have heuristics to default it. Viz: We have -> be flexible in types of terms and constructive in kinds of types. (Note that while it's hard to know the type-of-term vs kind-of-type distinction once the type is sloshing around GHC, it's much easier to know it as we're processing user input.) Then we introduce two new arrows: one is always constructive and one is always flexible. Maybe |-> is constructive (that vertical bar looks quite sturdy) and $-> is flexible (that $ glyph has a vertical bar, too, but is all squiggly). Maybe it's also time to say that any operator that ends in -> is reserved or something. (That will annoy a lot of people.) For flex-polymorphic (that's much better than "constructive-polymorphism" or "constructivity-polymorphism") functions, we could imagine a -> @f b, using the infix type application syntax of #12363 (which pre-dated our proposals process).

To be clear, I don't love any idea here (my favorite is the word "constructive"), but I'm just brainstorming in public a bit so we can iron out the last few wrinkles and move this forward.

@goldfirere

This comment has been minimized.

Copy link
Contributor

goldfirere commented Oct 14, 2019

(I had not seen @JakobBruenker's post before writing my own just now.)

Ah, yes, I had forgotten about '->. I'm OK with that, too. That was meant to be in sympathy with the ' operator for namespace selection, but the namespace-selection mechanism seems to be moving in a different direction. '-> has some nice advantages that @JakobBruenker points out.

@JakobBruenker

This comment has been minimized.

Copy link

JakobBruenker commented Oct 14, 2019

For flex-polymorphic [...] functions, we could imagine a -> @f b

I prefer this over the ->{m} syntax.

The $-> and |-> arrows seem all right to me, and even the prospect for hypothetically extending them to forall etc. is better than I expected, with |. and $. only having one result found by hoogle, each.

However, doesn't the idea of defaulting -> to be constructive in kinds go against your point 1. in #242 (comment), of using -> as the unmatchable flexible arrow, @goldfirere? Or do you not consider this to be an issue if two new arrows are introduced?

@kcsongor

This comment has been minimized.

Copy link
Author

kcsongor commented Oct 14, 2019

@goldfirere I don't have very strong opinions about naming, but at a first glance I like "constructive/flex". I was thinking along similar lines, that is to say "passive/active" where "passive" means that the term cannot reduce, and "active" means that it can. This, however, might be more confusing as there is no direct reference to constructors. How about "constructive/functional"? Again, the latter is quite overloaded, which again is unfortunate.

As a stop-gap, we could just say that -> is constructive-polymorphic (flex-polymorphic?)

Just to clarify I understand what you have in mind here: is the idea to implicitly introduce a "flex"-unification variable whenever a -> shows up, and hand it off to the constrain solver, then make sure to never quantify over the metavariables and just default the unconstrained ones? If so, I actually had a similar idea, and I think it could work quite well in the term case (and also together with promotion), which I have written down here.

@JakobBruenker
The '-> syntax suggests to me that there is some promotion going on, and |-> is reminiscent of functional dependencies. These are just observations, I am happy with either syntax.

@goldfirere

This comment has been minimized.

Copy link
Contributor

goldfirere commented Oct 14, 2019

A chat today moved things further.

  • My chat partners (notably @simonpj and @sweirich) did not like "constructive" as it's used for too much already. While that's true, it's used in type-theory contexts more so than in Haskell contexts. I still like it. In the end, I couldn't convince anyone to use "constructive", and so we used "matchable" and "unmatchable".

    I like "functional" too, but that also seems overloaded.

  • Originally I had thought that the complication with arrows, etc., was about migrating from today to tomorrow, and that led me thinking about certain designs. But I had a realization today: even when this feature is old, most Haskellers will not want to worry about whether their functions are constructive. That is, they'll want to write type family Map (f :: a -> b) (xs :: [a]) :: [b] without worrying and they'll want to be able to say Map Just without worrying. So maybe we want -> to mean: "GHC, just do your best here". And then we have two other arrows to allow users to specify exactly what they mean. I guess this idea is the same as the proposal I wrote earlier, and the same as Csongor's thoughts, but I think viewing the "ambiguous" arrow as a feature to embrace, not a wart, changes our perspective a bit.

  • Regarding polymorphism: linear types has stolen the syntax before the arrow. This proposal can take up residence after the arrow. And then it's up to someone else to find a general solution. :) I'm out of ideas here, clearly.

@kcsongor

This comment has been minimized.

Copy link
Author

kcsongor commented Oct 15, 2019

I think viewing the "ambiguous" arrow as a feature to embrace, not a wart, changes our perspective a bit.

Agreed. My only concern with the idea has been that such ambiguity has not yet been done in other parts of the language (as far as I'm aware), but there is a systematic way to resolve the syntactic ambiguity, and it indeed feels more user friendly (without sacrificing expressivity).
As an additional data point, Coq does something similar for inferring universe levels, and makes the universe hierarchy quite a lot nicer to work with than the manual quantification that Agda has.

@JakobBruenker

This comment has been minimized.

Copy link

JakobBruenker commented Oct 15, 2019

did not like "constructive" as it's used for too much already.

Obvious alternatives that perhaps aren't used as widely would be "constructing" and "constructional", though I don't think they're as nice.

The obvious opposite of "flexible" would be "rigid".

Though, I could well be wrong about this, but I got the impression that you can't actually have generative functions in Haskell that are also non-injective? If that's true, it might be enough to simply use the word "generative" in most contexts, though I suppose that's not as clean from a theoretical standpoint.

edit: Ah, actually, looking at the definition of "generative" in the thesis again, it states that it's really a property about pairs of types, so I assume that any function at least has to be generative with respect to itself? Since f a ~ f b implies f ~ f. Which would render my point moot.

@goldfirere

This comment has been minimized.

Copy link
Contributor

goldfirere commented Oct 16, 2019

I thought about "generative". But we can have generative, non-injective type constructors -- at a representational role.

data P a
type role P phantom

Considered at a representational role, P is generative (if P ty1 ~R a ty2, then surely a ~R P) but it's not injective. It's true we don't have these at a nominal role. Yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.