> Hi everyone,
>
> Dave and I just whiteboarded ideas for function types, in light of the
> issues that Andrew was seeing. We came to these tentative conclusions:
>
> * Functions being noncopyable doesn't seem to work, because it breaks
> "bind". "bind" must copy the environment of the function bound to.
That wasn't quite the problem I ran into with bind. Right now the rustc doesn't
support rebinding functions anyways, so we can only bind functions with no
environment anyways. So it is a problem, but it is overshadowed by another bug
right now.
The very similar problem that was causing me some grief is that when we bind
alias parameters to functions, we copy the value into the environment.
Functions being non-copyable prevents you from doing useful things like "bind
map(*some_boxed_bound_fn, _)". (This particular issue was causing me grief.
When, as a work-around, I tried to write a version of map that took a boxed
function, I ran into some bind bugs that prevented that from working. I've got
a pull request in for fixes to those.)
> * Blocks (unnamed lambdas) and named functions have different
> properties. The former can capture stack locals, while the latter has
> dynamic lifetime and can capture only shared state. Also, blocks have
> extra control flow options (break, continue, outer return) while named
> functions don't have this ability.
>
*nod*
> * Interior semantics aren't enough to guarantee that unnamed lambdas
> can't escape their defined scope. It'd be possible to swap an interior
> "fn" with another interior "fn" (in some data structure, say) and have
> it escape its scope.
>
*nod*
> * We would like to be able to declare functions that take as one (or
> more) of their arguments either a named function or a block, without
> having to duplicate the function. For example, we should be able to
> say
> map([ 1, 2, 3 ], { |x| x * x }) or map([ 1, 2, 3 ], square).
>
This is the issue that has been probably worrying me the most.
As it stands right now (if you can't pass bare functions), when writing a
higher order function you need to make a choice between taking a box or taking
an alias.
If you choose to take an @fn, then you just can't take blocks, but you can do
whatever you want to with the function.
If you choose to take an &fn, then you can take either blocks or "regular
functions", but callers can't bind a value for that function argument.
Furthermore, you are restricted in what you can do with the function.
Importantly, you can't pass it to any higher order function that takes an @fn.
Also, to pass it a @fn, you need to dereference it, which is a little ugly.
> To deal with these issues, here's a conservative proposal as a
> starting
> point:
>
> (1) The general "fn" type represents a block. It can only appear as
> the
> type of an alias parameter to a function.
>
And the fn type would still need to be non-copyable, right?
> (2) All named functions have type "@fn". They are reference counted.
> They can close over outer variables, but only those on the heap (i.e.
> only over boxes).
>
Is "@fn" intended to be "the @ type constructor applied to a fn type" or is it
intended to be a distinct type?
>From a practical implementation perspective, how do we make sure that LLVM can
>avoid indirect calls to known functions if it is getting them out of the heap?
I think I like having a distinction between the type of "blocks" and the type
of "normal functions".
> (3) As a corollary to point (1), blocks can only be constructed with
> the
> { |x| ... } syntax when calling functions. They are able to use
> "break",
> "continue", and "return" with the semantics that we discussed
> previously.
>
*nod*.
I think that I would then like anonymous function syntax (fn(...) -> ... { ...
}) to stay around but be normal functions and not blocks.
> (4) "bind" stays around, but only works on values of type @fn.
>
And always produces an @fn, presumably? Although in some circumstances we could
produce a stack allocated bound function...
I think this just solves half of the problem with bind, though, since you still
can't bind to an alias function argument.
Although, see below for an idea that might work?
> (5) Named functions (@fn) can be converted to type "fn" when calling a
> function using the dereference operator "*". In the example above, we
> could call "map" using map([ 1, 2, 3 ], *square). Under the hood, an
> extra function will be generated; this is required because blocks need
> a
> different signature than named functions in order to handle "break"
> and
> "return".
The different calling conventions worry me. I wonder if there is a way around
that. I'll ponder that.
What exactly is the current planned scheme for how to handle blocks returning?
I seem to recall it involves returning a value and testing it when calling the
block? Will this need to be explicit in the calling code?
>
> Drawbacks:
>
> * "fn" and "@fn" are now different types; there is no
> compositionality,
> although the dereference operator lessens the pain of this somewhat.
>
> * Functions can't be sent over channels.
>
> We could potentially extend this later to remove some of the
> limitations, but I think this works as a starting point. Andrew, I'm
> particularly curious as to whether this helps solve some of the issues
> you were encountering. To the rest of the team, I'm interested to hear
> your feedback.
>
I think these changes mostly leave me in the same place with the problems I've
been running into. I think it doesn't really change too much about the
difficulty in making interface decisions about whether to take an @fn or an
&fn. Making all named functions @fns will clean up some syntactic noise (by not
requiring you to box them when handing them to something that wants a @fn) and
will introduce some more (by requiring you to deref when passing to something
that takes an &fn).
Part of how nicely things work out depends on what conventions we adopt for
when to use what.
If we decide "we want to be able to support blocks in any cases where it isn't
impossible, so we should always endeavor to take &fn", then it will be clunky.
The common case will be that we are passing @fn to things that take &fn and
we'll need to deref (and generate a wrapper function).
If we decide "make it an @fn unless it is clearly something that we want to be
a block", then it will be less clunky, but we won't be able to use blocks as
often.
One idea to make the first choice more pleasant:
We might be able to pull off a trick where you can bind an @tau to an argument
of type &tau by storing a pointer to the box in the environment.
And then if we added autoderef when passing an @tau to something that takes a
&tau... Hm.
-"Andrew"?
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev