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

The new union access is not transitive across record field #692

Closed
jneira opened this issue Nov 21, 2018 · 9 comments
Closed

The new union access is not transitive across record field #692

jneira opened this issue Nov 21, 2018 · 9 comments

Comments

@jneira
Copy link
Collaborator

jneira commented Nov 21, 2018

This dhall definition works as intended:

let Scope = < Public : {} | Private : {} >
in Scope.Public {=}

However, puting the union in a record field doesnt work (at least as i expected):

let Scope = < Public : {} | Private : {} >
let types = { Scope = Scope }
in types.Scope.Public {=}

It throws:

Use "dhall --explain" for detailed errors

 Error : Not a record or a union

   types.Scope.Public
(stdin):3:4
@jneira
Copy link
Collaborator Author

jneira commented Nov 21, 2018

Well. it seems this was already discussed in the pr. So, would be an alternative to dhall-to-cabal usage of records of unions taking in account that constructors will not work when it is equal to id?

@jneira
Copy link
Collaborator Author

jneira commented Nov 21, 2018

Adding a let with the record field makes it work too:

let Scope = < Public : {} | Private : {} >
let types = { Scope = Scope }
let MyScope = types.Scope
in MyScope.Public {=}

I feel that behaviour it is not very intuitive 🤔

@Gabriella439
Copy link
Collaborator

@jneira: The standard permits what you are trying to do. This is just a bug in the Haskell implementation since it is not complying with the standard. I believe I can fix this pretty easily before cutting the next release.

Gabriella439 added a commit that referenced this issue Nov 21, 2018
Fixes #692

The standard permits a user to access a constructor from a type stored inside
a record, but the Haskell implementation had a mistake which prevented this.
Specifically, the Haskell implementation was not normalizing the union type
as the standard specified before attempting to access the constructor, leading
to an unexpected type error.
@Gabriella439
Copy link
Collaborator

Fix is up here: #694

I will merge that before cutting the release tonight if nobody objects

Gabriella439 added a commit that referenced this issue Nov 22, 2018
Fixes #692

The standard permits a user to access a constructor from a type stored inside
a record, but the Haskell implementation had a mistake which prevented this.
Specifically, the Haskell implementation was not normalizing the union type
as the standard specified before attempting to access the constructor, leading
to an unexpected type error.
@jneira
Copy link
Collaborator Author

jneira commented Nov 22, 2018

Just tested with dhall-to-cabal and it works fine, thanks @Gabriel439
However you cant nest the record of unions in another record. This code:

let types = { Scopes = < Public : {} | Private : {} >}
let prelude = { types = types }
in prelude.types.Scopes.Public {=}

fails with

 Use "dhall --explain" for detailed errors
 Error: Invalid field
              { types = types }

explained:

You provided a record literal with a field named:
↳ types
... whose value is:
↳ { Scopes = < Private : {} | Public : {} > }
... which is not a term or ❰Type❱

This one follows the new dhall spec?
In that case we'll have to separate types from values in dhall-to-cabal prelude.

@ocharles
Copy link
Member

ocharles commented Nov 22, 2018 via email

@Gabriella439
Copy link
Collaborator

Actually, it's disallowed for a different reason. To explain why, I'll need to specify some terminology:

  • "x is a type" (with a lower-case "t") means that the type of the type of x is Kind
  • "x is a kind" (with a lower-case "k") means that the type of the type of x is Sort
  • "x is a Type" (with an upper-case "T") means that the type of x is Type
  • "x is a Kind" (with an upper-case "K") means that the type of x is Kind

All Kinds are kinds and all Types are types, but the converse is not necessarily true: not all types are Types and not all kinds are Kinds. For example:

  • List is a type, but not a Type:

    List : Type  Type : Kind
  • { x = Bool } is a kind, but not a Kind:

    { x = Bool } : { x : Type } : Sort  -- In version 4.0.0 of the standard

Carefully note that in version 3.0.0 of the standard a record of types was treated as a type instead of a kind:

```haskell
{ x = Bool } : { x : Type } : Kind  -- In version 3.0.0 of the standard
```

One of the necessary changes to fix dhall-lang/dhall-lang#250 in version 4.0.0 of the standard was that a record of types is now treated as a kind. This is relevant to the error you got.

The standard also separately specifies that a record can store Kinds like this:

{ x = Type }

... but does not enable support for storing more general (lower-case "k") kinds, for reasons having to do with how it would be encoded in CCω.

So the exact issue here is that this:

< Private : {} | Public : {} >

... is a type, and therefore this:

{ Scopes = < Private : {} | Public : {} > }

... is a kind, but it is not a Kind, therefore you cannot store it inside of another record.

This implies that a types record cannot be nested.

@jneira
Copy link
Collaborator Author

