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

Contract (and maybe Label) type #2167

Open
jneem opened this issue Feb 12, 2025 · 1 comment
Open

Contract (and maybe Label) type #2167

jneem opened this issue Feb 12, 2025 · 1 comment

Comments

@jneem
Copy link
Member

jneem commented Feb 12, 2025

I had a try at refactoring json-schema-to-nickel's contract library as combinators on eager contracts (as opposed to passing around functions and turning them into contracts at the end). It works ok, but the static typing experience is somewhat worse than the current situation: we don't have types for contracts or labels and so almost everything just ends up having the type Dyn -> Dyn -> Dyn.

What if we built-in an opaque Contract type? Then you could have any_of: [Contract] -> Contract? Also, having a Label type would allow std.contract.custom: (Label -> Dyn -> [| ... |]) -> Contract which I think is nicer than the current situation.

Backwards compatibility is one possible problem with this suggestion; I'm not sure that the standard library could be updated like I suggested above.

@yannham
Copy link
Member

yannham commented Feb 12, 2025

One issue with adding built-in types, even without using them in the stdlib, is indeed backward compatibility: if we add those types as new keywords, then anyone who wrote let Contract = { .. } will be affected. One strategy would be to wait for let-types to make some progress, but the design isn't well developed. A second source of backward incompatibility is that the new type of any_of is not a subtype of the current type of any_of, which I guess is the backward incompatibility you're thinking of.

It also means that now anyone who uses any_of even in an untyped environment will pay for additional checks. For example std.contract.any_of [Number, String], we'll check that Number and String are indeed proper contracts. Same for contract.check or contract.apply if we type them. In fact the cost isn't necessarily a problem - many Nickel stdlib function applies contracts everywhere, but this adds another technical difficulty for error reporting: if the contract fails the Contract contract, things start to get messy 😅 I remember a paper being precisely about that. The solution wasn't necessarily complicated, I think it amounted to add a new bit in the label to indicate if you're in contract code or not (and then contract code might trigger "user" code when they evaluate their argument, so one must be careful when defining the transitions).

In general, when adding a new type, we need to answer the following questions:

  • What would be the runtime contract corresponding to Contract? That is, what do we check at runtime to ensure that something is a contract? Currently a contract can be a record, something built with contract.custom or also naked functions, at least for the time being. On the surface it might be handled ok as a lazyness-preserving dynamic check.
  • What are the typing rules? I guess anything build with from_predicate, from_validator and custom is of type Contract. What about records? If we want to type std.contract.any_of [{foo}, {bar}], we need some subtyping rule saying that {; a} <: Contract, which is probably ok in the current state of the typechecker. Similarly, it's possible to accomodate naked functions Dyn -> Dyn -> [| ... |], but the nature of the return type with a contract is going to make that harder. On the other hand, you could say that in a statically typed block, we require the use of a contract constructor and reject naked functions. After all, naked functions as contracts are just here for backward compatibility.

All in all, it would be a step in the right direction and align well with the rest of the Nickel philosophy IMHO but wasn't implemented from the start because of the several design questions mentioned above, though none of which is a hard blocker. I wonder if that should be a 2.0 feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants