Reia: Now with "Magic Rebinding"

1 view
Skip to first unread message

Tony Arcieri

unread,
Dec 14, 2009, 12:01:40 AM12/14/09
to Reia Mailing List

The new "minimalist" branch of Reia now supports "magic rebinding", which allows Ruby-style setters on immutable objects.  The rest of this email comes from the blog post I wrote on the matter:

http://www.unlimitednovelty.com/2009/12/reia-now-with-magic-rebinding.html

In Ruby, thanks to its first class syntax for "hashes" and mutable state, it's quite easy to do:

h = {}
h[key] = value
The equivalent code in Erlang is noisier, thanks to immutable state and single assignment:
Dict1 = dict:new(),
Dict2 = dict:store(Key, Value, Dict1).
Since Reia lacks mutable state, it never before had syntax as simple as Ruby's... but now it does!

I have been trying to hold off on adding syntactic sugar like this to my new "minimalist" branch of Reia. However, this is a feature I meant to add to the old implementation, and tried to retrofit it in long after the implementation had grown quite complex, never managing to succeed. I decided to tackle it now, and I'm happy to announce that it works! Furthermore, it can be used in complex pattern matching expressions:
>> m = {}
=> {}
>> (m[:foo], m[:bar], m[:baz]) = (1,2,3)
=> (1,2,3)
>> m
=> {:bar=>2,:baz=>3,:foo=>1}
So what is going on here exactly? Reia is an immutable state language, so surely I'm not mutating the value that "m" references.

In these cases, Reia is "altering" the local variable binding. Each time you change a member of a map ("hash" for you Ruby folks, "dict" for you Erlang folks), a new version of that map is calculated, then bound to "m". Behind the scenes, the Reia compiler is translating these calls into destructive assignments.

