The Same Algorithm in Four Languages
C++, JavaScript, TypeScript, Python: four addItems, one algorithm. The concept, the eight-line pseudocode and the six examples do not change; the find is a for loop with a pointer, an arrow function, the same with a type, or a generator with next. What varies is syntax and a handful of language decisions — which is what makes the algorithm the thing worth learning.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
You implemented addItem in TypeScript. What changes when you write it in Python or C++, what does not — and what does that tell you about what you actually learned?
The TypeScript cart works. A colleague needs the same logic in Python for a service, and you feel you would have to start over — as if the cart you built were a TypeScript cart, and a Python cart were a different thing you have not learned yet.
Search for "shopping cart Python" and adapt what comes back, because Python is a different language and presumably has a different way of doing carts.
The Python result has a different algorithm — a dict keyed by product, no catalog check, quantities that can be zero — and the two carts now disagree on the examples. The colleague's service and your app have different rules for the second add, and neither of you decided that.
- The Python result has a different algorithm — a dict keyed by product, no catalog check, quantities that can be zero — and the two carts now disagree on the examples. The colleague's service and your app have different rules for the second add, and neither of you decided that.
- The search was for the language when the thing to translate was eight lines of pseudocode you already had. The cart was never a TypeScript cart; treating it as one meant the pseudocode, the examples and the tests were left behind and only the syntax came along.
- Having "learned" the cart in TypeScript, you cannot tell which of the things you know are the cart's and which are TypeScript's —
findreturningundefined,===,push— so every new language looks like a new cart.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Fix what must not change before touching the second language: the concept, the state (items with product id and quantity), the eight pseudocode lines, the six examples and the tests derived from them. That is the algorithm and its behaviour. The translation is judged by one thing — the same tests, in the new language, pass (Examples Become Tests).
- Translate the pseudocode, not the TypeScript. Each pseudocode line has a language-specific idiom:
find … with …is.find(arrow)in JavaScript and TypeScript,next((i for i in items if …), None)in Python, aforloop returning a pointer in C++.rejectisthrow new Error,raise ValueError,throw std::invalid_argument. Going pseudocode → language keeps the language's decisions from the first translation out of the second. - List what the new language decided differently, the way you did for the first (Implement One Operation). Python:
Nonefor absent,is Nonefor the check, dict access for fields. C++: a pointer that is null for absent,push_back, an explicitfindItemfunction because there is no one-liner. These are facts about languages; the algorithm has not moved. - Read the four versions side by side and name what is identical: the order of the checks, the find before the append, the branch on existence, the increment by the given quantity, the return of the cart. What you learned when you built the cart is that list — and it is what transfers, which is why the four-language view exists (Programming, Design, Engineering).
The find, in two languages
The record's add_item in Python beside the TypeScript from the previous lesson. Read the two finds first: an arrow function against a generator with next, both an O(n) scan, both returning an absent-value the next line branches on, both carrying the same comment. Then read the rest and count the lines that differ in more than syntax — there are none.
1export function addItem(cart: Cart, productId: ProductId, quantity = 1): Cart {2 if (!catalog.has(productId)) throw new Error('unknown product')3 if (quantity <= 0) throw new Error('quantity must be positive')4 const existing = cart.items.find((i) => i.productId === productId) // one entry per product5 if (existing) existing.quantity += quantity6 else cart.items.push({ productId, quantity })7 return cart8}What each language decided
The record's Python first — the same eight pseudocode lines, with next and a generator where TypeScript had an arrow — then the table of what to look for across all four (the concept ladder on this page shows them). Every row of the table is a fact about a language; no row is a fact about the cart. The C++ row's "explicit loop" is the same scan the others hide in one call — the complexity is O(n) in all four, and only C++ makes you watch it.
| Pseudocode line | JavaScript / TypeScript | Python | C++ |
|---|---|---|---|
| reject "…" | throw new Error(…) | raise ValueError(…) | throw std::invalid_argument(…) |
| find entry … with … | .find((i) => i.productId === productId) | next((i for i in items if …), None) | an explicit for loop returning CartItem* — the scan written out |
| if item exists | truthiness of the object or undefined | truthiness of the dict or None | a non-null pointer |
| item.quantity + quantity | existing.quantity += quantity (in place) | existing["quantity"] += quantity (in place) | existing->quantity += quantity (in place) |
| append … to cart.items | cart.items.push({…}) | cart["items"].append({…}) | cart.items.push_back({…}) |
| types | TS: annotations on the same lines; JS: none | none | struct CartItem, struct Cart |
1def add_item(cart, product_id, quantity=1):2 if product_id not in CATALOG:3 raise ValueError("unknown product")4 if quantity <= 0:5 raise ValueError("quantity must be positive")6 existing = next((i for i in cart["items"] if i["product_id"] == product_id), None) # one entry per product7 if existing:8 existing["quantity"] += quantity9 else:10 cart["items"].append({"product_id": product_id, "quantity": quantity})11 return cartLine for line against the TypeScript above: raise for throw, next(…, None) for .find, is-style truthiness on a dict for truthiness on an object, append for push. Nothing about the cart moved.
What stays fixed, level by level
The ladder is the point of the lesson: the levels that do not move when the language changes are what was learned when the cart was built. Syntax is the bottom rung. Everything above it transferred to Python untouched, and would to a fifth language.
- ConceptA temporary collection of products the user intends to purchase — unchanged. — It was never about code; the Python service and the TypeScript app hold the same idea.
- State and rulesItems with product id and quantity; one entry per product; quantity > 0; unknown products rejected — unchanged. — They were derived from meaning and examples, neither of which has a language.
- AlgorithmThe eight pseudocode lines: check, check, find, branch, increment or append, return — unchanged. — The pseudocode was written language-neutral so that this level would not move.
- BehaviourThe six examples and the tests from them: the same after in every language. — Behaviour is the definition of "same algorithm"; the tests are how the claim is checked rather than asserted.
- Language decisionsAbsent value, rejection mechanism, mutation, whether the find is a one-liner. — Each language answers these its own way; the answers are short and worth a note, and they are all that has to be re-learned.
- SyntaxArrows, colons, braces,
->. — The only level that is entirely about the language — and the one a learner mistakes for the whole.
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.
- Does a cart have identity? Yes, weakly. Two carts with the same items are still two carts, because each belongs to someone and will become a different order. It needs an id once it leaves memory; in memory the variable is the identity.
- Who owns it? A shopper — a logged-in user or an anonymous session. The owner is part of the state because "my cart" has to be findable again.
- How long does it exist? From the first add until checkout or abandonment. Whether it survives a reload, a closed browser or a login is not a property of the concept; it is a persistence decision made later, and each answer changes where the cart lives.
- Should it survive reload? Usually yes for a store, usually no for a demo. V1 in memory says no; V2 browser storage says yes on one device; V3 server storage says yes everywhere the user is logged in.
- Should it survive login? Only if an anonymous cart and a logged-in cart are merged — a rule that does not exist in V1 and appears as a modification later.
- itemscollection of CartItemkeepThe cart is its items; without them nothing else means anything.
- items[].productIdidkeepThe reference to what is being bought. The catalog owns the product; the cart only points at it.
- items[].quantityinteger > 0keepTwo laptops is one entry with quantity 2, not two entries — the rule "one entry per product" needs a quantity to hold.
- owneruser id or session iddependsSo the cart can be found again by the person it belongs to.
- items[].productNamestringderiveIt would be convenient to render the cart without a catalog lookup.
- items[].pricemoneydependsThe total needs a price per item.
- totalmoneydropEvery screen shows the total.
- currencycodedependsPrices need a currency to be added.
- createdAttimestampdropAbandoned carts might be expired or emailed about.
- update Add item — the updated cart
- delete Remove item — the updated cart
- update Change quantity — the updated cart
- read View items — the list of entries — product id and quantity — for rendering
- domain Calculate total — the sum of price × quantity over the entries
- delete Clear cart — the empty cart
- • Every quantity is greater than zero.
- • One logical entry per product.
- • The total is never negative.
- • An unknown product cannot be added.
- • Quantity cannot exceed available stock — if inventory is enforced here.
How to do it
Most important first.
- Before the second language, write the tests in it first, translated from the examples; they are the definition of "same algorithm" (Predict the State Before Running the Code).
- Translate from the pseudocode line by line, never from the first language's code; the first language's idioms are not part of the algorithm.
- Keep the rule comments — "one entry per product" on the find — in every language; they are the algorithm's names for its own lines.
- Make a two-column note per language: what it decided (absent value, rejection mechanism, mutation) and what it did not (everything in the pseudocode).
- When a language makes a rule structural — a dict's keys cannot repeat — note that the representation changed, not the algorithm, and that "view in order" may have changed with it (Array Cart vs Map Cart).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- TypeScript → Python, from the record.
cart.items.find((i) => i.productId === productId)becomesnext((i for i in cart["items"] if i["product_id"] == product_id), None);if (existing)becomesif existing:;throw new Error('unknown product')becomesraise ValueError("unknown product");cart.items.push({…})becomescart["items"].append({…}). The comment "one entry per product" is on the same line in both. The add-again test passes in both:[{"product_id": "laptop", "quantity": 2}]. - TypeScript → C++. There is no one-line find, so the record writes
findItemas a loop returning aCartItem*— "the scan the map would replace", as its comment says — andaddItembranches on the pointer. The loop is the same O(n) scan the arrow function performs; C++ merely makes you write it out, which is a good way to see whatfindwas doing all along. - Something that did change: JavaScript and TypeScript share every line except the type annotations, which is why the record's two versions differ only in
: Cart,: ProductIdandSet<ProductId>. That is the smallest possible demonstration that types are a layer over the algorithm, not part of it.
How you know it worked
What now exists that did not before, and what question you can now ask.
- The same three tests pass in every language, and each was translated from the same example.
- You can list, per language, what it decided — and the list is short and about absence, rejection and mutation, never about the cart.
- Shown a fifth language, you expect to translate the pseudocode in an hour and can say which three decisions you will have to look up.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?What is fixed before the second language — and is it the pseudocode and the tests, or the first language's code?
- ?For each pseudocode line, what is the new language's idiom for it?
- ?What did this language decide that the pseudocode did not — absence, rejection, mutation?
- ?Reading the four side by side, what is identical — and is that list what I thought I had learned?
What can go wrong
- The second language is translated from the first language's code, and TypeScript's truthiness check becomes Python's
if existing:on a dict — which works — and then C++'sif (existing)on a pointer — which works — and nobody notices that "exists" was decided three different ways by accident. - The four versions are read for their differences and the learner concludes that they are four algorithms with a family resemblance.
- Idiom is mistaken for algorithm: Python's dict makes "one entry per product" structural, and the learner reports that Python's cart "does not need the find" — the representation changed and the rule moved into it, which is a different lesson.
- Four languages is three more than the project needs, and the time spent on C++ is time not spent on the cart's next operation.
- Translating from pseudocode rather than from working code is slower for a bilingual engineer who can see the idiom mapping directly.
- Language-neutral thinking can undervalue idiom: a Python cart that ignores dicts to preserve the array-and-scan is faithful to the pseudocode and slightly foreign to a Python reader.
- "So the language does not matter." It matters for idiom, performance, tooling and the team; it does not change the algorithm. The falsifiable form is "the same tests pass in every language with the same pseudocode behind them", and that is what the four versions demonstrate.
- "Learn one language deeply before any other." Precise for syntax and tooling; as a claim about the cart it is backwards — the algorithm is learned once and the second language is where you find out whether you learned the algorithm or the idiom.
- "Four languages means four implementations to maintain." The concept ladder shows four to teach what is stable; a project ships one. Nothing in the lesson says to keep four.
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- GENERALFixing the pseudocode and the tests, translating line by line, and listing each language's decisions is the same move for any operation on any concept; the three decisions to look up — absence, rejection, mutation — recur across languages.
- DOMAIN-SPECIFICFor an algorithm whose point *is* the language — memory layout in C++, async in JavaScript — the "language decides nothing" claim weakens, and the translation changes the algorithm's shape; the cart is chosen because it is not such a case.
- ILLUSTRATIVEThe four versions, the hour for a fifth language and the colleague's Python service are the concept record's invented example; the languages' behaviours are real, the store is not.
Where the depth lives
This domain asks the question and hands the answer off by name.