[rust-dev] do

Chris Morgan me at chrismorgan.info
Fri Nov 29 23:02:08 PST 2013


(Do I win the prize for the shortest thread name yet?)

> error: last argument in `do` call has non-procedure type: ||

In the past three or four days I've seen at least as many enquiries in
#rust about this error, and I'm sure there have been at least several
others while I haven't been monitoring it. This is evidently causing
quite a bit of confusion.

Here's a summary of the change. The syntax is the same as it was before::

    do expr { block }
    do expr |args| { block }
    do expr(args) { block }
    do expr(args) |args| { block }

These used to desugar to the following, respectively::

    expr(|| { block })
    expr(|args| { block })
    expr(args, || { block })
    expr(args, |args| { block })

These now desugar to the following, respectively::

    expr(proc() { block })
    expr(proc(args) { block })
    expr(args, proc() { block })
    expr(args, proc(args) { block })

The change is that it now accepts a procedure rather than a closure.
No syntax change, just a semantics change which breaks a lot of code.

Closure: a stack function; used to be ``&fn(..) -> _``, is now ``|..|
-> _``. Can be called multiple times, requires no allocations and is
not Send.

Procedure: a heap function; used to be ``~once fn(..) -> _``, is now
``proc(..) -> _``. Can be called once, requires heap allocation and is
Send.

Procedures are good for sending cross-task; things like the task body
are a good match. Still, I think there are a few problems with how
things are at present (i.e. after the do semantics change):

1. ``do`` is still using the syntax of a closure (``|..| { .. }``),
despite it now being a procedure.

2. All of a sudden, things using closures need to shift away from
using ``do`` or use procedures; this is causing confusion and may
cause bad design decisions where nice sugar triumphs over what is
actually needed; often the best solution may not be clear. (I, for
example, had not thought about the fact that ``proc`` was going to
allocate; the ``~once fn`` name was clearer about that. I'll speak
about ``&once fn`` another time. Don't mention it now, this thread is
just about ``do``.)

I have two solutions that I think could answer these concerns. Leaving
it as it is seems a bad idea to me.

(a) Kill ``do``
---------------

I've had mixed feelings about ``do``. Overall, it's pretty trivial
syntax sugar, but it's sugar of a dubious sort, because it changes
something that looks like a function call with N arguments to be a
function call with N+1 arguments. That's just a matter of learning it.

Still, ``do`` *is* nice sugar in the way it gets rid of parentheses at
the end. Overall, is it worth it? I don't know.

Once ``do`` is gone, there's no problem left: just remove the sugar
*everywhere* it was used and everything works and will do for the
foreseeable future.

(b) Make ``do`` support both closures and procedures
----------------------------------------------------

The syntax of ``do`` can be clearly seen to include the closure
syntax. We could easily extend it to support both closures and
procedures.

Here is a proposed ``do`` using closures once more, keeping the syntax
it had last week::

    do expr || { block }
    do expr(args) || { block }
    do expr |args| { block }
    do expr(args) |args| { block }

Here is a proposed ``do`` using procedures as the current behaviour
is, but with new syntax which is clearly a procedure::

    do expr proc() { block }
    do expr(args) proc() { block }
    do expr proc(args) { block }
    do expr(args) proc(args) { block }

This does leave these cases which are currently valid unclear::

    do expr { block }
    do expr(args) { block }

The options for this are (a) disallowing it; (b) making it always of
the function types; and (c) inferring the type. I generally prefer the
last solution but it is the most difficult. I'm not sure how it all
fits into the function traits stuff at all.

Incidentally, all this leaves the possibility open of making ``do``
work for *any* argument type, where ``do expr1 expr2`` simply desugars
to ``expr1(expr2)`` and ``do expr1(args) expr2`` to ``do expr1(args,
expr2)``. I don't know if that would be a good thing or not; it's
probably best to avoid discussion of that at present.

Summary
=======

Leaving ``do`` in its present form seems to me a distinctly bad idea,
with the syntax of one form of function while it uses another form of
function. I think we need to redo ``do`` very soon. (I'd save this
joke for later in the thread, but I'm afraid someone else might steal
it. I expect all responses to indicate they're in favour of this by
using the title "Re: do" :P.)

For myself, I have no preference to indicate; I am torn between the two options.


More information about the Rust-dev mailing list