I was told that Rust has a semantics in affine logic -- so one has deletion/weakening but not duplication/contraction.
The following compiles:
fn throw_away<A, B>(x: A, _y: B) -> A {
x
}
Because duplication is disallowed, the following does not compile:
fn dup<A>(x: A) -> (A, A) {
(x, x)
}
Similarly, neither of these compile:
fn throw_away3<A, B>(x: A, f: fn(A) -> B) -> A {
x;
f(x)
}
fn throw_away4<A, B>(x: A, f: fn(A) -> B) -> A {
throw_away(x, f(x))
}
Weakening is also witnessable
fn weaken<A, B, C>(f: fn(A) -> B) -> impl Fn(A, C) -> B {
move |x: A, y: C| f(x)
}
Instead of returning fn(A, C) -> B
, we returned impl Fn(A, C) -> B
. Is there a way to return fn(A, C) -> B
instead? It's fine if not; I'm just curious.
Something else I expect is that you can lift A
to () -> A
. However, functions in Rust can be duplicated and used more than once. For example,
fn app_twice(f: fn(A) -> A, x: A) -> A {
f(f(x))
}
Suppose there was actually a function lift(x: A) -> fn() -> A
, then we could break the move semantics. For example, this would allow
fn dup_allowed(x: A) -> (A, A) {
let h = lift(x);
(h(), h())
}
Thus to lift A
to fn() -> A
, we need to know that the function is "linear/affine" or can be used only once. Rust provides a type for this: FnOnce() -> A
. In the following, the first compiles, and the second does not.
fn app_once(f: impl FnOnce(A) -> A, x: A) -> A {
f(x)
}
fn app_twice2(f: impl FnOnce(A) -> A, x: A) -> A {
f(f(x))
}
The following functions are inverses of each other (probably, I don't know Rust's semantics well enough to say that they are actually inverse to each other):
fn lift_up<A>(x: A) -> impl FnOnce() -> A {
move || x
}
fn lift_up_r<A>(f: impl FnOnce() -> A) -> A {
f()
}
Since fn dup<A>(x: A) -> (A, A) { (x,x) }
does not compile, I thought that the following might be a problem:
fn dup<A>(x: fn() -> A) -> (A, A) {
(x(), x())
}
It seems that Rust is doing something special for fn(A) -> B
types.
Why don't I have to declare that x is reusable/duplicable in the above?
Perhaps something different is going on. Declared functions are a bit special fn f(x: A) -> B { ... }
is a particular witness that A -> B
. Thus if f
needs to be used multiple times, it can be reproved as many times as needed, but fn(A) -> B
is a completely different thing: it is not a constructed thing but a hypothetical thing, and must be using that fn(A) -> B
s are duplicatable. In fact, I've been thinking that it's more like a freely duplicable entity. Here's my rough analogy:
fn my_fun<A,B>(x :A) -> B { M }
"is" x:A |- M:Bfn(A) -> B
"is" !(A -o B) hence freely duplicable- Thus
fn() -> A
"is" !(() -o A) = !A hencefn () -> A
is the (co)free duplication on A fn dup_arg<A: Copy>(x: A) -> B { M }
"says" that A has duplication or is a comonoidimpl FnOnce (A) -> B
"is" A -o B
But this can't be right... For what is impl Fn(A) -> B
? From playing around a bit, it seems that fn(A) -> B
is more strict than Fn(A) -> B
. What am I missing?
No, because a
fn
is by definition not a closure: it cannot contain any state that wasn't compiled into the program (in this case, the value off
). This is closely related to your next observation: because afn
cannot close over anything, it trivially cannot contain any non-Copy
types and therefore can always be called multiple times, or itself copied, without violating the properties we're discussing.Precisely: all
fn(..) -> _
types implementFn
andCopy
(as well asFnOnce
).Copy
is the marker trait ('marker' meaning it provides no methods) that has the special purpose of telling the compiler that it is free to copy the bits of a type automatically whenever it is used more than once. Anything implementingCopy
is opting out of the move-but-not-copy system -- but can't thereby violate the non-Copy-ness of a different type.Fn
is the trait for functions that can be called by immutable reference (not modifying or consuming the function itself). This is in principle separate fromCopy
, but it's very similar in effect; the differences that one could end up with (some of these can't happen in ordinary code) are:Fn
but notCopy
orClone
, then you can't store the function multiple places but you can call it as many times as you want.Copy
but notFn
(onlyFnOnce
), then this is invisible because every call of it (except for the last) implicitly copies it.Clone
but notFn
orCopy
, then you would have to.clone()
it each time you called it (except the last).lift_up_r
accepts functions thatlift_up
did not produce; for example, iff
has a side effect, panics, or hangs thenlet f = lift_up(lift_up_r(f));
has that effect. Ignoring that, they are inverses. An even better pair of inverses without that caveat would be functions for moving a value into astruct
and back out -- which this is effectively doing, except for allowing inputs that aren't of that particular struct type.When you have a generic function with a type variable,
fn dup<A>
, the compiler makes no assumptions about the properties ofA
(except that it isSized
unless you opt out of that implicit bound, because working with non-Sized
values is highly restrictive and usually not what you want). In particular, it does not assume thatA
implementsCopy
.On the other hand, as I mentioned above, all
fn
types implementFn
andCopy
, so they can always be duplicated and reused.The way to write a
dup
function which operates on general functions and fails to compile in the way you expect is:Here, we tell the compiler that
F
is a type of function which is consumed by calling it, and don't tell it about any way to duplicateF
. So, it fails to compile with "error[E0382]: use of moved value:x
". The shortest way to make this compile would be to add the boundF: Copy
, and the most general would be to addF: Clone
and an explicit.clone()
call.I'm no logician, but I think that the first half of this is not correct. In particular, (outside of some irrelevant considerations about generics) there are no properties that "a declared function" has that an arbitrary value of type
fn(A) -> B
does not have. Rather, the value of typefn(A) -> B
can be copied, and that copiability corresponds directly to the fact that "it can be reproved", because (until we start introducing ideas like JIT code generation) every value of typefn(A) -> B
refers to a compiled piece of code (and no other data) -- and hence a lemma that the compiler has checked and given the program license to reuse it as many times as needed at run time.The
impl
syntax serves different roles, but in argument position it is almost exactly a shorthand for generics. If I writethen that is equivalent to
except that the caller is not allowed to specify any of the parameters when any
impl
argument types exist (this is not relevant to your interests but I mention it for accuracy). Thus, we're telling the compiler thatF
can be anything as long as it is callable as a reusable function. In particular, we're not specifyingF: Copy
orF: Clone
.fn(A) -> B
, on the other hand, is a concrete type which implementsFn(A) -> B
andCopy
, so you get that for free.In return position,
fn ... -> impl Fn(A) -> B
, theimpl
denotes an existential type: you're asserting that there exists some type implementingFn
which the function will return. The compiler tracks the concrete type in order to generate code, but your program avoids naming it. This is necessary when returning a closure, but optional when returning a function that does not close over anything: for example, you can write