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

Variant constructor matching (w/o argument) differs depending on package structure #7207

Closed
jberthold-da opened this issue Aug 24, 2020 · 10 comments · Fixed by #7303
Closed
Assignees
Labels
language Language team work

Comments

@jberthold-da
Copy link
Contributor

jberthold-da commented Aug 24, 2020

When working with multiple DAML packages imported into one another, I found that variant constructors without arguments take a unit argument when imported from another package, but take no argument when used in the same package. The following example shows the problem:

Package 1 contains this module with a variant definition and a function f1 with a case match on the variant

module Package1File where

data AVariant = V1 Int
              | V2

f1 : AVariant -> Text
f1 x = case x of
         V1 i -> "V1"
         V2   -> "V2"

Package 2 contains a module which imports Package1File and contains an identical function f2:

module Package2File where

import Package1File -- from package 1

f2 : AVariant -> Text
f2 x = case x of
         V1 i -> "V1"
         V2 _ -> "V2"

Note that the match on V2 uses a _ for an uninteresting argument.
This example code compiles when distributed into two packages, but f2 is incorrect when it resides inside the first package.

@jberthold-da jberthold-da added the language Language team work label Aug 24, 2020
@sofiafaro-da
Copy link
Contributor

sofiafaro-da commented Aug 25, 2020

This is a problem with the expressivity of variant types at the DAML-LF level, since there's no distinction between "variant constructor with no inputs" and "variant constructor with a single input of unit type" at the DAML-LF level, as is documented in the official docs.

Perhaps it would be best to pick the other default in data-dependencies? I.e. to translate all "variant constructor with a single input of unit type" into "variant constructor with no inputs". Either way, one of these two will always be translated back into DAML incorrectly in data-dependencies, unless we change something more fundamental about how types are translated into DAML-LF or represented on the ledger API.

This would be a breaking change for existing projects that rely on data-dependencies.

cc @bame-da

@bame-da
Copy link
Contributor

bame-da commented Aug 25, 2020

If you changed the behaviour in a new DAML-LF version (eg 1.9) so that data-dependencies of LF >= 1.9 packages behaved properly by distinguishing the two cases, think this could be called a bug-fix. Despite being documented, the behaviour @jberthold-da is observing looks sufficiently buggy/broken, and the case where this would affect anyone (recompiling to a new LF version and then using that as a data-dependency) is sufficiently unlikely.

@remyhaemmerle-da
Copy link
Collaborator

remyhaemmerle-da commented Aug 25, 2020

I agree this behaviour is not ideal, but I do not think we should complexity LF. LF variants are unary, making the argument of a variant optional is almost as complex as making them n-nary.

@sofiafaro-da
Copy link
Contributor

sofiafaro-da commented Aug 25, 2020

Ok, so, after looking into this and talking with @remyhaemmerle-da, it looks like we could change the way we're compiling variants, so that "variant constructors of no input" end up as synthetic variant-record constructors where the record has no fields. We should gate this change by LF version at least, since it's going to affect people who currently use data-dependencies or the ledger api.

(That all said, multi-package projects with access to the original source code should not rely on data-dependencies when possible, since it's at best an approximation of the original types.)

@jberthold-da
Copy link
Contributor Author

(That all said, multi-package projects with access to the original source code should not rely on data-dependencies when possible, since it's at best an approximation of the original types.)

But obviously we need to data-dependencies when data needs to migrate during an upgrade to a newer SDK version, or not?

synthetic variant-record constructors where the record has no fields

But that would still require a trailing {} as an argument to the constructor in the above, or am I missing something?

@sofiafaro-da
Copy link
Contributor

But obviously we need to data-dependencies when data needs to migrate during an upgrade to a newer SDK version, or not?

Yes, you're right. There should be an effort to minimize how much goes in data-dependencies (vs. dependencies), but type definitions usually need to be in data-dependencies, so my point wasn't very relevant. Sorry for bringing it up.

synthetic variant-record constructors where the record has no fields

But that would still require a trailing {} as an argument to the constructor in the above, or am I missing something?

No, I don't think so. When a constructor has no arguments, V2 and V2 {} are the same as far as GHC is concerned. But we can also take care to translate a synthetic variant-record constructor with no fields in DAML-LF back into an empty variant constructor in DAML, so it's not going to be a problem.

As a workaround for now, you can actually use V2 {} for pattern matching, which will ignore all arguments to the constructor. So this should work in both cases:

f : AVariant -> Text
f x = case x of
         V1 i -> "V1"
         V2 {} -> "V2"

@jberthold-da
Copy link
Contributor Author

As a workaround for now, you can actually use V2 {} for pattern matching, which will ignore all arguments to the constructor. So this should work in both cases:
V2 {} -> "V2"

That works for the match, thanks. The problem reaches further, though, I also want to construct values using V2 and am hitting the same issue.
For my use case I can just insert the units (as I know the constructor lives in a different package) and remove that later when the compiler is fixed.

@jberthold-da
Copy link
Contributor Author

Maybe worthwhile to note: The change of arity of this constructor only happens when the original package is depended upon as a data dependency. When using a full dependency the constructor match and value without argument compiles fine.

@sofiafaro-da
Copy link
Contributor

sofiafaro-da commented Sep 1, 2020

Maybe worthwhile to note: The change of arity of this constructor only happens when the original package is depended upon as a data dependency. When using a full dependency the constructor match and value without argument compiles fine.

Yes, this is expected. "dependencies" use GHC's built-in mechanism (interface files) so you have all information from the original package, whereas "data-dependencies" go back from built DALF files, and try to provide a DAML interface to LF code by adding stubs. It is at best an approximation of the original interface, and should, at this point, be thought of as something similar to using the ledger API or language bindings.

Ok, so, after looking into this and talking with @remyhaemmerle-da, it looks like we could change the way we're compiling variants, so that "variant constructors of no input" end up as synthetic variant-record constructors where the record has no fields. We should gate this change by LF version at least, since it's going to affect people who currently use data-dependencies or the ledger api.

I've been implementing this and it looks like a bigger change than I thought, and wouldn't be surprised if it broke a lot of existing code. I would be a lot more comfortable with my first proposed solution -- to change the default way of translating DAML-LF variants with unit constructors back into DAML types, so that you get a constructor with no argument instead of a constructor with a unit argument. We could even deprecate variant constructor with unit arguments from DAML, since they have dubious utility, like we did for record types whose constructor name doesn't match the type name (because it doesn't have a representation at the LF level).

cc @hurryabit

@hurryabit
Copy link
Contributor

I agree with the suggestion to deprecate variant constructors with a single unnamed argument of type (). I would start by showing a warning that explains how you'll get in trouble when using this with data-dependencies and what to use instead.

sofiafaro-da added a commit that referenced this issue Sep 2, 2020
This fixes #7207 and adds a regression test. In a
separate PR I'll add a warning for variants with
single argument of unit type and add a changelog
entry.

changelog_begin
changelog_end
sofiafaro-da added a commit that referenced this issue Sep 2, 2020
* Preserve empty variant constructor in data-deps.

This fixes #7207 and adds a regression test. In a
separate PR I'll add a warning for variants with
single argument of unit type and add a changelog
entry.

changelog_begin
changelog_end

* Add special case for single constructor variants

* Add test for special case
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
language Language team work
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants