ta0kira / zeolite

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

Look into the feasibility of anonymous structures, as a generalization of lambdas. #6

Closed ta0kira closed 2 years ago

ta0kira commented 4 years ago

Possible syntax:

The syntax will be easy; designing closure semantics will be the difficult part.

ta0kira commented 4 years ago

Also see #9 regarding tracing.

ta0kira commented 4 years ago

It might be cleaner to make function types a separate ValueType constructor so that they can't be used in parameter substitution. This will also avoid having to update the formalization of the type system to treat function types as type instances. (Also, the idea of a function type in a union is dubious.)

I'd also like to avoid lambdas for a few reasons:

At worst, a private function could be created and bound to self.

Parsing of infix functions actually provides most of what's needed, as far as parsing function-passing. Additionally, ValueOperation would need a new constructor for () without a function name.

There will also be an issue of passing call<#x> vs. call with unsubstituted param #x. For example, should [<#x> #x defines Equals<#x> (#x) -> (#x)] be a valid function type?

ta0kira commented 4 years ago

Also, what about currying params or args? The former could be done with call<Foo,_>, but the latter sounds like a syntactic mess.

Separately, what would call<?> mean (with type-inference)? That's currently valid for infix.

ta0kira commented 3 years ago

I've mostly forgotten about this, which means it hasn't been a limitation yet. I'll reopen this if/when it becomes a priority. Rather than doing the "usual" function object and lambda thing, there might be a better solution with anonymous structures.

ta0kira commented 3 years ago

I can't tell if allowing function references (bound or not) would require specifying variances for function params. Probably not, since you wouldn't need the param to check conversion of the function type.

For example:

call<#x> (#x) -> (#x)

f <- call<Foo>
g <- f  // assume g needs (Bar) -> (Baz)

The param #x is irrelevant after Foo gets bound; it just turns (#x) -> (#x) into (Foo) -> (Foo).

I think what makes this different from type-level params is that the latter represents usage in a collection of functions that need to be kept as a collection using the containing type.

ta0kira commented 3 years ago

Thinking about this a bit more, it seems like anonymous structures is the way to go, since it's like an OOP analog to lambdas.

ta0kira commented 3 years ago

A few issues that might come up with anonymous structs:

  1. What will the type be? The intersection of all of its refiness? What type is #self? (That might be easy, since #self just inherits all refines and defines, plus the current category.)

  2. All functions will need to be @value, which means no defines. How will the compiler resolve function calls? In particular, something like self.foo(); what type is self here? What about internal functions not from refines? (Note that dispatching of the latter won't be a problem because compileFunctionCall already bypasses TypeValue::Call for calls to self.)

  3. How will params and values be captured?

    // Just use <> as a way to capture #x and #y?
    // Inline use of {} to initialize members?
    \ callWithFoo(anonymous<#x,#y>{ value } {
         refines Foo<#x>
    
         @value Value value
    
         foo () {
           \ Bar:baz<#y>()
         }
       })
  4. What if the contract for an inherited function requires making a copy of the object? Should #self{ ... } be allowed? (anonymous<#x,#y>{ ... } would imply a separate type, if that's the syntax we're going to use.)

  5. Do param filters need to be reiterated? If so, they will need to be checked when the params are captured.

  6. How will the C++ classes be named? They will need to avoid name clashes if a single category uses more than one anonymous. Maybe something like Anonymous_100_19, appending line and column numbers. Will they live in the .cpp for the category that uses them? (Probably, since we won't be generating anything in the usual Category_Foo.hpp header.) We also need to avoid link-time clashes if separate .0rxs happen to use anonymous on the same line/column when defining public categories.

  7. Coincidentally, the compiler already supports @value-scoped params, from the recently-removed (#158) internal-params feature. This should make it easy to refer to params that aren't in parent when compiling procedures.

  8. The parsed representation will need some functionality of both AnyCategory c and DefinedCategory c. This implies that it should be parsed as a (AnyCategory c, DefinedCategory c), adding one new constructor to each.

ta0kira commented 3 years ago

Regarding parsing, I just realized that the (AnyCategory c, DefinedCategory c) will be parsed within the respective parent DefinedCategory c.

It might therefore make sense to do this:

  1. Create a new DefinedCategory constructor for the "defined" part of anonymous.
  2. Create a new AnyCategory constructor for the "declared" part of anonymous.
  3. Add an Expression constructor to init a specific anonymous struct, which will contain an "init" section as well as one each of AnyCategory c and DefinedCategory c.
  4. When compiling the DefinedCategory in generateCategoryDefinition, recursively extract the (AnyCategory c, DefinedCategory c) (recursive because an anonymous could contain another anonymous) and generate the respective code within the same .cpp. (Keep in mind that the order will matter in the .cpp.)
ta0kira commented 2 years ago

Another consideration is anonymous @types that will only be passed as params:

\ callWithFoo<anonymous {
      refines Foo
      defines Factory<Foo>

      new () {
        return #self{ }
      }

      foo () {
        // ...
      }
    }>()

This syntax is quite confusing and awkward. As far as the definition itself, the main syntactic difference vs. a @value is the missing initializer {} after anonymous.


On the topic of capturing params, how would we deal with params included in filters that haven't been explicitly captured?

concrete Foo<#x> {
  @type foo<#y,#z>
    #y requires Bar<#z>
    #z requires Bar<#x>
  () -> ()
}

define Foo {
  foo () {
    // #y depends on #z, which depends on #x.
    \ call(anonymous<#y> ...  )
  }
}
ta0kira commented 2 years ago

I think anonymous structures will be too much of a mess, both in terms of compiler implementation and in terms of code to use it in Zeolite programs.

It might be useful to support automatic encapsulation of a named function in a @value interface (the way Java does), but I think that could also be a mess, e.g., when dealing with function params.


I'm going to close this again. I'll reopen it if I actually end up in a situation where this is going to save a lot of effort.