-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Specify enhanced control structures #27
base: spec-gold
Are you sure you want to change the base?
Conversation
effectively tail-recursion: :: | ||
|
||
fun factorial(n: u16) -> u16 | ||
let acc: u16, k: u16 = 1, n do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this is missing a mut
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it is, but I might be wrong. All that "immutable" means is that "you can take a pointer to this, and as long as it's still valid, the result of dereferencing that pointer won't change".
In the factorial example, this is actually true; if you take a pointer to, say, acc
, then you must be taking the pointer inside the let-do-block. However, that pointer is only valid to the end of the scope. The redo
statement has a subtlety, here -- since it ends the block, it closes the scope, but until the scope is closed, the bindings keep the same value. It then re-starts the block, and re-opens the scope, this time with the new memory values. In other words, calling redo
necessarily invalidates the pointer.
fun foo(n: u16) -> u16
let k: u16, i: u16 = n, n do
let j: &u16 = &k
if k > 1 then
/* this is fine */
redo (i - 1, *j - 1)
else
return k
end
end
end
Notice above--the pointer j
is actually valid until the next statement executed after the redo
statement, and can be used safely! The memory used by k
isn't changed until then -- in fact, k
isn't even rebound until the redo
statement completes. The fact that we re-use the memory for k
and i
should be an implementation detail; by the time we re-use it, you no longer have normal access to it anyway.
I agree that this is a little confusing at first, but I don't want to make the use of redo
require you to smear gratuitous mut
s everywhere; in fact, I kind of see it as an alternative to mut
. It gives you two options for an algorithm:
- Implement the algorithm iteratively, explicitly marking the memory that's to be re-used. Memory to be re-used can be changed arbitrarily.
- Implement the algorithm tail-recursively, allowing the compiler to automatically re-use memory based on scope. Memory to be re-used can only be changed per-iteration.
Which option is clearer depends on the algorithm and the person writing it, of course, though I personally feel like option 2 really is the better option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i see. this is starting to explain some of the advantages to your enhanced redo. i'm still struggling with it, but i'm going to let it sit for a while to be sure my aversion isn't "this is different!" but rather "this could be better".
i am definitely on board with bare do blocks, named loops, redo statements. for named loops, it feels a little awkward to have the name on the right side of the "do"... once i see "do", i expect the next thing to be a statement. and i think it's reasonable to expect that. i'm also worried that our use of colons is losing any sense of consistency. lemme think on that, but i'm not sure if i can do better than the options you've already suggested. for multiple-binding let statements, i just feel that the explanation you provided in the spec document is muddled. you've tried to define a basic let statement, a multiple-binding let statement, and a let statement bound to a loop's scope all at once and i think it wasn't clear.
i think this means "assign the value also, is this allowed?
does it mean both values get assigned a value of zero? or that can a multiple-binding let statement contain bindings of multiple different types? as for this concept of let statements being bound to a scope in a new way, i'm confused. it seems to be only useful for the redo statement. my hesitation is mostly this:
these bindings are both scoped within the loop, but only because of a single comma! they certainly don't look like they belong to the inner loop. if i'm reading this, i might put a this is not the whitespace style you used in your document, but i think it is the natural syntax final summary if i've correctly understood your motivation for multiple-binding let statements, scoping a let statement to a loop in a new way, and allowing expressions in a redo statement, it's basically to support this tail recursion example you've provided:
however, if i'm correctly understanding what these new features are meant to do, here is the same code written without multiple-binding let statements, let statements scoped to a loop in a new way, or expressions in a redo statement.
your example saved a few keystrokes, but isn't more optimized than mine and i argue that it's less readable than mine for three reasons:
|
I agree with this 100% -- colons should only be used for notating types. Of the options I've given, I like Alternative 1 best, with
It is. Maybe I should define the combined syntax, then provide examples of each sub-part?
Yeah. I think my thought process was that it allows for nice stuff later like
My inclination is the say "no", but I guess allowing uninitialized declarations that can't be read from until they've been assigned could be useful. C# allows it, but Rust does it better by making control structures into expressions. Maybe "no" for now, then relax it later if it seems useful?
Yes!
That's true. If all you wanted to do was restrict the scope of some variables, you could just throw them in a bare
More proof that my description is muddy, since the above code snippet is a syntax error the way I intended the spec to be read. It should be this instead (if we're allowing C-style multiple assignment as discussed above):
If we're not allowing C-style multiple assignment (i.e. PR-as-reviewed), it would be one of the following:
My motivation is kind of mixed. On the one hand, the features given are really useful as internal compiler constructs anyway, and while one could do without them, they are handy if you want to implement a naturally-tail-recursive algorithm tail (which, admittedly, factorial is not). When it comes down to it, anything that you can express tail-recursively can be expressed iteratively as well -- this is the entire point of tail recursion. I think that support for recursion is important. That's a hard sell on the C64, but I like to think that this captures a lot of the parts that one actually wants. If you have an algorithm that's naturally tail-recursive, you can express it. If you don't, then you can express it either tail recursively or iteratively.
This criticism applies equally to function calls. At least the |
As a side note, if you REALLY wanted to implement
|
Yet another alternative syntax for named loops, riffing on the idea that redo is similar to a tail-recursive function call.
|
what about this? kinda similar to function declarations.
examples:
|
Specifically, this provides the following:
do
blocks. This is equivalent to the ability to just put a{ ... }
somewhere in C-like languages, which can be quite useful. Easy to implement.let
blocks with multiple bindings. Not earth-shaking, but plays well with theredo
feature.let
blocks with an attached scope. Easy to implement (sincelet
statements have an implicit scope anyway), and another feature which plays nice with theredo
feature.redo
statement.The redo statement is a little weird, and is a combination of a Scheme feature and a Perl5 feature. In Perl5, the
redo
statement is the missing partner tobreak
andcontinue
(ornext
in Perl), in that it allows you to rerun the current loop iteration without re-evaluating the condition. It's equivalent to something like:which for us would be
On the Scheme side, you can label your
let
forms. A Schemelet
is actually equivalent to creating and immediately calling an anonymous closure, and labelling alet
just gives it a name. You can then express tail-recursive looping constructs by calling the given name, and it lets you do tail-recursion without having to provide an additional helper function with an accumulator.Our
redo
would combine those features, with the caveat that it must be tail-recursive (since regular recursion on the C64 would be a disaster). It's actually somewhat cheaper than a function call. In addition, the compiler can use it internally to express loops; it's essentially something that we'd be implementing anyway, and it's so useful that we might as well expose it to the user.I think I'd like, if possible, to avoid having to introduce a
goto
statement into Gold-syntax, andredo
is a step towards providing a facility which provides a lot of its power in a structured, safe fashion.I'm not sure about the
do: foo ... end
syntax for naming blocks, since:
normally means that we're specifying a type. It's a kind of binding, so it feels like=
should be involved, so here's my two alternate syntaxes for labelleddo
,let-do
, andwhile-do
respectively. Note that baredo
is dropped in favour of an emptylet
binding in both: