PlainSpec
PlainSpec is a pseduo language I designed for coding interviews to help communicate and gather thoughts in a english like syntax. It is very effective for:
- interviews using OOP like structres (implementing classes, a method, modelling a problem…)
- interviews where the focus is on code correctness and catching complex edge cases rather than algorithmic gotchas
So here’s the deal:
An interviewer gives you a few classes, fields and method signatures. Prehaps a comment or two explaining it.
PlainSpec translates that supplied API into an ordinary-language model that both people can inspect before implementation begins, and this may seem like a waste of time but it ensures 2 things:
- that both you and the interviewer are in agreement of the problem and solution, which also gives them an early chance to help you out or correct your understanding
- this actually makes your code faster and less error prone since you won’t need to think about algorithmic logic alongside code correctness
It is not polished documentation and it is not implementation pseudocode. It is a disposable, editable statement of what the program means.
1. The language
Write PlainSpec directly inside the class as a block comment. Use the real class and method names as headings.
EntityName
- <field> represents ...
- <field> starts as ...
- set <field> to be <value> when ...
actionName(...)
- means ...
- set ...
- for each ...
- when <case>:
- update ...
- returns ...That is the whole vocabulary:
- represents says what a field or object means in the actual system.
- starts as says what the state looks like before any actions happen.
- set says what initial or new value something gets.
- means says what an action does in normal English.
- for each says what collection the action needs to consider.
- while says a condition that runs while the condition is true
- when separates cases that behave differently.
- update says what changes.
- returns says what the caller gets back.
2. How to use it in an interview
Tell the interviewer what you are doing:
Before I code, I want to write the system back in English so we can make sure I understand the entities and how they interact. I will talk through it while I write.
Then write PlainSpec in the class comment. Keep speaking. If something is unclear, ask about it right where it matters instead of quietly choosing an answer.
Once it looks complete, say it back once:
This is my understanding of what each thing represents and what the method should do in each case. Does this match what you mean?
When the interviewer agrees, start coding. If they add an extension, update the English first and then update the code.
Example: Locker Room
from dataclasses import dataclass
from typing import Optional
"""
In plainspec:
Locker
- represents a container that holds an item
- Locker.item is None when the locker is empty
LockerRoom
- represents a collection of lockers
- LockerRoom.lockers is a list of lockers, unordered
- LockerRoom.store() takes an item name, and stores the item in the first empty locker
- for each locker in LockerRoom:
- if the locker is empty -> store it in the locker and return a reference to that locker
- if the locker is occupied -> continue
- if we have gone through all lockers and none are free, return None
"""
@dataclass
class Locker:
item: Optional[str] # None means empty
class LockerRoom:
def __init__(self, lockers: list[Locker]):
self.lockers = lockers
def store(self, item: str) -> Optional[Locker]:
"""
Store the item in the first empty locker.
Return that locker, or None if every locker is occupied.
"""
passExample:
from dataclasses import dataclass
from typing import Optional
@dataclass
class Batch:
product: str
quantity: int
expires_at: int
@dataclass
class Allocation:
batch: Batch
quantity: int
class Clock:
def now(self) -> int:
...
"""
Warehouse
- represents a warehouse that can fulfill orders of products
- contains a list of batches
- contains a clock that tells us the current time
- fulfill():
- an action that takes in a requested mapping of product -> quantity and attemps to fulfill the request
- set an empty list of allocations
- from each batch in the warehouse, build a reverse index dict of product -> list(Batch), where the list is sorted in increasing expire order (earliest first)
- for each product in request:
- look up how much quanttiy we have in our reverse index that is unexpired
- if the product cannot be fulfilled, return None
- if the product can be fulfilled, create the necessary allocations (representing deductions for however many batches are necessary to fulfill the requested product), appending to our list of allocations
- if we have gone through every product then the request can be fulfilled
- for each alloaction, apply it, and return the list
-fulfill():
- create reverse index: product name -> list of non expired batches
- sort each list by expiry date
- for every product in the request, attempt to fulfill it. if unfulfillable return None immediately. else, append to a running list of allocations
- if every product in your request is fulfillable, carry out the deductions in your allocations and return the allocations
"""
class Warehouse:
def __init__(self, batches: list[Batch], clock: Clock):
self.batches = batches
self.clock = clock
def fulfill(
self,
requested: dict[str, int],
) -> Optional[list[Allocation]]:
"""
Fulfill every requested product using unexpired stock.
For each product, use batches that expire sooner before batches
that expire later. A request may be split across multiple batches.
Return the allocations and deduct the allocated quantities.
If the complete order cannot be fulfilled, return None and
leave every batch unchanged.
"""
product_to_batches = defaultdict(list)
for batch in self.batches:
if (
batch.expires_at > self.clock.now()
and
batch.quantity > 0
and
batch_product in requested
):
product_to_batches[batch.product].append(batch)
for product in product_to_batches:
product_to_batches[product].sort(key= lambda x: x.expires_at)
for product in requested:
available_batches = product_to_batches[batch.product]