-
Notifications
You must be signed in to change notification settings - Fork 211
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
Add kind polymorphism. #563
Conversation
I wanted to propose this before I spend too much time on it, so let me know if it’d be acceptable in some form. (And it would also require a change to the language spec.) Here’s an example: ./polyfunctor ```dhall λ(j : Kind) → λ(k : Kind) → λ(c : j → j → Type) → λ(d : k → k → Type) → λ(f : j → k) → { map : ∀(a : j) → ∀(b : j) → c a b → d (f a) (f b) } ``` then ```dhall ./polyfunctor Type Type (λ(a : Type) → λ(b : Type) → a → b) (λ(a : Type) → λ(b : Type) → a → b) Optional ``` normalizes to ```dhall { map : ∀(a : Type) → ∀(b : Type) → (a → b) → Optional a → Optional b } ``` and ```dhall ./polyfunctor (Type → Type) (Type → Type) (λ(a : Type → Type) → λ(b : Type → Type) → ∀(i : Type) → a i → b i) (λ(a : Type → Type) → λ(b : Type → Type) → ∀(i : Type) → a i → b i) ``` normalizes to ```dhall λ(f : (Type → Type) → Type → Type) → { map : ∀(a : Type → Type) → ∀(b : Type → Type) → (∀(i : Type) → a i → b i) → ∀(i : Type) → f a i → f b i } ```
Just to have a non-endofunctor: ./polyfunctor
(Type -> Type)
Type
(λ(a : Type → Type) → λ(b : Type → Type) → ∀(i : Type) → a i → b i)
(λ(a : Type) → λ(b : Type) → a → b) normalizes to λ(f : (Type → Type) → Type)
→ { map :
∀(a : Type → Type)
→ ∀(b : Type → Type)
→ (∀(i : Type) → a i → b i)
→ f a
→ f b
} (which is the “kind” of thing I would use in dada for mapping over fixed-point operators). |
@sellout: Yes, I'll accept this. The main benefit of this (which you appear to have noticed while implementing this) is that it allows records of types with mixed kinds (i.e. It also doesn't affect existing user code |
If we can do that step, what’s the problem with doing the same for values as well? That the type system would need to become dependent in the process? |
Then how would you feel about universes? And maybe universe polymorphism? 😆 |
@Profpatsch: No, separating types and terms in records is independent of whether or not the type system is dependent. In other words, even if we turned on dependent types we would still need to keep types and terms separate. Basically all of Dhall's higher-level features (like builtins, records, lists, unions, etc.) can be encoded in terms of lower-level features (i.e. pure lambda calculus) using Dhall's own type system. For example, a record of the form: { foo = 1, bar = True } ... could be encoded like this: λ(result : Type) → λ(makeRecord : ∀(foo : Natural) → ∀(bar : Bool) → result) → makeRecord 1 True ... and accessing a field of a record like that: λ(r : { foo : Natural, bar : Bool }) → r.foo ... could be encoded like this: λ(r : ∀(result : Type) → (∀(foo : Natural) → ∀(bar : Bool) → result) → result)
→ r Natural (λ(foo : Natural) → λ(bar : Bool) → foo) The encoding might be very clumsy to use and might be less efficient, but it's theoretically possible to translate all of Dhall's features to raw System Fω. The reason these encodings are important is that they establish that Dhall's higher-level features are still sound and non-Turing-complete since they can be translated to a non-Turing-complete language (System Fω in this case). The reason this feature is sometimes confused with dependent types is because if you don't have a sound way to translate records to the underlying lambda calculus then records become a backdoor that you can use to break the type system. One example of that breaking the type system was using records to make code accidentally dependently typed, but that's not the only possible way that it can break the type system. As @sellout just mentioned, if you want to have records that mix types and terms, then what you actually need is universe polymorphism in order to be able to encode records of types and terms in lambda calculus. Universe polymorphism means that instead of { foo = 1, bar = Natural } ... would translate to: λ(n : Natural) → λ(result : Type n) → λ(makeRecord : ∀(foo : Natural) →∀(bar : Type) → result) →makeRecord 1 Natural @sellout: I'm not sure about that yet. Let me spend some time reasoning through how it's encoded and how that would affect the type-checking rules for records (and how to do it in a way that doesn't break existing user code). |
@sellout: So the issue with using universe polymorphism is that you can't hide it from the user when type-checking record types. The issue you run into is that the type of a record type would end up being polymorphic over the universe and you wouldn't be able to hide that universe-polymorphism from the end user for a mixed record of terms and types. For example, under the current type system, the type of the type of a record of terms is Let me motivate this with a concrete example with the following type-level function: -- ./Map.dhall
--
-- Simulate a map as an association list
λ(type : { key : Type, value : Type }) → List { key : type.key, value : type.value }
The interpreter (correctly) does not flag However, now imagine that you have a record with these contents: { key = Text, value = Natural, x = 0 }
: { key : Type, value : Type, x : Natural }
: ∀(n : Natural) → Type n ... which could potentially be used as either a record of terms or types. The user would need to explicitly instantiate it to a record of types before they could pass it to So I think the best we'll be able to do for the foreseeable future is your original proposal to add There's another related issue I considered alongside this one, which is the proposal you raised earlier for dependent records. I think that even if we don't support universe polymorphism it doesn't preclude the possibility of dependent records. The reason why is that in the vast majority of use cases for dependent records (i.e. modules with scoped access to the ordered record fields) you usually always want the final result of the scoped access to be a term (or another module), so the type of the type of a dependent record can be fixed to |
Is this necessarily true? I may be out of my depth here, but I was thinking that effectively { key : Type, value : Type, x : Natural } : Kind isn’t valid with kind polymorphism, universe polymorphism wouldn’t necessarily allow { key : U 0, value : U 0, x : Natural } : U 1 With universe polymorphism, I was hoping to be able to to do something roughly like -- pattern functor for lists
λ(level : Level)
→ λ(a : U level)
→ λ(b : U level)
→ < Nil : ({} : U level)
| Cons : { head : a, tail : b }
>
: U level to enable HLists, etc. |
@sellout: So I looked into this more closely and it looks like in Agda the way they do this is that the universe level represents an upper bound on the type of the type of the value that you can extract from a record. Specifically, if the type of the type of However, I think that if you want this for heterogeneous lists then it sounds like what you want is metaprogramming which I think would be solved better by access to the abstract syntax tree rather than implementing it with the language. The reason why is that if you implement heterogeneous lists within the language you are going to lose all the syntactic niceties and features of Dhall records which you could keep if you access the record structure through the Haskell API. |
I think this is bad idea. % cat example.dhall
λ(j : Kind) → λ(a : Kind → Kind) → λ(b : Kind) → a (a b)
% cat example.dhall | dhall type
∀(j : Kind) → ∀(a : Kind → Kind) → ∀(b : Kind) → Kind https://en.wikipedia.org/wiki/System_U#Girard's_paradox
as above
A simplification of Girard's paradox by Antonius J. C. Hurkens let bottom = forall (any : Type) -> any
in let not = \(p : Type) -> bottom
in let pow = \(X : Kind) -> X -> Type
in let U = forall (X : Kind) -> (pow (pow X) -> X) -> pow (pow X)
in let tau : pow (pow U) -> U
= \(t : pow (pow U)) ->
\(X : Kind) ->
\(f : pow (pow X) -> X) ->
\(p : pow X) ->
(t (\(x : U) -> p (f ((x X) f))))
in let sigma : U -> pow (pow U)
= \(s : U) ->
((s U) (\(t : pow (pow U)) -> tau t))
in let Delta : pow U
= \(y : U) ->
not (forall (p : pow U) -> ((sigma y) p) -> (p (tau (sigma y))))
in let Omega : U
= tau (\ (p : pow U) -> forall (x : U) -> (((sigma x) p) -> p x))
in \(t : pow (pow U)) -> sigma (tau t) |
Now that the spec change is merged (dhall-lang/dhall-lang#238), what’s the the plan for this? More changes to be made? I’ve written tons of code using this branch: |
And I noticed it just failed … but I can’t figure out what I broke from the log. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what's my opinion on this (I mean the whole special casing Sort
vs having Universe n
at least under the hood for simplicity of standardization/implementation), so my only comment about this PR will be: looks good but let's add some normalization and typechecking tests that exercise the new feature 👍
@f-f Good point. I can copy over some stuff from the other repos I linked. |
@sellout: The failure was a transient issue. I restarted the build and it passes now. |
Cool. I’m moving today, but hopefully can add the tests early this week. |
I ran into one issue when testing this branch, which is that a record like |
This looks good to me. Did you have any other changes planned? |
@Gabriel439 Yeah, I noticed (when writing tests) that |
@sellout: If you're busy, just add a failing test and I can fix things so that the test passes |
One fails because `Sort` isn’t parsed correctly. I added `Sort` to the “obvious” places in the parser, but that’s not cutting it. Not sure if it’s something like trying to parse `Some` and then failing to backtrack or what. Also made a small change to the parser tests, since previously they were all getting grouped under “whitespace”, and I don’t think that was correct.
…into kind-polymorphism
@Gabriel439 Ok, I pushed the failing test, along with other stuff. |
This still required removing the test for type-checking `Kind → Kind` since `Sort` is not valid as a type annotation (since it has no type of its own) I replaced it with a test for parsing `Sort` and instead did an ad-hoc verification that `Kind → Kind` had an inferred type of `Sort`
Fix parsing of `Sort`
I wanted to propose this before I spend too much time on it, so let me know if
it’d be acceptable in some form. (And it would also require a change to the
language spec.)
Here’s an example:
./polyfunctor
then
normalizes to
and
normalizes to