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

Record projection by expression #499

Merged
merged 21 commits into from
May 28, 2019

Conversation

dingxiangfei2009
Copy link
Contributor

@dingxiangfei2009 dingxiangfei2009 commented Apr 21, 2019

Close #183, replacing #494.

This PR extends record project by permitting expression normalising to records as the projection selector.

The highlight is that to project fields prescribed by an expression from a record, a round bracket is necessary, eg., r.(e).

@dingxiangfei2009 dingxiangfei2009 changed the title Record projection by xpression Record projection by expression Apr 21, 2019
@dingxiangfei2009
Copy link
Contributor Author

@Gabriel439 I noticed that #493 is quite relevant to this PR. I am thinking of expanding the scope of this work to address the requirement in that issue, though I am not sure if this is okay.

Copy link
Contributor

@Gabriella439 Gabriella439 left a comment

Choose a reason for hiding this comment

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

There are two other things that need to be added:

  • How to α-normalize t.(s)
  • How to encode/decode t.(s) (i.e. in binary.md)

standard/semantics.md Outdated Show resolved Hide resolved
@@ -3894,6 +3906,16 @@ You can only select field(s) from the record if they are present:
Γ ⊢ e.{ x } : { x : T }


Γ ⊢ e :⇥ { ts… } : Kind Γ ⊢ s :⇥ { ss… } : Kind
──────────────────────────────────────────────────── ; ss ⊆ ts
Γ ⊢ e.(s) : { ss… }
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe that the type of e.(s) should be the normal form of s rather than the type of s. In other words:

Γ ⊢ e :⇥ { ts… }   Γ ⊢ s : T   s ⇥ { ss… }
───────────────────────────────────────────  ; ss ⊆ ts
Γ ⊢ e.(s) : { ss… }

I also don't think you need the Kind/Sort type annotations. The type of the type of each record would be checked along the way as part of type-checking e and s

Copy link
Contributor Author

@dingxiangfei2009 dingxiangfei2009 Apr 26, 2019

Choose a reason for hiding this comment

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

My idea is to make it explicit that, in order to type check e.(s), s should lie in a universe immediately above the one where e lives. But I am not sure how to express this constraint.

By the way, what is T in Γ ⊢ s : T ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Think of Γ ⊢ s : T as pseudo-code for:

T <- inferTypeWithContext Γ s

In other words, Γ and s are the two inputs to the inferTypeWithContext function and T is the output of the function. In this case T is the inferred type of s

standard/semantics.md Outdated Show resolved Hide resolved
standard/semantics.md Outdated Show resolved Hide resolved
@Gabriella439
Copy link
Contributor

@dingxiangfei2009: I think #493 should probably be handled as part of a separate pull request so that each change can be understood separately in the history

@singpolyma
Copy link
Collaborator

I would also request that unit test cases be added for each new rule.

@f-f f-f mentioned this pull request Apr 25, 2019
@dingxiangfei2009
Copy link
Contributor Author

@Gabriel439 Speaking of adding a α-normalisation rule, though it is not immediately related, I find the deduction rules for ↑(d, x, m, ε) a bit under-specified. The deduction rules in standard/README.md never mention the situation for different values of d and m. The definition of the data in these tuples is lacking, too. Is there any literature I can refer to regarding this notation?

@Nadrieril
Copy link
Member

Nadrieril commented Apr 26, 2019

@dingxiangfei2009 I may be wrong, but it sounds to me like you haven't seen this: https://github.com/dhall-lang/dhall-lang/blob/master/standard/shift.md . If you have, would you mind being more precise about what you find lacking ?

@dingxiangfei2009
Copy link
Contributor Author

@Nadrieril Ah, I see! It has what I want to read about.

@Nadrieril
Copy link
Member

#509 should make it easier to spot which judgments are defined and where in the future.

standard/beta-normalization.md Outdated Show resolved Hide resolved

encode(t₀) = t₁ encode(T₀) = T₁
─────────────────────────────────
encode(t₀.(T₀)) = [ 27, t₁, T₁ ]
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest using 19 instead of 27, since record projection by type can survive β-normalization. For example, this is a well-typed expression that is in normal form:

