ta0kira / zeolite

Zeolite is a statically-typed, general-purpose programming language.
Apache License 2.0
18 stars 0 forks source link

Add a covariant `#self` meta-type. #129

Closed ta0kira closed 3 years ago

ta0kira commented 3 years ago

The overall goal would be to have a meta-type that an interface can use to denote that the derived class is always required.

The main use-case would be for things like iterators and builders, which always have at least one function that returns an object of the object's own type. Although these semantics are possible without a language change (see below), it would be inconvenient enough for people to not want to use it.


I think the semantics would be similar to this:

@value interface ForwardIterator<|#self,#x> {
  #self refines ForwardIterator<#self,#x>

  next () -> (#self)
  get () -> (#x)
}

@value interface ReverseIterator<|#self,#x> {
  #self refines ReverseIterator<#self,#x>

  prev () -> (#self)
  get () -> (#x)
}

concrete BidirectionalIterator<|#x> {
  refines ForwardIterator<BidirectionalIterator<#x>,#x>
  refines ReverseIterator<BidirectionalIterator<#x>,#x>

  // next *must* return BidirectionalIterator<#x>
  // prev *must* return BidirectionalIterator<#x>
}

The main difference is that #self would automatically be assigned to the class doing the refines or defines.


Potential issues:


It seems like we can just parse it as a normal param, then do a dynamic substitution whenever reasoning about types is needed.


This also raises an interesting question about parameters-as-variable semantics: Should type declarations be treated as functions and type substitutions as function calls? (That is, from the perspective of compilation.)

ta0kira commented 3 years ago

It could also be a bit of work to include this in the formalization of the type system.

ta0kira commented 3 years ago

This looks like a problem, but it might not be:

@value interface Foo {
  bar () -> (#self)
}

concrete Something {
  @value call<#x>
    #x allows Foo  // <-- what is #self in Foo.bar?
  () -> (#x)
}

This is probably a non-issue because you can't call Foo.bar on #x without #x requires Foo.


Separately, with something like #x defines Foo where Foo.bar uses #self, there will need to be a step where #self is replaced with #x when a call to #x.bar is resolved within a procedure. This can likely be done in ccGetTypeFunction.

Actually, I don't know if param filters are special here; I think we just need to replace #self with whatever the call is being bound to, i.e., the type of the value for @value functions or the type instance for @type functions.

ta0kira commented 3 years ago

Static replacements needed:


It seems like there should be an issue using #self in @value interface since such interfaces can't be used as @value types; however, it isn't an issue, because such functions will always be called from something that can be a @value type.

ta0kira commented 3 years ago

Static replacement is also needed in reduce and typename calls.

ta0kira commented 3 years ago

Something I inadvertently accounted for in the solution is when #self is an intersection type:

@value interface Type1 {
  call () -> (#self)
}

@value interface Type2 {}

concrete Type3 {
  refines Type1
  refines Type2

  @type create () -> (Type3)
}

// ...

[Type1&Type2] value <- Type3.create()
value <- value.call()