Maps, Tuples, and even Lists now support assignments in this way (although Lists only for consistency's sake... I hate to think of people actually setting values in lists by index). Tuples and Lists even support Ruby-style negative indexing:
>> t = (1,2,3)
=> (1,2,3)
>> t[-1] = 42
=> 42
>> t
=> (1,2,42)
I plan on eventually exposing this functionality to user-defined types as well, in the form of "bang methods" on immutable objects. Users of Ruby are likely familiar with them:
>> arr = [1,2,3]
=> [1,2,3]
>> arr.reverse; arr
=> [1,2,3]
>> arr.reverse!; arr
=> [3,2,1]
Here you can see that calling the "reverse" method on an array (without the !) does not modify the original array in-place. Instead, it returns a new array in reverse order. The complimentary "reverse!" method performs an in-place modification of the array.

The "method!" idiom in Ruby is generally used to indicate methods that modify their receivers as opposed to versions which do not. However this is not a solid requirement, and "!" is often added to any methods considered "dangerous". There's no specific meaning to putting "!" on the end of a method and certainly nothing Ruby does differently at the language level.

In Reia, "bang methods" will be a first class language construct, and will always rebind the receiver with the return value of the method. This will provide a simple way to allow "in place" modifications of immutable objects, by having "bang methods" create and return a new immutable object.

It's the best of both worlds: the ease of use that comes from mutable state, with the concurrency benefits of being completely immutable.

--
Tony Arcieri
Medioh! A Kudelski Brand

candlerb

unread,
Dec 17, 2009, 5:13:18 AM12/17/09
to Reia
Awesome... and only a gnats-whisker away from instance variables,
where the 'State' in a gen_server is a dict.

Aside: I notice tuples are entered using parentheses, but I haven't
found how to make a tuple with one entry yet.

Another aside: for anyone who wants to play, here is a patch which
shows the executed code as actual erlang source.

--- a/src/compiler/reia_bytecode.erl
+++ b/src/compiler/reia_bytecode.erl
@@ -30,7 +30,15 @@ compile(Filename, Expressions) ->
compile(Filename, Expressions, #compile_options{}).

compile(Filename, Expressions, Options) ->
- io:format("Output Code: ~p~n", [Expressions]),
+ %% io:format("Output Code: ~p~n", [Expressions]),
+ lists:foreach(fun(Module) ->
+ case Module of
+ {module,_,_,Functions} ->
+ lists:foreach(fun(Form) ->
+ io:put_chars(erl_pp:form(Form)) end, Functions);
+ _ -> 0
+ end
+ end, Expressions),
case compile_expressions(Filename, Expressions, Options) of
{ok, _Module, Bin} ->
Module = #reia_module{filename=Filename, base_module=Bin},

Example:

>> a = 1
Input Code: [{match,1,{identifier,1,a},{integer,1,1}}]
toplevel({}, nil) ->
__reia_eval_return_value_0 = a_0 = 1,
{__reia_eval_return_value_0,[{a,a_0}]}.
=> 1
>> a += 1
Input Code: [{binary_op,1,'+=',{identifier,1,a},{integer,1,1}}]
toplevel({a_0}, nil) ->
__reia_eval_return_value_0 = a_1 = a_0 + 1,
{__reia_eval_return_value_0,[{a,a_1}]}.
=> 2
>> b = a * a
Input Code: [{match,1,
{identifier,1,b},
{binary_op,1,'*',{identifier,1,a},{identifier,
1,a}}}]
toplevel({a_0}, nil) ->
__reia_eval_return_value_0 = b_0 = a_0 * a_0,
{__reia_eval_return_value_0,[{a,a_0},{b,b_0}]}.
=> 4

candlerb

unread,
Dec 17, 2009, 5:17:08 AM12/17/09
to Reia
Hmm, it looks like the Reia parser treats + and - as right-
associative?

>> 1 - 2 - 3
Input Code: [{binary_op,1,'-',
{integer,1,1},
{binary_op,1,'-',{integer,1,2},{integer,
1,3}}}]
toplevel({a_0,b_0}, nil) ->
__reia_eval_return_value_0 = 1 - (2 - 3),


{__reia_eval_return_value_0,[{a,a_0},{b,b_0}]}.

=> 2

Tony Arcieri

unread,
Dec 17, 2009, 12:27:02 PM12/17/09
to re...@googlegroups.com
On Thu, Dec 17, 2009 at 3:13 AM, candlerb <b.ca...@pobox.com> wrote:
Awesome... and only a gnats-whisker away from instance variables,
where the 'State' in a gen_server is a dict.

Yes, and in fact, this was working in the previous implementation, although the implementation was a bit of a nightmare and required the collusion of 3 different compiler passes.  In the new implementation the implementation will be dramatically simpler, and should require just one pass.
 
Aside: I notice tuples are entered using parentheses, but I haven't
found how to make a tuple with one entry yet.

It's supposed to be the same as Python i.e. (42,) however it appears the grammar for this is broken at the moment.
 
Hmm, it looks like the Reia parser treats + and - as right-
associative?

Yes, the entire grammar is right recursive.  Erlang's is as well:

1> io:parse_erl_exprs('. ').
. 1 - 2 - 3.
{ok,[{op,1,'-',
         {op,1,'-',{integer,1,1},{integer,1,2}},
         {integer,1,3}}],
    2}

Thank immutability and singly linked lists here... you either have to assemble lists right recursively or assemble them in reverse order and flip them around when you're done.  Erlang chose to make the grammar right recursive and "unzip" all pushdowns on the stack.  I just followed Erlang's lead.

Tony Arcieri

unread,
Dec 17, 2009, 12:30:42 PM12/17/09
to re...@googlegroups.com
On Thu, Dec 17, 2009 at 10:27 AM, Tony Arcieri <to...@medioh.com> wrote:
1> io:parse_erl_exprs('. ').
. 1 - 2 - 3.
{ok,[{op,1,'-',
         {op,1,'-',{integer,1,1},{integer,1,2}},
         {integer,1,3}}],
    2}

Err hmm, I was reading that wrong... let me take another look at how Erlang handles this in its grammar.

Thanks for pointing that out.
 

candlerb

unread,
Dec 18, 2009, 4:29:01 AM12/18/09
to Reia
What you need is:

A := A '+' M
| A '-' M
| M

so that 1 - 2 - 3 is parsed as

A
/ | \
A - M
/ | \ |
A - M 3
| |
M 2
|
1

(and this is what erlang's own parser has, see add_expr).

Whether the grammar is left-recursive or right-recursive is a separate
issue. The grammar I wrote at the top is left-recursive, but you can
transform it mechanically into a right-recursive form, which if I
remember correctly is

A := M A2

A2 := '+' M A2
| '-' M A2
| empty

Since there is no left recursion you could parse this new version with
a top-down recursive parser. But an LALR parser generator like yacc/
yecc is quite happy with left-recursive grammars so there is no need
for you to do this.

These grammars build the same parse tree from 1-2-3, whereas A := M
'-' A builds a different parse tree.

Regards,

Brian.

Tony Arcieri

unread,
Dec 19, 2009, 2:20:10 PM12/19/09
to re...@googlegroups.com
On Fri, Dec 18, 2009 at 2:29 AM, candlerb <b.ca...@pobox.com> wrote:
What you need is:

 A := A '+' M
   | A '-' M
   | M

so that 1 - 2 - 3 is parsed as

       A
  /    |    \
 A     -     M
/ | \         |
A - M         3
|   |
M   2
|
1

(and this is what erlang's own parser has, see add_expr).

Check HEAD... all binary operators are now left associative.

Tony Arcieri

unread,
Dec 19, 2009, 2:25:08 PM12/19/09
to re...@googlegroups.com
On Thu, Dec 17, 2009 at 3:13 AM, candlerb <b.ca...@pobox.com> wrote:
Aside: I notice tuples are entered using parentheses, but I haven't
found how to make a tuple with one entry yet.

Handling of single element tuples is now fixed as well:

>> (42,)
=> (42,)

candlerb

unread,
Dec 20, 2009, 5:22:33 PM12/20/09
to Reia
> Handling of single element tuples is now fixed as well:

... plus an astonishing amount of other work in the last couple of
days. Keep up the good work!

A couple of minor observations:

(1)
b = 3 < 4
gives a syntax error. However, both
(b = 3) < 4
and
b = (3 < 4)
are accepted.

(2) You appear to have made matches left-associative (commit
cf2e2a5f), but ruby's assignment operator is right-associative, and as
far as I can see from the Erlang grammar, it is in Erlang too:

expr_100 -> expr_150 '=' expr_100 : {match,?line('$2'),'$1','$3'}.

Regards,

Brian.

Tony Arcieri

unread,
Dec 22, 2009, 12:09:31 AM12/22/09
to re...@googlegroups.com
On Sun, Dec 20, 2009 at 3:22 PM, candlerb <b.ca...@pobox.com> wrote:
... plus an astonishing amount of other work in the last couple of
days. Keep up the good work!

Thanks! 

A couple of minor observations:

(1)
   b = 3 < 4
gives a syntax error. However, both
   (b = 3) < 4
and
   b = (3 < 4)
are accepted.
(2) You appear to have made matches left-associative (commit
cf2e2a5f), but ruby's assignment operator is right-associative

I must've caught that at some point (probably after you pointed out a similar problem with binary operators), but added a lot to the grammar since then, and made a mistake.  See above.

candlerb

unread,
Dec 22, 2009, 3:50:47 AM12/22/09
to Reia
> > (2) You appear to have made matches left-associative (commit
> > cf2e2a5f), but ruby's assignment operator is right-associative
>
> I must've caught that at some point (probably after you pointed out a
> similar problem with binary operators), but added a lot to the grammar since
> then, and made a mistake.  See above.

It's still left associative, but I think it "just works" because of
erlang's match semantics. For example:
a = b = 2

gives
...
{match,1,
{match,1,{var,1,a_0},{var,1,b_0}},
{integer,1,2}}},

That is, (a = b) = 2

But if you make the following change then it's parsed as a = (b = 2)

--- a/src/compiler/reia_parse.yrl
+++ b/src/compiler/reia_parse.yrl
@@ -85,13 +85,13 @@ inline_exprs -> expr : ['$1'].
%% Expressions
expr -> match_expr : '$1'.

-match_expr -> match_expr '=' bool_expr :
+match_expr -> bool_expr '=' match_expr :
#match{
line=?line('$2'),
left='$1',
right='$3'
}.
-match_expr -> match_expr rebind_op bool_expr :
+match_expr -> bool_expr rebind_op match_expr :
#binary_op{
line=?line('$1'),
type=?op('$2'),

It's interesting that even without this patch, an example like
b = 1
a = b += 2
works. Reia is parsing this as

(a = b) = ((a = b) + 2)

which I find rather surprising. With the patch it becomes

a = (b = (b + 2))

Regards,

Brian.

Tony Arcieri

unread,
Dec 23, 2009, 12:00:18 AM12/23/09
to re...@googlegroups.com
On Tue, Dec 22, 2009 at 1:50 AM, candlerb <b.ca...@pobox.com> wrote:
--- a/src/compiler/reia_parse.yrl
+++ b/src/compiler/reia_parse.yrl
@@ -85,13 +85,13 @@ inline_exprs -> expr : ['$1'].
 %% Expressions
 expr -> match_expr : '$1'.

-match_expr -> match_expr '=' bool_expr :
+match_expr -> bool_expr '=' match_expr :
  #match{
    line=?line('$2'),
    left='$1',
    right='$3'
  }.
-match_expr -> match_expr rebind_op bool_expr :
+match_expr -> bool_expr rebind_op match_expr :
  #binary_op{
    line=?line('$1'),
    type=?op('$2'),

Patched!

It's interesting that even without this patch, an example like
   b = 1
   a = b += 2
works. Reia is parsing this as

(a = b) = ((a = b) + 2)

which I find rather surprising.

Indeed... I was quite confused myself.

Reply all
Reply to author
Forward
0 new messages