So, taking OCaml as a typical example of an ML language, the "signature of an API" you are talking about could be something like:
API
module type Stack = sig
type 'item collection
val empty: 'item collection
val push: 'item collection -> 'item -> 'item collection
val pop: 'item collection -> ('item * 'item collection) option
end
which defines an API called Stack
.
Transposition from OCaml to Rust terms
-
type 'item collection
with a collection
(associated) type, which is generic over some 'item
parameter (In OCaml, the "lifetime syntax" of 'identifier
signifies a type parameter; also, generic parameters, in OCaml, go before the generic thing: t option
rather than Option<T>
, for instance).
trait Stack {
type Collection<Item>;
…
}
-
val empty: 'item collection
with a empty
(associated) constant (val
) of (generic) type Collection<Item>
:
trait … {
…
/// Pseudo-code, in Rust.
const EMPTY<Item>: Self::Collection<Item>;
}
-
val push: 'item collection -> 'item -> 'item collection
with a push
(associated) constant whose (generic) type is that of a function signature (in Rust, this would be an associated function), with the signature being written in a currified fashion:
trait … {
…
fn push<Item> (
_: Self::Collection<Item>,
_: Item,
) -> Self::Collection<Item>
;
}
-
And ditto for pop
(T * U
is the OCaml syntax for a tuple type; in Rust: (T, U)
).
An implementor / implementation
Now, we can try to define an implementation of this:
(* our implementor : the API it implements *)
module ListStack : Stack = struct
type 'item collection = 'item list
let empty = []
let push list new_head = new_head::list
let pop = function
| head::tail -> Some (head, tail)
| [] -> None
end
with an implementation using the very pervasive in ML languages data structure: a cons-list.
-
for short, in Rust parlance: ()
is the empty list, otherwise lists are defined as something like (head, tail)
, with head
being the first item of the list, and tail
being a sub-list with the remaining items. That is, the sequence, 42, 27, 0
, in a cons-list, with Rust tuples, would be expressed as:
(42, (27, (0, ())))
In OCaml, the (head, tail)
list-packagin is done with the ::
operator, both for constructing and destructuring, and the empty list is represented as []
.
If we were to forget some item, or implement it with an incorrect signature, we'd get an implementation mismatch error. For instance, reversing the order of args in push
, we get:
Error: Signature mismatch:
Modules do not match:
sig
type 'item collection = 'item list
val empty : 'a list
val push : 'a -> 'a list -> 'a list
val pop : 'a list -> ('a * 'a list) option
end
is not included in
Stack
Values do not match:
val push : 'a -> 'a list -> 'a list
is not included in
val push : 'item collection -> 'item -> 'item collection
In Rust
The "generic const" thingy is not directly expressible, in Rust (we'd need a helper trait for that); and the generic associated type requires nightly and would needlessly complexify the API here. Rather, we're gonna move genericity from the associated items up to the OCaml Module
/ Rust trait; which actually makes more sense (the reason this is not done in OCaml is that their Module genericity only works over other Modules (the whole thing being called a Functor), and that lifting a type to a Module is cumbersome, so it ends up being way more convenient to just be generic at the associated item level.
But in Rust we'll directly and properly be generic at the trait level:
trait Stack<Item> {
type Collection;
const EMPTY: Self::Collection;
fn push (_: Self::Collection, _: Item) -> Self::Collection;
fn pop (_: Self::Collection) -> Option<Item, Self::Collection>;
}
Note: OCaml modules don't have Self
, so we need an associated item for it. To be completely idiomatic in Rust, that Self::Collection
associated type could direclty be the implementor of the trait: let's replace Self::Collection
with Self
:
trait Stack<Item> : Sized {
const EMPTY: Self;
fn push(self, _: Item) -> Self;
fn pop(self) -> Option<(Item, Self)>;
}
and the implementation:
enum List<T> {
Nil,
Cons(T, Box<List<T>>),
}
impl<Item> Stack<Item> for List<Item> {
const EMPTY: List<Item> = List::Nil;
fn push(self: List<Item>, item: Item) -> List<Item> {
List::Cons(item, Box::new(self))
}
fn pop(self: List<Item>) -> Option<(Item, List<Item>)> {
match self {
| List::Nil => None,
| List::Cons(head, tail) => Some((head, *tail)),
}
}
}
And now, regarding the OP question: what if we had the implementation violate the desired API? We'd get an error as well:
error[E0053]: method `push` has an incompatible type for trait
--> src/main.rs:16:23
|
14 | impl<Item> Stack<Item> for List<Item> {
| ---- this type parameter
15 | const EMPTY: List<Item> = List::Nil;
16 | fn push(item: Item, this: List<Item>) -> List<Item> {
| ^^^^
| |
| expected enum `List`, found type parameter `Item`
| help: change the parameter type to match the trait: `List<Item>`
|
note: type in trait
--> src/main.rs:5:20
|
5 | fn push(_: Self, _: Item) -> Self;
| ^^^^
= note: expected fn pointer `fn(List<Item>, Item) -> List<_>`
found fn pointer `fn(Item, List<Item>) -> List<_>`
Testing it
Now, we can even write a test for this "generic" API.
OCaml
Generic test:
(* Convenience stuff: *)
(* rather than doing `foo (bar (baz …))`, write `foo @< bar @< baz …` *)
let (@<) f arg = f arg
(* assert_eq helper *)
let assert_eq x y = assert (x == y)
(* non-keyword assert (to play with @<) *)
let assert_ predicate = assert predicate
module TestStack(S : Stack) = struct
let test_it () = (
let l = S.empty in
let l = S.push l 42 in
let l = S.push l 27 in
let (fst, l) = Option.get @< S.pop l in
assert_eq fst 27;
let (snd, l) = Option.get @< S.pop l in
assert_eq snd 42;
assert_ @< Option.is_none @< S.pop l;
)
end
Running it against our ListStack
:
module TestListStack = TestStack(ListStack);;
TestListStack.test_it();;
Rust
Generic test (we have to pick the integer item type in advance, rather than inside the test's body):
struct TestStack<S : Stack<i32>> /* whatever: */ (*mut Self);
impl<S : Stack<i32>> TestStack<S> {
fn test_it() {
let l = S::EMPTY;
let l = l.push(42);
let l = l.push(27);
let (fst, l) = l.pop().unwrap();
assert_eq!(fst, 27);
let (snd, l) = l.pop().unwrap();
assert_eq!(snd, 42);
assert!(l.pop().is_none());
}
}
Running it against our List
:
type TestListStack = TestStack<List<i32>>;
TestListStack::test_it();
Note that there is a level of extra boilerplate here, in Rust, because OCaml couldn't feature a function generic over a Module, only "Modules" can (then called Functors), thence the TestStack
helper Functor, which is the one providing the non-generic testing function.
I've tried to replicate the resulting semantics in Rust, but given that Rust does not have the distinction between traits (module sig
natures) & impl
s (module struct
), and types, a generic function can then take a type parameter expected to impl
the given trait.
-
Thence yielding the following simplification:
fn test_it<S : Stack<i32>>() {
let l = S::EMPTY;
let l = l.push(42);
let l = l.push(27);
let (fst, l) = l.pop().unwrap();
assert_eq!(fst, 27);
let (snd, l) = l.pop().unwrap();
assert_eq!(snd, 42);
assert!(l.pop().is_none());
}
test_it::<ListStack<i32>>();