From 133fcd156d3d11cbdc4ffad1cfca3b814ace48b0 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 2 Dec 2025 12:05:24 +0100 Subject: update bank with model and flow of talk --- pbt/ts/src/bank.ts | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 125 insertions(+), 7 deletions(-) (limited to 'pbt/ts/src/bank.ts') diff --git a/pbt/ts/src/bank.ts b/pbt/ts/src/bank.ts index f5981a4..bf14e43 100644 --- a/pbt/ts/src/bank.ts +++ b/pbt/ts/src/bank.ts @@ -1,9 +1,17 @@ -import {Option, fromNullable, matchW} from 'fp-ts/lib/Option'; +import {Option, fromNullable, matchW, toNullable} from 'fp-ts/lib/Option'; // keep it simple export type Account = string; +/// A basic "Adapter" to a banking system +/// +/// NOTE: we make the functions Promise-aware in order to support +/// async/await easily. export interface Bank { + /// Retrieve the balance of given account. + /// + /// @param account The account to retrieve balance of + /// @return: nothing if account is unknown, its balance otherwise balance: (account: Account) => Promise>; /// Deposit amount into account @@ -16,9 +24,125 @@ export interface Bank { /// @param account The account to deposit into /// @param amt The amount to deposit deposit: (account: Account, amt: number) => Promise; + + /// Withdraw amount from given `account`. + /// + /// This function decreases the balance of the given account by the + /// specified amount. + /// + /// @param account The account to deposit into + /// @param amt The amount to withdraw withdraw: (account: Account, amt: number) => Promise; } +/// All possible operations +export type Operation = GetBalance | Withdraw | Deposit; + +export type GetBalance = {op: 'GetBalance'; account: Account}; + +export type Withdraw = {op: 'Withdraw'; account: Account; amount: number}; + +export type Deposit = {op: 'Deposit'; account: Account; amount: number}; + +// +// +// +// +// +// + +/// A model interpreting bank operations on a simple in-memory map. +/// Interpreting a sequence of operations on the model will return an +/// updated state (map of accounts) and a sequence of observed outputs. +export function model( + accounts: Map, +): (ops: Operation[]) => [Map, (number | null)[]] { + return (ops: Operation[]) => { + const current = new Map(accounts); + const outputs: (number | null)[] = []; + for (const op of ops) { + switch (op.op) { + case 'GetBalance': { + outputs.push(current.get(op.account) || null); + break; + } + case 'Deposit': { + const bal = current.get(op.account); + if (bal) { + current.set(op.account, bal + op.amount); + } + break; + } + case 'Withdraw': { + const bal = current.get(op.account); + if (bal) { + current.set(op.account, bal - op.amount); + } + break; + } + } + } + return [current, outputs]; + }; +} + +/// A predicate asserting that some `Bank` implementation matches +/// our model. +export const bankImplementationMatchesModel = + ( + initialAccounts: Map, // Some initial dataset to work with + ) => + ( + bank: Bank, // The actual bank implementation we want to test + ) => + async ( + ops: Operation[], // an (arbitrary) sequence of operations + ): Promise => { + // run operations on bank implementation + const observedOutputs: (number | null)[] = []; + + for (const op of ops) { + switch (op.op) { + case 'Deposit': + await bank.deposit(op.account, op.amount); + break; + case 'Withdraw': + await bank.withdraw(op.account, op.amount); + break; + case 'GetBalance': + observedOutputs.push(await bank.balance(op.account).then(toNullable)); + break; + } + } + + // evaluate operations on model + const [endBalances, outputs] = model(initialAccounts)(ops); + + // compare outputs + for (const [expected, actual] of outputs.map((v, i) => [ + v, + observedOutputs[i], + ])) { + if (expected !== actual) { + console.log(`Output mismatch: expected ${expected}, got ${actual}`); + return false; + } + } + + // compare end balances + for (const [account, expected] of endBalances.entries()) { + const actual = await bank.balance(account).then(toNullable); + if (actual !== expected) { + console.log( + `Final balance mismatch for account ${account}: expected ${expected}, got ${actual}`, + ); + return false; + } + } + + return true; + }; + export class SimpleBank implements Bank { private accounts: Map; @@ -57,9 +181,3 @@ export class SimpleBank implements Bank { return Promise.resolve(); } } - -export type Operation = GetBalance | Withdraw | Deposit; - -export type GetBalance = {op: 'GetBalance'; account: Account}; -export type Withdraw = {op: 'Withdraw'; account: Account; amount: number}; -export type Deposit = {op: 'Deposit'; account: Account; amount: number}; -- cgit v1.3.1