Example Feature Requirements
Let us consider the following simplistic requirements to an order processing feature of a typical e-commerce application:- There is a stock of products.
- Each product in stock is counted in whole numbers.
- An order is a set of products with their respective quantities.
- An order can be accepted for processing only if there is enough stock of all ordered products.
- Orders are processed in the order they are submitted.
- Processing an order means decreasing the stock quantities of respective products.
VDM-SL
VDM is based on the concept of refinement: Starting with a top level abstract specification and then gradually refining (adding details to) types and functions.VDM-SL used for examples is based on lambda-calculus, thus essentially is a statically typed functional language, in contrast to its extensions: VDM++ for object-oriented and VDM-RT for real-time applications.
We shall consider three levels of refinement:
- Abstract specification.
- Detailed specification with implicitly defined functions - what they do, not how.
- Detailed specification with explicitly defined functions so that they can be executed with VDM-SL interpreter.
The abstract specification defines only key data types and signatures of functions - the data flow.
-- Top level data and flow outline
types
  Product = token;
  Quantity = nat1;
  ProductSpec = map Product to Quantity;
  
  Stock = ProductSpec;
  Order = ProductSpec;
  
  Operations :: 
    Stock 
    seq of Order
    inv mk_Operations(stock, orders) == let consolidatedOrders = consolidate(orders) in 
      forall product in set dom consolidatedOrders & 
        product in set dom stock 
        and consolidatedOrders(product) <= stock(product);
  
functions
  consolidate(specs: seq of ProductSpec) consolidatedSpec: ProductSpec == undefined;
  checkOut(op: Operations, order: Order) op': Operations == undefined;
  processOrder(op: Operations) op': Operations == undefined;
Remarks:
- We use token type for something we do not want to or need to specify at given level of abstraction.
- We can provide invariant for data types with inv expression.
- We keep all the functions undefined to start with - relying on type signatures only.
Our first refinement is to define the key functions - checkOut and processOrder - implicitly, i.e. by specifying their pre-condition and, more importantly, post-condition, which essentially is the definition of what the function does.
-- Refined impclicit specification
types
  Product = token;
  Quantity = nat1;
  ProductSpec = map Product to Quantity;
  
  Stock = ProductSpec;
  Order = ProductSpec;
  
  Operations :: 
    Stock 
    seq of Order
    inv mk_Operations(stock, orders) == let consolidatedOrders = consolidate(orders) in 
      forall product in set dom consolidatedOrders & 
        product in set dom stock 
        and consolidatedOrders(product) <= stock(product);
  
functions
  consolidate(specs: seq of ProductSpec) consolidatedSpec: ProductSpec == undefined;
  checkOut(op: Operations, order: Order) op': Operations
    pre orderProductsAreValid(op, order) and thereIsEnoughStock(op, order)
    
    post let 
      mk_Operations(stock, orders) = op, 
      mk_Operations(stock', orders') = op' in 
        stock' = stock                  -- stock was not changed 
        and orders' = orders ^ [order]; -- order was appended to the list of orders
  orderProductsAreValid(op: Operations, order: Order) result: bool == undefined;  -- self descriptve
  thereIsEnoughStock(op: Operations, order: Order)result: bool == undefined;      -- self descriptve
  
  processOrder(op: Operations) op': Operations
    post let 
      mk_Operations(stock, orders) = op, 
      mk_Operations(stock', orders') = op',
      theOrder = hd orders in
        orders' = tl orders                                   -- processed order was removed
        and forall product in set dom theOrder & 
          stock'(product) = stock(product) - theOrder(product); -- stock quantities were decreased
Remarks:
- We may also keep some secondary functions undefined for the time being.
Next step is to provide all functions with their explicit bodies that can be effectively evaluated, thus interpreted with, e.g. the Overture Tool.
-- Refined explicit (executable) specification
types
  Product = <vanilla> | <chocolate> | <strawberry>;   -- token refined into a set of quotes
  Quantity = nat1;
  ProductSpec = map Product to Quantity;
  
  Stock = ProductSpec;
  Order = ProductSpec;
  
  Operations :: 
    Stock 
    seq of Order
    inv mk_Operations(stock, orders) == let consolidatedOrders = consolidate(orders) in 
      forall product in set dom consolidatedOrders & 
        product in set dom stock 
        and consolidatedOrders(product) <= stock(product);
  
functions
  consolidate(specs: seq of ProductSpec) consolidatedSpec: ProductSpec == consolidateRec({|->}, specs);
  
  consolidateRec(consodilatedSpec: ProductSpec, specs: seq of ProductSpec) consolidatedSpec: ProductSpec == 
    if specs = [] then
      consodilatedSpec
    else 
      let theSpec = hd specs in 
        consolidateRec(consodilatedSpec 
          ++ {p |-> consodilatedSpec(p) + theSpec(p) | p in set dom theSpec}, tl specs);
  checkOut(op: Operations, order: Order) op': Operations == let mk_Operations(stock, orders) = op in 
    mk_Operations(stock, orders ^ [order])  -- keep the stock and add the order
    pre orderProductsAreValid(op, order) and thereIsEnoughStock(op, order);
  orderProductsAreValid(op: Operations, order: Order) result: bool == let mk_Operations(stock, -) = op in
    dom order subset dom stock;
    
  thereIsEnoughStock(op: Operations, order: Order)result: bool == let mk_Operations(stock, -) = op in
    forall product in set dom order & order(product) <= stock(product);
  
  processOrder(op: Operations) op': Operations == 
    let 
      mk_Operations(stock, orders) = op, 
      theOrder = hd orders in
        -- decrease the stock
        mk_Operations(stock ++ {p |-> stock(p) - theOrder(p) | p in set dom theOrder}, tl orders)
    pre let mk_Operations(-, orders) = op in len orders > 0;
    
  test() result : bool == processOrder(
    checkOut(
      mk_Operations({<vanilla> |-> 10, <chocolate> |-> 10, <strawberry> |-> 10}, []),
      {<vanilla> |-> 5, <chocolate> |-> 7}
      )
    ) = mk_Operations({<vanilla> |-> 5, <chocolate> |-> 3, <strawberry> |-> 10}, []);Remarks:
