📜 ⬆️ ⬇️

We add numbers on Rust

Implementing the arithmetic of natural numbers with Peano numbers is a popular programming learning problem. I was wondering whether it is possible to implement them on Rust. Thus, my task is to write down and add the natural numbers with type-type checking.



If you believe Wikipedia "Axioms Peano - one of the systems of axioms for natural numbers, introduced in the XIX century by the Italian mathematician Giuseppe Peano."


We are interested in two of them - with which you can enter and use natural numbers:



Literally we will write on raste with the help:


enum Nat { Zero, Succ(Nat) } 

Nat is either a zero or the next positive integer. But such a record has a problem - we have received infinitely many natural numbers:


error: recursive type `Nat` has infinite size


An obvious solution would be to store any pointer to the next positive number instead of Succ(Nat) .
But then at the level of types there will be only two values ​​- Zero and Box<Succ> . It does not work: there is no information about the numbers at the level of types - 5 and 7 have one type.


Run the code on play.rust-lang.org


We write the same numbers a little differently:


 use std::marker::PhantomData; struct Zero; struct Succ<T> { _marker : PhantomData<T> } 

Zero is a data type with one possible value - Zero. Succ is a data type also with a single value - Succ, but with a payload in the form of a data type. May be Succ<i32> , Succ<String> , Succ<Succ<_>> , etc.


Now we have separately zero and the following number separately. Now the type Succ<Succ<Succ<Succ<Zero>>>> is valid. But there is a problem - Zero is not connected with Succ. Solution - we introduce the type Nat:


 trait Nat { fn new() -> Self; } 

Nat is a type that should implement any type that is part of natural numbers, that is, Zero and Succ.
The new function allows you to drop a value from the type level to the data level by creating a concrete instance.
We implement it for Zero and Succ:


 impl Nat for Zero { fn new() -> Self { Zero } } impl<T : Nat> Nat for Succ<T> { fn new() -> Self { Succ { _marker : PhantomData } } } 

Now we can create a natural number!


Run the code on play.rust-lang.org


 let two : Succ<Succ<Zero>> = Nat::new(); let three : Succ<Succ<Succ<Zero>>> = Nat::new(); 

Our next goal is to get the number 4 from number 3! We introduce the type Incr:


 trait Incr : Nat { type Result; } 

It differs from our past image of Nat in that it contains not only functions, but also type. The increment type T on 1 according to our rules of natural numbers is Succ. What we write:

 impl<T : Nat> Incr for T { type Result = Succ<T>; } 

Thus, we implemented the Incr type for all types T with realized Nat.


Now you can write a function that accepts something with the Incr type (which is implemented for all natural numbers) and returns what is written in the Incr type implementation. This is literally and write:


 fn incr<T : Incr, Res : Nat>(_ : T) -> Res where T : Incr<Result = Res> { Res::new() } 

Now we can get 4!


 let three : Succ<Succ<Succ<Zero>>> = Nat::new(); let four = incr(three); 

And we can, for example, write a function for working with numbers and be sure that it will not do anything except an increment:


 fn next<In : Nat, Out : Nat>(x : In) -> Out where In : Incr<Result = Out> { incr(x) } 

Useful! Now let's try to add the numbers: according to Peano's axiomatics, addition can be written like this:


 x + 0 = x x + Succ(y) = Succ(x + y) 

Let's try to write it in terms of rasta:


 trait Sum<B> : Nat { type Result; } 

We introduce the type of Sum. Another type of syntax appeared - a type parameterized by type. In this case, Sum<B> is all that can be added with B.


We realize addition of zero:


 impl<A : Nat> Sum<A> for Zero { type Result = A; } 

and the addition is not zero:


 impl<A : Nat, B : Nat, Res> Sum<A> for Succ<B> where B : Sum<A, Result = Res> { type Result = Succ<Res>; } 

If you look closely, you can see that this is x + Succ(y) = Succ(x + y) .


By analogy with incr, we will write a convenient function:


 fn sum<A : Sum<B>, B : Nat, Res : Nat>(_ : A, _ : B) -> Res where A : Sum<B, Result = Res> { Res::new() } 

Now we can add numbers!


 let two : Succ<Succ<Zero>> = Nat::new(); let three : Succ<Succ<Succ<Zero>>> = Nat::new(); let four = incr(three); let six = sum(two, four); 

But this is clearly not enough. It would be desirable, for example, to display the result on the screen. The easiest option is to write this:


 let six : () = sum(two, four) 

The compiler will report an error and beautifully show the type we need in the "expected" field:


 note: expected type `Succ<Succ<Succ<Succ<Succ<Succ<Zero>>>>>>` note: found type `()` 

But I want to withdraw our number in a more honest way. We use the type Display .


Let's start with Zero:


 use std::fmt::{Display, Result, Formatter}; impl Display for Zero { fn fmt(&self, f: &mut Formatter) -> Result { write!(f, "Zero") } } 

Zero just withdraw.


Now a little recursion:


 impl<T : Nat + Display> Display for Succ<T> { fn fmt(&self, f: &mut Formatter) -> Result { write!(f, "(Succ {})", T::new()) } } 

Run the code on play.rust-lang.org


Now you can print our 6:


 (Succ (Succ (Succ (Succ (Succ (Succ Zero)))))) 

We have succeeded! I leave the reader to enter the multiplication:


 x * Zero = Zero x * Succ(y) = x * y + x 

So with the help of monomorphizing plant types, you can add numbers at compile time. I am not sure that the presented solution is correct or idiomatic, but it seemed to me curious.


')

Source: https://habr.com/ru/post/310572/


All Articles