jneira commented Nov 23, 2018

Great explanation, as always. I feel that nesting records should be allowed in general (seems to me that it is more uniform) but i know that it is more important make sound the type system

@phadej
Copy link
Contributor

phadej commented Nov 23, 2018

Pairs

Let's start with a variant of Gabriel's example:

-- let RecordT = {x : Type, y : Type -> Type} -- we cannot write this!
let Record : {x : Type, y : Type -> Type} = {x =  Bool, y = List}
in Record.y Record.x
  • dhall type says Type
  • dhall says List Bool
  • Record : {x : Type, y : Type -> Type} : Sort

We try to understand why this is so. Standard doesn't explain (or I fail to find)
why this is so.

Church encoding

If we look at Church encoding of pairs

-- cannot write this either:
-- let RecordT = forall (r : Kind) -> (Type -> (Type -> Type) -> r) -> r
let Record = \(r : Kind) ->
             \(f :  Type -> (Type -> Type) -> r) ->
             f Bool List
in Record Type (\(x : Type) -> \(y : Type -> Type) -> y x)
  • dhall type says Type
  • dhall says List Bool

So I guess this is the explanation, why records are typed in this way.

Type is impredicative

For comparison, on the term level, this is not an issue, because
Type is an impredicative universe:

-- this type is ok:
let RecordT = forall (r : Type) -> (Natural -> (Natural -> Natural) -> r) -> r
let Record : RecordT
           = \(r : Type) ->
             \(f : Natural -> (Natural -> Natural) -> r) ->
             f 42 (\(n : Natural) -> n + 1)
in Record Natural (\(x : Natural) -> \(y : Natural -> Natural) -> y x)
  • dhall type says Natural
  • dhall says 43
  • Record : RecordT : Type, not Kind!

