ESC

Type to search the knowledge base.

Method-Chaining Calculator

Build a calculator object with add, subtract, multiply, divide, and a terminal value. Mutable this vs immutable copies, and divide-by-zero as an API choice.

beginner5 min read
  • javascript
  • oop
  • chaining
  • interview

The prompt is calc.add(5).multiply(2).subtract(3).value() and they want a number. People either forget to return anything (chain dies after add) or they return this.value from every method (chain dies with a number in the middle). Both fail the same way: the next identifier is not a calculator.

A method-chaining calculator is an object whose mutating operations return a calculator, plus a terminal read that returns a number. The design fork is whether that returned calculator is the same object (return this) or a new one. Interviews usually want this. Production code that shares the instance across callers often wants copies.

Divide-by-zero is not a math trivia question. IEEE 754 gives Infinity. Your API may still throw. Pick one and say it before you type.

  • Chain methods return a calculator. The terminal method (or getter) returns the number.
  • return this mutates one instance. Returning new Calculator(next) is referentially transparent.
  • Interviews usually ask for this. Say the tradeoff in one sentence.
  • Divide by zero: throw, or return Infinity / -Infinity / NaN as the spec’s / would.
  • Don’t hide the total behind a get value() unless you also keep a value() method if the prompt used parens.

Return this, or return a new calculator

Fluent APIs work because each step has a receiver that still has the methods. this is that receiver for ordinary methods. Arrows as class fields would freeze this to the instance (often fine) or to undefined in a loose object literal (not fine). Use methods.

Mutable: return this

function createCalculator(initial = 0) {
  let value = initial;

  const api = {
    add(n) {
      value += n;
      return api;
    },
    subtract(n) {
      value -= n;
      return api;
    },
    multiply(n) {
      value *= n;
      return api;
    },
    divide(n) {
      if (n === 0) throw new RangeError('Division by zero');
      value /= n;
      return api;
    },
    value() {
      return value;
    },
  };

  return api;
}

createCalculator(10).add(5).multiply(2).subtract(4).divide(2).value();
// 13

A class is the same contract with this._value instead of a closed-over value. The factory above avoids leaking _value as a writable field. Classes are fine if the interviewer started with class.

class Calculator {
  constructor(initial = 0) {
    this._value = initial;
  }
  add(n) {
    this._value += n;
    return this;
  }
  subtract(n) {
    this._value -= n;
    return this;
  }
  multiply(n) {
    this._value *= n;
    return this;
  }
  divide(n) {
    if (n === 0) throw new RangeError('Division by zero');
    this._value /= n;
    return this;
  }
  value() {
    return this._value;
  }
}

new Calculator(2).add(3).multiply(4).value(); // 20

Alias hazard: const a = new Calculator(1); const b = a.add(1); then a and b are the same object. b.multiply(10) changes what a.value() returns. That is the cost of return this.

Immutable: return a new calculator

class ImmutableCalc {
  constructor(initial = 0) {
    this._value = initial;
  }
  add(n) {
    return new ImmutableCalc(this._value + n);
  }
  subtract(n) {
    return new ImmutableCalc(this._value - n);
  }
  multiply(n) {
    return new ImmutableCalc(this._value * n);
  }
  divide(n) {
    if (n === 0) throw new RangeError('Division by zero');
    return new ImmutableCalc(this._value / n);
  }
  value() {
    return this._value;
  }
}

const start = new ImmutableCalc(1);
const plusOne = start.add(1);
start.value(); // 1
plusOne.value(); // 2

Each step allocates. Chains with undo, React state, or “pass this snapshot to a child” want this shape. See immutability patterns. On a 20-minute whiteboard, return this is what they are scoring unless they said “don’t mutate.”

Style add returns Alias safety Typical prompt
Mutable this no “chain add/multiply, then value”
Immutable new instance yes “each call returns a new calculator”

Divide by zero is an API choice

JavaScript’s / does not throw:

1 / 0; // Infinity
-1 / 0; // -Infinity
0 / 0; // NaN

That is IEEE 754, covered in number precision. A calculator that silently returns Infinity will then multiply(2) into more infinities and look “working” until a UI prints Infinity. Throwing RangeError fails the chain immediately. Either is defensible. Mixing them (throw on 0, but not on 0 / 0 via a previous NaN) is how you look unsure.

If you allow NaN inputs, every later operation stays NaN. Reject non-finite n at the boundary if the prompt is a numeric keypad.

Terminal read

Prompts vary:

  • .value() method
  • .equals() / .result()
  • getter get value() so .value has no parens

A getter is fine (getters and setters), but calc.value without calling it still needs to be a number, and you lose a place to put breakpoints. If the example in the prompt has (), write a method.

Don’t toString the object into a number unless they asked for +calc or template interpolation. That is the infinite currying trick, not this API.

Calculator footguns

  1. Returning this._value from add. Chain breaks. Return the object.
  2. Using arrows on an object literal. add: (n) => { this._value += n; return this; } takes this from the enclosing scope, not the calculator.
  3. Floats. 0.1 + 0.2 is not 0.3. Integer money does not belong in this calculator.
  4. Order. add(1).multiply(2) is not multiply(2).add(1). Chain order is evaluation order, left to right, no operator precedence between methods.
  5. Sharing a mutable instance from a module. export const calc = new Calculator() is a global. Two features chaining it will clobber each other.

Interview out loud

“Each of add, subtract, multiply, and divide updates the total and returns the calculator so the next method exists. value() is the terminal read and returns a number. I will mutate this and return this unless you want immutable snapshots, in which case each method returns a new instance. Divide by zero: I throw RangeError rather than handing back Infinity, and I will switch if you prefer IEEE behavior. I will not use arrows for the methods, because this has to be the calculator. Alias risk: two variables holding the mutable instance are the same object.”

Further reading

Related guides