ta0kira / zeolite

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

Better support for `optional` types? #206

Closed ta0kira closed 11 months ago

ta0kira commented 11 months ago

Some ideas:

  1. Optionally call a function on optional. Something like what Ruby does? foo&.bar() would call bar() if foo isn't empty. This would require adding optional to bar's return types.

    Other than possibly-confusing semantics of optional optional, it's unclear if this should short-circuit evaluating arguments. In foo&.bar(baz()), would baz() be called if foo is empty? Skipping baz() might be easier to implement in C++, since we could just repeatedly reuse the same ReturnTuple for calls in a single chain.

  2. Optionally overwrite value, with short-circuit evaluation. foo <-| bar() would evaluate bar() and assign the result to foo iff foo is empty. I think it would just expand to:

    if (! `present` foo) {
     foo <- bar()
    }
  3. Implement || for optional types, with the obvious semantics.

    • Unclear how easy it would be to make this short-circuit. Maybe implement it the same way as &.
    • I guess it would default to the left side for type inference? It might be better to skip || and use <-| instead because the state variable determines the type.
ta0kira commented 11 months ago

<-| should have both statement and inline assignment implementations so that something like return (foo <-| bar()) can be done.

ta0kira commented 11 months ago

What would the return type of inline <-| be if the right side isn't optional? It would be easy enough to make it non-optional because that can automatically convert to optional.

optional Int value <- empty
Int value2 <- (value <-| 123)

This isn't unique to <-|, however; we could just as easily apply this to <-.


For inline <- (and not <-|) there's a question of if we'd use the entire type from the right side, e.g.,

optional Formatted value <- empty
Int value2 <- (value <- 123)

Overall, this seems fine, although there could be issues when it comes to unboxed types.


For the return type of inline <-|:

  1. With optional A on the left and B on the right: [A|B].
  2. With optional A on the left and optional B on the right: optional [A|B].
ta0kira commented 11 months ago

I think short-circuiting &. will require a compound expression so that it can be executed inline and it doesn't evaluate the left expression more than once.

({
  BoxedValue result = // execute left side
  BoxedValue::Present(result)
      ? // execute call on result
      : result;
})

Using a lambda would be infeasible due to the complexity of capturing all necessary variables.


Rather than putting that mess everywhere it's needed, it can probably go in a macro in category-source.hpp. Then it would be just a matter of selecting TYPE_VALUE_CALL_UNLESS_EMPTY vs. TypeValue::Call.

#define TYPE_VALUE_CALL_UNLESS_EMPTY(expr, func, args) ({ \
    BoxedValue result = (expr).At(0); \
    BoxedValue::Present(result) \
        ? TypeValue::Call(result, func, args) \
        : result; \
    })
ta0kira commented 11 months ago

I'm going to skip || just because of the type ambiguity.

  1. Use the type on the left?
  2. With optional A on the left and optional B on the right, use optional [A|B]?
  3. What about something like foo() || returnsNothing(), or for that matter foo() && returnsNothing()?
  4. What if the left is optional Bool and the right is Bool? Do we convert the right to optional Bool? For example, in optionalBool || false || something(), if optionalBool is empty, does something() get called`?
ta0kira commented 10 months ago

The proposed functionality for || with optional seems very useful still; it just needs a different operator to avoid ambiguity. I think <|| clearly indicates both "alternative" (||) and preference for the left with <.