I wanted to try to build a proper implementation of Peano numbers using struct
s, but it seems my generics game is not good enough yet and I could use some help. I read the docs on generics and some StackOverflow questions, but they don't fit my case.
I introduced a Peano
trait and Zero
and Succ
types:
trait Peano {}
struct Zero;
struct Succ<T: Peano>(T);
And implemented a Peano
trait for both types to be able to abstract over both:
impl Peano for Zero {}
impl<T> Peano for Succ<T> where T: Peano {}
At first I wanted to implement std::ops::Add
for Peano
, but I quickly saw that I was doing something very wrong, so I decided to start with something simpler - enumeration:
trait Enumerate<T: Peano> {
fn succ(&self) -> Succ<T>;
fn pred(&self) -> Option<T>;
}
impl<T> Enumerate<T> for Zero where T: Peano {
fn succ(&self) -> Succ<T> { Succ(*self) } // mismatched types: Zero instead of T
fn pred(&self) -> Option<T> { None }
}
impl<T> Enumerate<T> for Succ<T> where T: Peano {
fn succ(&self) -> Succ<T> { Succ(*self) } // mismatched types: Succ<T> instead of T
fn pred(&self) -> Option<T> { Some(self.0) }
}
What am I missing? I experimented with boxing the results (though I would want to avoid that if possible), but the error just changed to mismatched types: Box<Succ<T>> instead of Box<Peano>
, so I'm not sure this is helpful.
Full code below:
trait Peano {}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Zero;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Succ<T: Peano>(T);
impl Peano for Zero {}
impl<T> Peano for Succ<T> where T: Peano {}
trait Enumerate<T: Peano> {
fn succ(&self) -> Succ<T>;
fn pred(&self) -> Option<T>;
}
impl<T> Enumerate<T> for Zero where T: Peano {
fn succ(&self) -> Succ<T> { Succ(*self) }
fn pred(&self) -> Option<T> { None }
}
impl<T> Enumerate<T> for Succ<T> where T: Peano {
fn succ(&self) -> Succ<T> { Succ(*self) }
fn pred(&self) -> Option<T> { Some(self.0) }
}
You have a
T
inEnumerate
... which serves no purpose.If you look back at your
Peano
trait, you will see that it has noT
: the implementation forSucc
has a parameter, but the trait itself does not.The same applies here.
Let us start with a reduced scope: an
Enumerate
that can only go forward.A few points of interest:
Self
, very useful when defining a trait since the type of implementers is unknown in advance: Peano + Sized
syntax after the trait nameNow, you also had a
prev
method which I did not implement. The thing is, it is nonsensical to applyprev
toZero
. In this case, I propose that you renameEnumerate
toNext
and I'll show how to create aPrev
trait:The syntax
type Output: Peano + Sized
is an associated type, it allows each implementer to specify which type to use for their specific case (and avoid having the user of the trait, having to guess which type they should use).Once specified, it can be referred to as
Self::Output
within the trait or as<X as Prev>::Output
from outside (ifX
implementsPrev
).And since the trait is separate, you only have a
Prev
implementation forPeano
numbers that actually have a predecessor.Why the
Sized
constraint?At the moment, Rust requires that return types have a known size. This is an implementation limit: in practice the caller has to reserve enough space on the stack for the callee to write down the return value.
However... for type-level computation this is useless! So, what do we do?
Well, first we add convenient method of checking the result of our computations (prettier than the
Debug
output):Then... let's get rid of those methods, they bring nothing; the reworked traits are thus:
and:
Now, you can go ahead and implement
Add
and co, though if you implement traits with methods you might need additional constraints.