λ(e : { x : Bool })  e.({ x : Bool })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see.

Is there any rule on how these numbers are assigned, for backward compatibility?

Copy link
Member

Choose a reason for hiding this comment

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

As mentioned here, we try to assign labels below 24 for syntactic features that survive normalization. Currently we use 0 to 18, except 13 and 17. 13 was used before so for backward-compatibility we want to reuse it as late as possible (see here). I don't know why 17 was skipped, but I'm assuming it is similar to 13. That means that the next free label is 19.

tests/parser/success/recordProjectByType.dhall Outdated Show resolved Hide resolved
@singpolyma
Copy link
Collaborator

singpolyma commented May 7, 2019 via email

@Gabriella439
Copy link
Contributor

@singpolyma: Oh yeah, good point. We should probably use the same label as normal record projection

@@ -0,0 +1 @@
{ a = 10, b = Some 10 }
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

dhall format automatically removes the new line when I add it.

@@ -0,0 +1 @@
{=}
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here.

@@ -0,0 +1 @@
{ a = 10, b = Some 10, c = [ "text" ]}.({ a : Natural, b : Optional Natural }) : { a : Natural, b : Optional Natural }
Copy link
Collaborator

Choose a reason for hiding this comment

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

type annotation on the end here seems unneeded?

@@ -0,0 +1 @@
let e = { a = 10, b = "Text" } let s = { a : Natural } in e.(s) : { a : Natural }
Copy link
Collaborator

Choose a reason for hiding this comment

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

this should be named A and have a B part with it -- the let seems like a lot of extra noise for a unit test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that we should still have a test like this one, even if not necessarily for a unit test. It's important to test that type inference works when the s is an abstract type since that's a likely case that an implementation might implement incorrectly.

Copy link
Contributor

@Gabriella439 Gabriella439 left a comment

Choose a reason for hiding this comment

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

This looks great! I think the only thing left is to see if we can have the binary encoding for e.(s) reuse the same label as for e.x like @singpolyma. Other than that I don't see anything missing

@@ -0,0 +1 @@
let e = { a = 10, b = "Text" } let s = { a : Natural } in e.(s) : { a : Natural }
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that we should still have a test like this one, even if not necessarily for a unit test. It's important to test that type inference works when the s is an abstract type since that's a likely case that an implementation might implement incorrectly.

standard/binary.md Outdated Show resolved Hide resolved
@singpolyma
Copy link
Collaborator

singpolyma commented May 15, 2019 via email

@Gabriella439
Copy link
Contributor

@singpolyma: Oh yeah, that's right. Ignore my concern, then.

@Gabriella439
Copy link
Contributor

I think the only thing missing is to add the expected inferred type for tests/type-inference/success/unit/RecordProjectionByType.dhall (i.e. rename it to tests/type-inference/success/unit/RecordProjectionByTypeA.dhall and add a tests/type-inference/success/unit/RecordProjectionByTypeB.dhall)

Signed-off-by: Ding Xiang Fei <dingxiangfei2009@gmail.com>
@dingxiangfei2009
Copy link
Contributor Author

Sorry for late response. Caught with some other business earlier this week. 😅

Gabriella439 added a commit to dhall-lang/dhall-haskell that referenced this pull request May 22, 2019
Copy link
Contributor

@Gabriella439 Gabriella439 left a comment

Choose a reason for hiding this comment

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

Almost there! 🙂

I just implemented this in dhall-lang/dhall-haskell#958 and noticed a few small things along the way

standard/binary.md Show resolved Hide resolved
@@ -501,6 +501,13 @@ You can only select field(s) from the record if they are present:
Γ ⊢ e :⇥ { x : T, ts… } Γ ⊢ { x : T, ts… } :⇥ Sort
────────────────────────────────────────────────────
Γ ⊢ e.{ x } : { x : T }


Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps add some prose here explaining that the e.(t) syntax lets the user select record fields by type. Currently there isn't any text in the standard explaining this feature

@@ -501,6 +501,13 @@ You can only select field(s) from the record if they are present:
Γ ⊢ e :⇥ { x : T, ts… } Γ ⊢ { x : T, ts… } :⇥ Sort
────────────────────────────────────────────────────
Γ ⊢ e.{ x } : { x : T }


