Inventory
This program keeps the stock list of a small shop — apples, bread, candles, milk — and applies a day of orders to it. Some orders are fine. Others ask for more than is on the shelf, for an item the shop has never stocked, or for a negative amount, and those are refused with a reason.
It is a checkpoint for Part 17: Collections, but most of what it practises comes from earlier: structs, variants with data, and above all fallible functions. Its rule is that a refused order changes nothing, so the stock is never left half-updated.
How it is put together
The data is three types. A StockItem is one line of the list, an Order is one thing that can happen to it, and a StockError is one reason an order can be refused, carrying the details that explain it:
variant StockError {
UnknownItem(char8[..]),
BadAmount(int32),
NotEnough { name: char8[..]; have: int32; wanted: int32; },
StillInStock { name: char8[..]; count: int32; }
}
The functions are layered. Main hands each order to Apply, which dispatches to one function per kind of order; all of them look items up through Find:
flowchart LR
main(["Main<br/>for each order"]) --> apply["Apply<br/>match order"]
apply --> sell["Sell"]
apply --> deliver["Deliver"]
apply --> retire["Retire"]
sell --> find["Find<br/>uint?"]
deliver --> find
retire --> find
deliver -- "new item" --> push["Vector.Push<br/>may fail: CollectionError"]| Piece | Lessons it uses |
|---|---|
StockItem, Order, StockError | Struct, Variant, Error variant |
The stock as Vector<StockItem> | Vector, Mutable reference |
Find returning uint? | Optional, Presence |
?? fail in Sell and Retire | Absence to error |
Apply failing with two error types | Error sum, Propagate |
| Taking orders and errors apart | Struct pattern, Typed pattern |
Finding an item, or not
Find answers with the item's position in the vector, or none. Names are compared with EqualsSlice, because == on two slices is refused — it would ask whether they are the same view, not whether they hold the same text:
func Find(stock: &Vector<StockItem>, name: char8[..]) -> uint? {
let items = stock.AsSlice();
for i in 0..items.length {
if EqualsSlice(items[i].name, name) {
return i;
}
}
return none;
}
In Sell, an absent position is a refusal. ?? fail turns the none into a failure on the spot, so the line after it can use index as a plain uint:
let index = Find(stock, name) ?? fail StockError::UnknownItem(name);
Check everything, then change
Sell makes every check before it touches the stock. Only when all of them pass does the count change, which is what makes a refused order harmless:
func Sell(stock: &var Vector<StockItem>, name: char8[..], amount: int32) -> ! StockError {
if amount <= 0 {
fail StockError::BadAmount(amount);
}
// An absent position becomes a failure on the spot.
let index = Find(stock, name) ?? fail StockError::UnknownItem(name);
var items = stock.AsMutableSlice();
if items[index].count < amount {
fail StockError::NotEnough { name: name, have: items[index].count, wanted: amount };
}
items[index].count -= amount;
}
AsMutableSlice gives a writable view of the vector's elements, so items[index].count -= amount changes the stock in place. Sell returns ! StockError with no success value: reaching the end of the body is the success.
Two kinds of failure, kept apart
A delivery of something new adds an item, and adding to a vector can run out of memory. That is a CollectionError — a different kind of failure altogether, and nobody's fault. So Deliver and Apply fail with the sum of the two error types, and every ? passes either kind through unchanged:
func Apply(stock: &var Vector<StockItem>, order: Order) -> ! (StockError | CollectionError) {
Main opens the outcome with typed patterns. A StockError is explained and counted; a CollectionError ends the day, because no order can be blamed for it:
match Apply(stock, order) {
.Success(()) => PrintLine(" done"),
.Failure(error: StockError) => {
Explain(error);
refused += 1;
},
// Running out of memory is no fault of the order, so it ends the day.
.Failure(error: CollectionError) => fail error
}
Main itself is declared -> ! CollectionError, so fail error ends the program with exit status 1.
Retiring a sold-out item
Retire refuses to drop an item that is still on the shelf. Once it has sold out, RemoveAt takes it out of the vector. In the run above, retiring milk is refused with 6 still in stock, candles (at 0) are removed, and later honey — delivered as a new item — appears at the end of the closing list.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Inventory: the stock list of a small shop, kept as structs in a `Vector`, and a day of orders
// applied to it, some of which cannot be carried out.
//
// Every change to the stock is a fallible function, and each way it can be refused is a case of
// `StockError` carrying the details: which item is unknown, how many were wanted and how many
// were there. A refused order changes nothing, so the stock is never left half-updated.
//
// A delivery of something new adds an item to the vector, and adding can run out of memory. That
// is a `CollectionError`, a different kind of failure altogether, so `Apply` fails with the sum
// `StockError | CollectionError` and both kinds pass through its `?` unchanged.
import Allocator::{ Allocator, SystemAllocator };
import Collections::{ CollectionError, EqualsSlice, Vector };
import Io::PrintLine;
struct StockItem {
name: char8[..];
count: int32;
}
variant StockError {
UnknownItem(char8[..]),
BadAmount(int32),
NotEnough { name: char8[..]; have: int32; wanted: int32; },
StillInStock { name: char8[..]; count: int32; }
}
variant Order {
Sell { name: char8[..]; amount: int32; },
Deliver { name: char8[..]; amount: int32; },
Retire(char8[..])
}
// Where the item sits in the vector, or `none` if the shop has never stocked it. Names are
// compared with `EqualsSlice`: `==` on two slices is refused, because it would ask whether they
// are the same view rather than whether they hold the same text.
func Find(stock: &Vector<StockItem>, name: char8[..]) -> uint? {
let items = stock.AsSlice();
for i in 0..items.length {
if EqualsSlice(items[i].name, name) {
return i;
}
}
return none;
}
func Sell(stock: &var Vector<StockItem>, name: char8[..], amount: int32) -> ! StockError {
if amount <= 0 {
fail StockError::BadAmount(amount);
}
// An absent position becomes a failure on the spot.
let index = Find(stock, name) ?? fail StockError::UnknownItem(name);
var items = stock.AsMutableSlice();
if items[index].count < amount {
fail StockError::NotEnough { name: name, have: items[index].count, wanted: amount };
}
items[index].count -= amount;
}
func Deliver(stock: &var Vector<StockItem>, name: char8[..], amount: int32)
-> ! (StockError | CollectionError) {
if amount <= 0 {
fail StockError::BadAmount(amount);
}
match Find(stock, name) {
index? => {
var items = stock.AsMutableSlice();
items[index].count += amount;
},
none => stock.Push(StockItem { name: name, count: amount })?
}
}
// An item leaves the list only once it has sold out.
func Retire(stock: &var Vector<StockItem>, name: char8[..]) -> ! StockError {
let index = Find(stock, name) ?? fail StockError::UnknownItem(name);
let count = stock.AsSlice()[index].count;
if count > 0 {
fail StockError::StillInStock { name: name, count: count };
}
// `RemoveAt` hands back the item it took out, as an optional; it is not needed here.
stock.RemoveAt(index);
}
func Apply(stock: &var Vector<StockItem>, order: Order) -> ! (StockError | CollectionError) {
match order {
.Sell { name, amount } => {
PrintLine("sell {} {}", amount, name);
Sell(stock, name, amount)?;
},
.Deliver { name, amount } => {
PrintLine("deliver {} {}", amount, name);
Deliver(stock, name, amount)?;
},
.Retire(name) => {
PrintLine("retire {}", name);
Retire(stock, name)?;
}
}
}
func Explain(error: StockError) {
match error {
.UnknownItem(name) => PrintLine(" refused: no such item as {}", name),
.BadAmount(amount) => PrintLine(" refused: {} is not an amount", amount),
.NotEnough { name, have, wanted } =>
PrintLine(" refused: wanted {} {}, only {} left", wanted, name, have),
.StillInStock { name, count } =>
PrintLine(" refused: {} {} still on the shelf", count, name)
}
}
func PrintStock(stock: &Vector<StockItem>) {
for item in stock {
PrintLine(" {:10} {:3}", item.name, item.count);
}
}
func Main() -> ! CollectionError {
var system = SystemAllocator();
let allocator: Allocator = system;
var stock = Vector<StockItem>(allocator);
stock.Push(StockItem { name: "apples", count: 12 })?;
stock.Push(StockItem { name: "bread", count: 3 })?;
stock.Push(StockItem { name: "candles", count: 0 })?;
stock.Push(StockItem { name: "milk", count: 6 })?;
PrintLine("opening stock");
PrintStock(stock);
let orders = [
Order::Sell { name: "apples", amount: 5 },
Order::Sell { name: "bread", amount: 4 },
Order::Sell { name: "honey", amount: 1 },
Order::Deliver { name: "bread", amount: 10 },
Order::Sell { name: "bread", amount: 4 },
Order::Deliver { name: "honey", amount: 2 },
Order::Sell { name: "milk", amount: -2 },
Order::Retire("milk"),
Order::Retire("candles"),
Order::Sell { name: "milk", amount: 6 }
];
var refused = 0;
for order in orders {
match Apply(stock, order) {
.Success(()) => PrintLine(" done"),
.Failure(error: StockError) => {
Explain(error);
refused += 1;
},
// Running out of memory is no fault of the order, so it ends the day.
.Failure(error: CollectionError) => fail error
}
}
PrintLine("closing stock, after {} refused orders", refused);
PrintStock(stock);
}
Besides Io, its Rux.toml lists Allocator and Collections under [Dependencies].
Run it
cd Examples/Projects/Inventory
rux run
opening stock
apples 12
bread 3
candles 0
milk 6
sell 5 apples
done
sell 4 bread
refused: wanted 4 bread, only 3 left
sell 1 honey
refused: no such item as honey
deliver 10 bread
done
sell 4 bread
done
deliver 2 honey
done
sell -2 milk
refused: -2 is not an amount
retire milk
refused: 6 milk still on the shelf
retire candles
done
sell 6 milk
done
closing stock, after 4 refused orders
apples 7
bread 9
milk 0
honey 2
Common mistakes
==.items[i].name == name is refused: error: operator '==' is not defined for slice type 'char8[..]', with the note that comparing the views would compare addresses rather than elements. Use EqualsSlice from Collections.CollectionError into a function that fails with StockError only.Declare
Deliver as -> ! StockError and its stock.Push(…)? stops compiling: error: '?' propagates error type 'CollectionError', but the enclosing function fails with 'StockError'. ? never converts one error into another; the function has to admit both.Remove the
.Failure(error: CollectionError) arm from Main and the match is incomplete: error: match on '! (CollectionError | StockError)' is not exhaustive; missing .Failure(_: CollectionError). Every member of the sum needs an answer, even if the answer is "give up".Try it yourself
- Add an order
Return { name; amount; }for goods a customer brings back. It should behave like a delivery, but refuse an item the shop has never stocked. - Give each item a price in cents and print the value of the closing stock. Which functions have to change, and which do not?
- Add a
LowStockwarning: after each successful sale, print a note if fewer than three are left. Is that a new case ofStockError, or something else? - Apply all of a customer's orders together or none of them: take a small array of orders, check them all first, and only then change the stock.
Learn more
- Vector —
Push,AsSliceandAsMutableSlice - Absence to error and Error sum
- Typed pattern — matching a sum by the type it holds
- Next project: Statistics, the checkpoint for Algorithms
25.8 Word count
Count how often each word appears in a tongue twister, and print the counts in alphabetical order.
25.10 Statistics
Summarise slices of numbers — count, extremes, mean, population and sample variance, and median — including an even count with repeated values, a single value and no values at all.