Because of impredicativity of Type (forall (x : Type) -> (... : Type) : Type)
we can nest records of values,
but because Kind is predicative (forall (x : Kind) -> (... : Kind) : Sort`)
a types record cannot be nested.

This is unfortunate.

Why this is not a problem, say in Agda or Coq (which are predicative)?
I present one way of thinking about this:

Agda

Church encoding

Let's first encode the Church pair in Agda, for comparison
(and get familiar with syntax)

module Expr where

open import Data.Nat using (ℕ)
open import Data.Bool using (Bool)
open import Data.List using (List)

-- Agda has universes too:
Type = Set
Kind = Set₁
Sort = Set₂
-- but there are infinitely many

RecordT : Sort
RecordT =  (r : Kind)  (Type  (Type  Type)  r)  r

Record : RecordT
Record r f = f Bool List

example : Type
example = Record Type λ x y  y x

Here, Dhall and Agda work similarly. (And even look very similar!)

"Built-in" Pairs

Agda has built in products (pairs), note that
RecordT₂ : Kind

module Interlude where
  open import Data.Product

  RecordT₂ : Kind -- !!!
  RecordT₂ = Type × (Type  Type)

  Record₂ : RecordT₂
  Record₂ = Bool , List

  -- Here, Agda needs a bit of help, i.e. type annotation
  example-nested₂ : (Type × Type) × ((Type  Type) × Type)
  example-nested₂ = ((ℕ  ℕ) , Bool) , (List , Bool)

And because RecordT₂ : Kind we can nest pairs or types.

  -- Here, Agda needs a bit of help, i.e. type annotation
  example₃ : (Type × Type) × ((Type  Type) × Type)
  example₃ = ((ℕ  ℕ) , Bool) , (List , Bool)

Making them ourselves

module SimplePair where
  -- Note that A × B lives in universe below than its Church Encoding!
  record _×_ {ℓ} (A : Set ℓ) (B : Set ℓ) : Setwhere
    constructor _,_
    field
      proj₁ : A
      proj₂ : B

  open _×_

  RecordT₃ : Kind -- !!!
  RecordT₃ = Type × (Type  Type)

  Record₃ : RecordT₃
  Record₃ = Bool , List

  -- C-n example₂ --> List Bool
  example₃ = (proj₂ Record₃) (proj₁ Record₃)

  -- because this pair is simpler, Agda can figure out types:
  example-nested₃ = ((ℕ  ℕ) , Bool) , (List , Bool)

So, for me, it looks safe to start treating records in this way; i.e.
with "built-in" typing rules, and not "merely a syntax" for Church encoding
(I'm not sure this is the case now, it looks like it is).

Σ-types work this way (CCω paper calls them negative pairs):

 Γ ⊢ A : □ i    Γ, x : A ⊢ B : □ j
-----------------------------------
 Γ ⊢ Σ x : A, B : □ (max i j)

Note that (dependent) pair lives in □ (max i j); not the □ (succ (max i j)) !

Dhall can have very simple variant:

 Γ ⊢ A : □ i    Γ ⊢ B : □ i
----------------------------
 Γ ⊢ A × B : □ i

Or it can make having types and terms in the same pair via

 Γ ⊢ A : □ i    Γ ⊢ B : □ j
----------------------------
 Γ ⊢ A × B : □ (max i j)

The record construction is universe polymorphic already, as it works for Type
and Kind; I don't see (theoretical) reason for it not to be a bit more
polymorphic. But in practive Non-recursive right-biased merge will be
quite tricky to type (disjoint merge would be simpler). Maybe one can make
it work by requiring that overlapping fields have the same sort.

{ a = Bool } // {a = Natural} ---> { a = Natural }
{ a = Bool } // {a = List}    ---> { a = List }
{ a = Bool}  // {a = 42}      ---> error, Bool : Type : Kind; but 42 : Natural : Type; Kind /= Type

This doesn't look much different than the way Recursive record merge
requiring colliding fields are records.

TL;DR

Records (pairs is disguise) should be built-in.

@Gabriella439 Gabriella439 reopened this Nov 23, 2018
Gabriella439 added a commit to dhall-lang/dhall-lang that referenced this issue Nov 23, 2018
Fixes dhall-lang/dhall-haskell#692

The motivation for this change is to permit nested records of types.

This changes a record of types to be treated as a type instead of a
kind, as suggested by @phadej in
dhall-lang/dhall-haskell#692 (comment)

The original encoding of a record of types used kind polymorphism, which
meant that a record of types behaved like a kind instead of a type.

However, there's another way to encode records which takes advantage of
the fact that they have a finite number of fields of of known type by
desugaring them to a finite number of function arguments.

In other words, a function of this type:

```haskell
∀(r : { x : Type, y : Type → Type }) → …
```

... desugars to a function of this type:

```haskell
∀(x : Type) → ∀(y : Type → Type) → …
```

Similarly, the following anonymouse function:

```haskell
∀(r : { x : Type, y : Type → Type }) → …
```

... desugars to:

```haskell
λ(x : Type) → λ(y : Type → Type) → …
```

Accessing a field from an abstract record:

```haskell
∀(r : { x : Type, y : Type → Type }) → … r.x …
```

... translates to referencing the correct parameter:

```haskell
λ(x : Type) → λ(y : Type → Type) → … x …
```

Function application where the record is an argument:

```haskell
f { x = Bool, y = List }

-- or:

∀(r : { x : Type, y : Type → Type }) → … f r …
```

... desugars to applying the function to each field:

```haskell
f Bool List

-- or:

λ(x : Type) → λ(y : Type → Type) → … f x y …
```

If you encode things this way then a record of types behaves like a
type.  To be precise, a record is a valid function argument if and only if its
constituent fields are also valid function arguments, so if its fields
are types then you can treat a record of them as a type, too.
Gabriella439 added a commit that referenced this issue Nov 23, 2018
Gabriella439 added a commit to dhall-lang/dhall-lang that referenced this issue Nov 26, 2018
Fixes dhall-lang/dhall-haskell#692

The motivation for this change is to permit nested records of types.

This changes a record of types to be treated as a type instead of a
kind, as suggested by @phadej in
dhall-lang/dhall-haskell#692 (comment)

The original encoding of a record of types used kind polymorphism, which
meant that a record of types behaved like a kind instead of a type.

However, there's another way to encode records which takes advantage of
the fact that they have a finite number of fields of of known type by
desugaring them to a finite number of function arguments.

In other words, a function of this type:

```haskell
∀(r : { x : Type, y : Type → Type }) → …
```

... desugars to a function of this type:

```haskell
∀(x : Type) → ∀(y : Type → Type) → …
```

Similarly, the following anonymouse function:

```haskell
∀(r : { x : Type, y : Type → Type }) → …
```

... desugars to:

```haskell
λ(x : Type) → λ(y : Type → Type) → …
```

Accessing a field from an abstract record:

```haskell
∀(r : { x : Type, y : Type → Type }) → … r.x …
```

... translates to referencing the correct parameter:

```haskell
λ(x : Type) → λ(y : Type → Type) → … x …
```

Function application where the record is an argument:

```haskell
f { x = Bool, y = List }

-- or:

∀(r : { x : Type, y : Type → Type }) → … f r …
```

... desugars to applying the function to each field:

```haskell
f Bool List

-- or:

λ(x : Type) → λ(y : Type → Type) → … f x y …
```

If you encode things this way then a record of types behaves like a
type.  To be precise, a record is a valid function argument if and only if its
constituent fields are also valid function arguments, so if its fields
are types then you can treat a record of them as a type, too.
Gabriella439 added a commit that referenced this issue Nov 27, 2018
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

No branches or pull requests

4 participants