brownplt / pyret-lang

The Pyret language.
Other
1.07k stars 111 forks source link

Extending methods defined for constructors results in a match exception later on #1625

Open hyphenrf opened 3 years ago

hyphenrf commented 3 years ago

I've been trying different ways to extend values produced by constructors but I can't seem to make it work.. My latest attempt with a function that should produce a subtype:

import sets as sets

shadow set = sets.tree-set

fun set-op(s):
  s.{
    method _plus(self, other):  self.union(other) end,
    method _times(self, other): self.intersect(other) end,
    method _minus(self, other): self.difference(other) end
  }
where:
  l = [set: 1, 2, 3] ^ set-op
  r = [set: 2, 3, 4]

  l + r is [set: 1, 2, 3, 4]
  l * r is [set: 2, 3]
  l - r is [set: 1]
end

produces a match exception instead.

jpolitz commented 3 years ago

Thanks for asking. This has been a longstanding issue where we aren't sure what we want. For example:

https://github.com/brownplt/pyret-lang/issues/307 https://groups.google.com/g/pyret-discuss/c/FUb96Hnobuw/m/iH_Vh6UIDFgJ

The weird output in that issue is just a symptom of a few problematic cases that revolve around what guarantees one ought to expect from datatype-ness.

There are two important cases to consider:

  1. Adding new fields (what you're doing here)
  2. Adding fields that already exist

Both cases are fairly fraught for datatypes.

The second is bad dynamically because there is a lot of annotation checking that would need to happen to re-bless the value as a member of the datatype after extension. Statically it's a bit easier to check, though if the programmer's intent was to have smart constructors for a datatype that excluded certain constructions, this could allow synthesizing values of that datatype with functional extension that the author of the smart constructor didn't intend.

The first is tricky for a combination of static and dynamic reasons, but mostly static reasons. Statically, the type-checker tracks the Set datatype including all of its fields and methods, and that's what it means to be a Set. Once it gets extended, it's some new type, which is Set plus stuff. We don't have a concrete syntax for that type (in part because expressing that type is not obvious to do). So all the existing code that uses Set could be made to work with the extensions, but we don't have a way to type-check (or really assign a good dynamic type to) the resulting value.

I think we should probably just make the case you're trying be an error directly rather than letting these Frankenvalues float around. At the moment there's just more design work to do in both cases, and I think until we resolve it this is just a not-so-great corner of the language to play with.

Of course, functional extension is pretty great with plain record types, and we use it all over the place in the compiler. It could also benefit from some type-level concatenation of records (the missing operator for handling the adding-new-fields case), and that's to-be-designed.

I hope that's a useful response – I would be happy to chat more about what needs to happen next here from a design and implementation standpoint to make this work in a more satisfying way.