I've just started Rust tutorial and ended with such code using recursion
extern crate rand;
use std::io;
use rand::Rng;
use std::cmp::Ordering;
use std::str::FromStr;
use std::fmt::{Display, Debug};
fn try_guess<T: Ord>(guess: T, actual: T) -> bool {
match guess.cmp(&actual) {
Ordering::Less => {
println!("Too small");
false
}
Ordering::Greater => {
println!("Too big");
false
}
Ordering::Equal => {
println!("You win!");
true
}
}
}
fn guess_loop<T: Ord + FromStr + Display + Copy>(actual: T)
where <T as FromStr>::Err: Debug
{
println!("PLease input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess_int: T = guess.trim()
.parse()
.expect("Should enter integer number");
println!("You guessed {} !", guess_int);
if !try_guess(guess_int, actual) {
guess_loop(actual)
}
}
fn main() {
println!("Guess the number!!!");
let secret_number = rand::thread_rng().gen_range(1, 51);
guess_loop(secret_number);
}
I was hoping to factor-out the recursion from the guess_loop
function and introduced a fix point operator:
fn guess_loop<T: Ord + FromStr + Display + Copy>(actual: T, recur: fn(T) -> ()) -> ()
where <T as FromStr>::Err: Debug
{
println!("PLease input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess_int: T = guess.trim()
.parse()
.expect("Should enter integer number");
println!("You guessed {} !", guess_int);
if !try_guess(guess_int, actual) {
recur(actual)
}
}
fn fix<T, R>(func: fn(T, fn(T) -> R) -> R) -> fn(T) -> R {
fn fixed(val: T) -> R {
func(val, fixed)
}
fixed
}
fn main() {
println!("Guess the number!!!");
let secret_number = rand::thread_rng().gen_range(1, 51);
fix(guess_loop)(secret_number);
}
but this led to numerous errors, such as
error[E0401]: can't use type parameters from outer function; try using a local type parameter instead
--> src/main.rs:49:19
|
49 | fn fixed(val: T) -> R {
| ^ use of type variable from outer function
error[E0401]: can't use type parameters from outer function; try using a local type parameter instead
--> src/main.rs:49:25
|
49 | fn fixed(val: T) -> R {
| ^ use of type variable from outer function
error[E0434]: can't capture dynamic environment in a fn item; use the || { ... } closure form instead
--> src/main.rs:50:9
|
50 | func(val, fixed)
| ^^^^
My next attempt was changing guess_loop
's definition to
fn guess_loop<T: Ord + FromStr + Display + Copy, F>(actual: T, recur: F) -> ()
where <T as FromStr>::Err: Debug,
F: Fn(T) -> ()
{ ... }
and redefine fix
as
fn fix<T, R, F>(func: fn(T, F) -> R) -> F
where F: Fn(T) -> R
{
let fixed = |val: T| func(val, fix(func));
fixed
}
this led to
error[E0308]: mismatched types
--> src/main.rs:53:5
|
53 | fixed
| ^^^^^ expected type parameter, found closure
|
= note: expected type `F`
= note: found type `[closure@src/main.rs:52:17: 52:46 func:_]`
error: the type of this value must be known in this context
--> src/main.rs:61:5
|
61 | fix(guess_loop)(secret_number);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
How can I write a similar fix
function?
This is an answer to my own question about implementing the Y combinator which is a subset of this question. In pure lambda expression, a version of the Y combinator looks like
The solution in Rosetta Code is too complicated and used
Box
to allocate memory in the heap. I want to simplify this.First, let's implement the type
Mu<T>
as a trait instead.Note that we need this trait to be object safe, which means we cannot ask for
Self
in any of its definition so the second parameter is typed&Mu<T>
and it is a trait object.Now we can write a generic
trait
implementation:With this, we can now write the y combinator as the following:
The above compiles in the Rust playground without enabling any features and using only the stable channel so this is a pretty good answer to my question.
However, the above would not work in practice because Rust is call-by-value but the code above is the call-by-name Y combinator.
The call-by-value solution
To work with the stable channel without requiring any features, we cannot return closures (which requires
impl Trait
). Instead, I came up with making anotherMu2
type that takes two type parameters:As above, let's implement this new trait.
The new Y combinator:
Now it is time to test our new facility.
Results in:
All done!
You can see that the
y
function has a slightly different signature than the questioner'sfix
, but it shouldn't matter.The direct recurring version
The same technology to avoid returning a closure can be used for the normal direct recurring version as well:
Basically, whenever you need to return a closure from a function, you can add the closure's parameter to the function, and change the return type to the closure's return type. Later on when you need a real closure, just create the closure by partial evaluating that function.
Further discussions
Compare to other languages, in Rust there is a big difference: the function given to find fix point must not have any internal states. In Rust this is a requirement that the
F
type parameter ofy
must beFn
, notFnMut
orFnOnce
.For example, we cannot implement a
fix_mut
that would be used likewithout unsafe code whilst this version, if it works, performs much better (O(N)) than the version given above (O(2^N)).
This is because you can only have one
&mut
of one object at a single time. But the idea of Y combinator, or even the fix point function, requires capturing/passing the function at the same time when calling it, that's two references and you can't just mark any of them immutable without marking another so.On the other hand, I was wonder if we could do something that other languages usually not able to but Rust seems to be able. I was thinking restricting the first argument type of
F
fromFn
toFnOnce
(asy
function will provide the implementation, change toFnMut
does not make sense, we know it will not have states, but change toFnOnce
means we want it to be used only once), Rust would not allow at the moment as we cannot pass unsized object by value.So basically, this implementation is the most flexible solution we could think of.
By the way, the work around of the immutable restriction is to use pseudo-mutation:
Firstly, variable names don't exist until after they're initialised. You can't have
fixed
refer to itself like that.Secondly, you can't return closures by-value from a function, period. Generic parameters are chosen by the caller, and the caller has no idea what the type of a closure inside the function is going to be.
I'm not claiming that what follows is the best way of doing this, but it was the simplest I was able to come up with that type-checks.
In order for
fixed_fn
to refer to itself, we have to create something for it to read from before it exists. Unfortunately, this means having a cycle, and Rust hates cycles. So, we do this by constructing a reference-countedRefCell<Option<_>>
that starts withNone
, and which will be mutated later to contain the fixed-point closure.Secondly, we can't use this handle as a callable, so we have to explicitly pull a pointer to the closure out so that we can pass it to
func
.Third, the compiler doesn't seem to be able to infer the type of
fixed
correctly. I was hoping it would be able to work out that it isRc<RefCell<Option<{closure}>>>
, but it refused to do so. As a result, we have to resort to storing aBox<Fn(T) -> R>
, since we can't name the type of the closure explicitly.Finally, we have to construct a new closure that takes a second handle to
fixed
, unpacks it, and calls it. Again, we can't usefixed
as a callable directly. We also can't re-use the closure insidefixed
, because to do that we'd have to put that inside its ownRc
and at that point, things are starting to get crazy.... more crazy.
Finally, we have to return this second closure in a
Box
because, as I said before, we can't return closures by value because we can't name their types in the signature.*deep breath*
If someone has a simpler solution, I'd love to see it. :P
Starting at where you left off:
The returned object has an unnameable closure type. Using a generic type won’t help here, since the type of the closure is decided by the callee, not the caller. Here’s where
impl
traits come in handy:We can’t pass
fix(func)
tofunc
because it expects a nameable type forF
. We’ll have to settle for a trait object instead:Now it’s time to fight the lifetime checker. The compiler complains:
This is a somewhat cryptic message. Since impl traits are always
'static
by default, this is a roundabout way of saying: “the closure does not live long enough for'static
”. To get the real error message, we append+ 'static
to theimpl Fn(T) -> R
and recompile:So that was the real problem. It is borrowing
func
. We don’t need to borrowfunc
becausefn
isCopy
, so we can duplicate it as much as we want. Let’s prepend the closure withmove
and get rid of the+ 'static
from earlier:And voila, it works! Well, almost … you’ll have to edit
guess_loop
and changefn(T) -> ()
to&Fn(T) -> ()
. I’m actually quite amazed that this solution doesn’t require any allocations.If you can’t use
impl
traits, you can instead write:which is unfortunately not allocation-free.
Also, we can generalize the result a bit to allow arbitrary closures and lifetimes:
In the process of figuring out a solution for your problem, I ended up writing a simpler version of
fix
, which actually ended up guide me towards a solution to yourfix
function:Here’s a demonstration of how this
fix
function could be used to calculate the factorial:This can be done at zero runtime cost if you're willing to use unstable features (i.e. a nightly compiler) and willing to... obfuscate your code slightly.
First, we need to turn the result of
fix
into a named struct. This struct needs to implementFn
, so we'll implement it manually (this is an unstable feature).However, we're not done yet. This fails to compile with the following error:
Note: In case you're not aware, in Rust, each function has its own, zero-sized type. If a function is generic, then each instantiation of that function will have its own type as well. For example, the type of
guess_loop::<X>
will be reported by the compiler asfn(i32, &X) {guess_loop::<X>}
(as you can see in the error message above, except with underscores where the concrete type hasn't been resolved yet). That type can be coerced to a function pointer type implicitly in some contexts or explicitly with a cast (as
).The problem is that, in the expression
fix(guess_loop)
, the compiler needs to instantiateguess_loop
, which is a generic function, and it looks like the compiler isn't able to figure out the proper type to instantiate it with. In fact, the type we would like to set for type parameterF
references the type ofguess_loop
. If we were to write it out in the style reported by the compiler, the type would look likefn(i32, &Fix<X>) {guess_loop::<Fix<&X>>}
, whereX
is replaced by the type itself (you can see now where the "cyclic type of infinite size" comes from).We can solve this by replacing the
guess_loop
function by a non-generic struct (we'll call itGuessLoop
) that implementsFn
by referring to itself. (You can't do this with a normal function because you can't name a function's type.)Notice that
GuessLoop
's implementation ofFn
is no longer generic on the type of therecur
parameter. What if we tried to make the implementation ofFn
generic (while still leaving the struct itself non-generic, to avoid cyclic types)?Unfortunately, this fails to compile with the following error:
Essentially, the compiler is unable to verify that
Fix<GuessLoop>
implementsFn(i32)
, because in order to do that, it needs to verify thatGuessLoop
implementsFn(i32, &Fix<GuessLoop>)
, but that is only true ifFix<GuessLoop>
implementsFn(i32)
(because thatimpl
is conditional), which is only true ifGuessLoop
implementsFn(i32, &Fix<GuessLoop>)
(because thatimpl
is conditional too), which... you get the idea. In order words, the two implementations ofFn
here are dependent on each other, and the compiler is unable to resolve that.