asoffer / Icarus

An experimental general-purpose programming language
Apache License 2.0
9 stars 2 forks source link

Scope composition #96

Open asoffer opened 2 years ago

asoffer commented 2 years ago

There seems to be a somewhat common pattern with nesting scopes, for which I think the following example is illustrative:

file.With (filename) open [f: file.File] {
  file.Lines (f) each [line: []char] {
    ProcessLine(line)
  }
}

We've specified f but only use it to pass to the next scope. It would be nice if we could compose these scopes directly.

This proposal is for a non-overloadable operator (tentatively spelled >>) which can connect nested scopes, passing the arguments provided from the outer block directly as arguments to the inner scope. Rewritten, the above code would be:

file.With (filename) open >> file.Lines () each [line: []char] {
  ProcessLine(line)
}

There are a few caveats here:

  1. We need parentheses still between Lines and each to resolve parsing ambiguities. It also would allow us to feed further arguments if Lines had accepted any. So more carefully, the arguments provided from the open block are passed as a prefix of the arguments to Lines, and more can be placed in the parentheses.
  2. If a scope has multiple blocks, this doesn't compose super neatly. There's nowhere to place the file.With error scope anymore. We could place it before open, meaning this mechanism only works for the last block in the scope. This solution feel satisfying to me though.

It's worth mentioning what my Advent Of Code day 2 solution would look like. If we adopted this, and changed the parsing rules slightly to be smarter about newlines (I think this is possible, but I'm not 100% positive), the solution would look like:

file.With ("input.txt") open >> file.Lines () each >> parse_command ()
  forward [n: i64] { horizontal += n }
  down    [n: i64] { vertical += n }
  up      [n: i64] { vertical -= n }

Which I think is particularly elegant.

asoffer commented 2 years ago

It dawn's on me that we already support this when blocks have no arguments (this is how else-if chains work) so the extension is pretty feasible, even without the >> operator.

wrhall commented 2 years ago

If file.With returned a monad error eg optional, you could maybe have a second scope that is unpack_or("...") which would not enter the scope (on that note, file could just do that, right?)

The second scope should be able to forward with >> to a third nested scope

asoffer commented 2 years ago

Could you provide a sample of what this would look like? I'm having trouble understanding.

wrhall commented 2 years ago
file.With ("input.txt") open >> monad.unpack >> file.Lines () each >> parse_command ()
  forward [n: i64] { horizontal += n }
  down    [n: i64] { vertical += n }
  up      [n: i64] { vertical -= n }

where you define Monad.unpack to short-circuit (e.g. like if false) if the optional is None

wrhall commented 2 years ago

This is (afaict) the? from Rust, except it maybe doesn't RETURN_IF (more like BREAK_IF)

asoffer commented 2 years ago

I see. Yes, you could do this, having the monad scope try to unwrap and enter the body if it succeeds and pass over if it fails. However, the file.With scope could do the same. In fact it already does if you don't provide an error handling scope. The question is, what do you do if you really do want a scope for error handling?

wrhall commented 2 years ago

Let me try again to understand the error issue. We're going from this:

file.With (filename) open [f: file.File] {
  file.Lines (f) each [line: []char] {
    ProcessLine(line)
  }
} onError {
  // do something
}

to something like this

file.With (filename) open >> file.Lines () each [line: []char] {
  ProcessLine(line)
}  // where does onError go? This is the close curly brace of file.Lines

Let's remove some sugar, though.

file.With (filename) open >> {
  file.Lines () each [line: []char] {
    ProcessLine(line)
  }
} onError { ... }

That seems ok. We can even compress it back to 3 lines if we want

file.With (filename) open >> { file.Lines () each [line: []char] {
    ProcessLine(line)
} } onError { ... }

How do I feel about this? Effective Icarus now warns you about onError scope placement when using syntax sugar, but other than that ... fine? The real issue is that we probably want something about our onError to redundantly confirm which scope it's holding errors for. And if you do that, you don't need the extra curly braces. And if you don't do that, you've made a bit of a confusing construct.

wrhall commented 2 years ago

All this said, I'm generally in favor of the >> syntax. I think you have lots of options for what this syntax looks like (within the confines of the language), so it doesn't have to be >>. But if that character is free it makes some amount of sense.