- We converted implicit post-conditions into function bodies - in complex cases, we may keep post-conditions as a way to verify the explicit implementation.
- We provided a small test function that calls checkOut and processOrder then verifies the result.
Haskell
Below is an implementation of the above specification in Haskell.module Main where
import qualified Data.Map as Map
data Product = Vanilla | Chocolate | Strawberry
    deriving (Eq, Ord)
type Quantity = Int
type ProductSpec = Map.Map Product Quantity
type Stock = ProductSpec
type Order = ProductSpec
data Operations = Operations Stock [Order]
    deriving Eq
type Error = String
checkOut :: Operations -> Order -> Either Operations Error
checkOut op@(Operations stock orders) order = if not (orderProductsAreValid op order)
    then Right "Some of the products are not in stock" 
    else if not (thereIsEnoughStock op  order)
        then Right "Not enough stock" 
        else Left $ Operations stock (orders ++ [order])
orderProductsAreValid :: Operations -> Order -> Bool
orderProductsAreValid op@(Operations stock _) order = all (\p -> Map.member p stock) (Map.keys order)
thereIsEnoughStock :: Operations -> Order -> Bool
thereIsEnoughStock op@(Operations stock _) order = all (\p -> (order Map.! p) <= (stock Map.! p)) (Map.keys order)
        
processOrder :: Operations -> Operations
processOrder op@(Operations stock (theOrder : restOfOrders)) = Operations (foldl updateStock stock (Map.keys theOrder)) restOfOrders
    where
        updateStock :: ProductSpec -> Product -> ProductSpec
        updateStock stock product = Map.update (\_ -> Just $ (stock Map.! product) - (theOrder Map.! product)) product stock
processOrder op@(Operations _ []) = op
test = testProcessOrder (checkOut (Operations (Map.fromList [(Vanilla, 10), (Chocolate, 10), (Strawberry, 10)]) [])
        (Map.fromList [(Vanilla, 5), (Chocolate, 7)]))
    where
        testProcessOrder (Left ops) = processOrder ops == (Operations (Map.fromList [(Vanilla, 5), (Chocolate, 3), (Strawberry, 10)]) [])
        testProcessOrder (Right _) = False
main = putStrLn $ show test
Remarks:
- The code is very similar, data type definitions are almost identical - the translation is straightforward.
- VDSM-SL version in many ways is more abstract and easy to read, e.g. comparedom order subset dom stock
 expression with the Haskell analogall (\p -> Map.member p stock) (Map.keys order)
- We had to deal with error handling in Haskell instead of simply relying on the pre-condition in checkOut.
F#
Similarly, below is an implementation of the above specification in F#.type Product = Vanilla | Chocolate | Strawberry
type Quantity = int
type ProductSpec = Map
type Stock = ProductSpec
type Order = ProductSpec
type Operations = {stock: Stock; orders: Order list}
exception OrderProcessing of string
let orderProductsAreValid {stock = stock} order = 
    Map.forall (fun product _ -> Map.containsKey product stock) order
let thereIsEnoughStock {stock = stock} order = 
    Map.forall (fun product _ -> (Map.find product stock) >= (Map.find product order)) order
let checkOut ops order = 
    if orderProductsAreValid ops order && thereIsEnoughStock ops order
    then { ops with orders = List.append ops.orders [order] }
    else raise (OrderProcessing "Incorrect order")
let processOrder {stock = stock; orders = orders} = 
    let order = List.head orders
    let updateStock stock product quantity = Map.add product (Map.find product stock - Map.find product order) stock
    in {stock = Map.fold updateStock stock order; orders = List.tail orders}
let test =
    let initialOps = {stock = Map.ofList [(Vanilla, 10); (Chocolate, 10); (Strawberry, 10)]; orders = []}
    let order = Map.ofList [(Vanilla, 5); (Chocolate, 7)]
    let expectedOps = {stock = Map.ofList [(Vanilla, 5); (Chocolate, 3); (Strawberry, 10)]; orders = []}
    in checkOut initialOps order |> processOrder = expectedOps
try
    printfn "%b" test
with
    | OrderProcessing message -> printfn "Error: %s" message 
Remarks:
- The code is also very similar, thus easy to translate.
- We opted for exceptions for error handling, thus the signature of checkOut was kept the same as in the specification.
Conclusions
We believe, this small example shows that VDM style refinement is a pragmatic tool for requirements analysis in the correct-by-construction approach to software development.Particularly, VDM-SL fits nicely as the design language in between requirements analysis and an implementation in a functional programming language.
 
No comments:
Post a Comment