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

Add test showing that finite hashes are subtypes of generic hashes of the right shape #41

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

ptarjan
Copy link
Contributor

@ptarjan ptarjan commented Sep 13, 2017

I was exploring the subtype relationships between finite hashes and generic hashes and was surprised by the two commented out results here. Should they be true? Or can you please explain to me why they aren't.

@jeffrey-s-foster
Copy link
Contributor

The first one, tt("{a: Integer, b: String}") <= tt("Hash"), fails because there's no rule that allows a parameterized type to be a subtype of a non-paramterized type with the same base. In other words, right now it's not the case that Hash<Symbol, Integer or String> <= Hash. Is this an important case to allow? I don't see a major problem with letting it typecheck it but it might warrant a warning that could be switched off with a configuration option.

The second one, tt("{a: Integer, b: String}") <= tt("Hash<Symbol,%any>"), is not allowed because of mutation. It should be that {a: Integer, b: String} <= Hash<Symbol, Integer or String>. But it's not the case that Hash<Symbol, Integer or String> <= Hash<Symbol, %any> because that would be unsound. For example, simplifying slightly, if x has type Hash<Symbol, Integer> and we let y = x where y has type Hash<Symbol, %any>, then we could set y[:a] = "foo", and now x's type does not match its actual value.

Because of this unsoundness I don't want to generally allow the second case, but this is one of those rules that can be annoying in practice. For example, I believe DART does allow this unsound subtyping, on the theory that it is mostly sound in practice and doing the sound thing gets in the way too much. So if this case is common, I wouldn't be adverse to optionally allowing the unsoundness.

@ptarjan
Copy link
Contributor Author

ptarjan commented Sep 15, 2017

The first one, tt("{a: Integer, b: String}") <= tt("Hash"), fails because there's no rule that allows a parameterized type to be a subtype of a non-paramterized type with the same base. In other words, right now it's not the case that Hash<Symbol, Integer or String> <= Hash. Is this an important case to allow? I don't see a major problem with letting it typecheck it but it might warrant a warning that could be switched off with a configuration option.

I'm not sure how important it is since right now I'm doing a pre-transform step from our runtime type system into RDL's string language so I can apply whatever transform at that point. It was pretty surprising to me that it didn't work that way, so to work with the principal of least surprise I'd probably suggest to make that work. Is it unsound? What would the warning be? Do you already have a warning framework?

The second one, tt("{a: Integer, b: String}") <= tt("Hash<Symbol,%any>"), is not allowed because of mutation. It should be that {a: Integer, b: String} <= Hash<Symbol, Integer or String>. But it's not the case that Hash<Symbol, Integer or String> <= Hash<Symbol, %any> because that would be unsound. For example, simplifying slightly, if x has type Hash<Symbol, Integer> and we let y = x where y has type Hash<Symbol, %any>, then we could set y[:a] = "foo", and now x's type does not match its actual value.

Because of this unsoundness I don't want to generally allow the second case, but this is one of those rules that can be annoying in practice. For example, I believe DART does allow this unsound subtyping, on the theory that it is mostly sound in practice and doing the sound thing gets in the way too much. So if this case is common, I wouldn't be adverse to optionally allowing the unsoundness.

Yeah, in the face of untyped code we think that we might have to make this unsoundness compromise. We're struggling with what happens when you do:

type 'Array<Integer> -> Array<Integer>'
def my_function(an_array)
  an_array.map {|element| some_untyped_function(element)}
end

We're unsure if the right way is to make the sub typing unsound or to introduce a %dynamic which can have anything done to it. I think the dynamic approach is what Hack, Flow and mypy do but it does introduce unsoundness in presence of gererics.

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.

2 participants