Γ ⊢ e :⇥ { ts… }
Copy link
Contributor

Choose a reason for hiding this comment

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

This judgment needs to somehow enforce that { ts… } and { ss… } are syntactically record types. One way to do that is define the judgment via induction (i.e. define a judgment to type-check the case where the two record types are empty and then define a judgment to type-check the case where the two record types are non-empty), like this:

Γ  e :⇥ {}
Γ  s : T
s  {}
────────────────
Γ  e.(s) : {}

Γ  e :⇥ { x : T₀,  }
Γ  s : T
s  { x : T₁, xs₁ }
Γ  e.({ xs₁ }) : T
────────────────────────
Γ  e.(s) : { x : T₁ ,xs₁ }

Copy link
Contributor Author

@dingxiangfei2009 dingxiangfei2009 May 22, 2019

Choose a reason for hiding this comment

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

@Gabriel439 For the base case, we can relax the requirement on e, since it is still sound, in the same universe, to project empty record from any record.

Also, do you think that the following induction step make sense?

Γ ⊢ e :⇥ { x : T₀, ts… }
Γ ⊢ s : T₀
s ⇥ { x : T₀, ss… }
Γ ⊢ e.({ ss… }) : T₁
───────────────────────────
Γ ⊢ e.(s) : { x : T₀, ss… }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The concern is with the induction step that you suggested earlier, in which Γ ⊢ s : T and Γ ⊢ e.({ xs₁… }) : T cannot reconcile.

Copy link
Contributor

@Gabriella439 Gabriella439 May 22, 2019

Choose a reason for hiding this comment

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

@Nadrieril: Oh yeah, the base case doesn't need to require that e is the empty record. That's a mistake on my part. Good catch!

Also, you're right that the two occurrences of T in the induction judgement should be distinct. That's another mistake on my part.

@Gabriella439
Copy link
Contributor

@singpolyma: Actually, I did run across an ambiguous encoding caught by the binary serialization round-trip property test. See:

https://hydra.dhall-lang.org/build/13281/nixlog/5

The issue is that a projection of an expression which is a built-in:

x.(Bool)

... and an expression projecting a quoted field of the same name:

x.{ `Bool` }

... share the same encoding. They are both encoded as:

[ 10, [ "x", 0 ], "Bool" ]

Technically, it's an ill-typed expression, but it still seems wrong to permit two different expressions to be encoded the same way.

It might be worth still using a different label just to avoid this potential ambiguity (and also to make it easier to write decoders).

@Gabriella439
Copy link
Contributor

Alternatively, we could add a "sub-label" to indicate that it's a record projection by type, perhaps by adding a null element, like this:

encode(e.(t)) = [ 10, encode(e), null, encode(t) ]

@singpolyma
Copy link
Collaborator

Hmm, ok. My initial reaction is that projection on a builtin is a crazy thing to do. But I suppose it's not impossible that a builtin that maps to a record type could ever be added to the language.

If they can't overlap, then I guess I don't feel strongly about label vs sub-label

@Gabriella439
Copy link
Contributor

@dingxiangfei2009: How about we do this, then: store the projection by type inside of another list so that it visually resembles the syntax:

encode(e.(t)) = [ 10, encode(e), [ encode(t) ] ]

@dingxiangfei2009
Copy link
Contributor Author

@Gabriel439 sounds like a better plan!

Copy link
Member

@f-f f-f left a comment

Choose a reason for hiding this comment

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

Looks great, thanks @dingxiangfei2009!

Copy link
Contributor

@Gabriella439 Gabriella439 left a comment

Choose a reason for hiding this comment

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

Thank you for doing this! 🙂

@Gabriella439
Copy link
Contributor

I'll go ahead and merge this since you have all the approvals. Thanks again! 🙂

@Gabriella439 Gabriella439 merged commit 034d48a into dhall-lang:master May 28, 2019
Gabriella439 added a commit to dhall-lang/dhall-haskell that referenced this pull request May 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Extend record projection to allow projecting by record type & import
6 participants