2
\$\begingroup\$

Motivation

I have worked extensively with JavaScript and have grown fond of some of its functional aspects. This got me wondering how one could implement the canonical example of a bank account - and a small application using it - in a purely functional manner in ES6.

Code

I introduce three types Account, Shop and Customer, all of which are defined through their respective factories. The resulting objects contain impure log methods, but I am unsure as to how I could get rid of that. I also introduce a type Iterable, that models a series of iterations.

Next, a very impure function iterateOnClick, which has the side effect of having each click on the buy-button execute one iteration of a given Iterable, is introduced

Penultimately, I introduce a factory makePurchaseIteration to create an Iterable which will let a customer buy kittens until they run out of funds and finally, a main function is defined and run which simply sets up the desired iteration.

/* --------------------------------- TYPES ---------------------------------- */
/*
 * Account
 *   Models a bank account.
 *
 * Fields:
 *   balance: number
 *     Current amount of dollars in account.
 * Methods:
 *   deposit: (amount: number) => Account
 *     Returns Account with balance increased by ${amount}.
 *   withdraw: (amount: number) => Account
 *     Returns Account with balance decreased by ${amount}.
 *   log: () => undefined
 *     Side effect: Logs the current balance.
 *
 * Factories: 
 *   Account: () => Account
 *     Creates Account with balance 0.
 *   Account: (initialAmount: number) => Account
 *     Creates Account with balance ${initialAmount}.
 */
const Account = (() => {
  const makeAccount = (slips) => {
    
    // Fields
    const balance = slips.reduce((sum, value) => sum + value);
    
    // Methods
    const deposit = (amount) => makeAccount([...slips, amount]);
    const withdraw = (amount) => makeAccount([...slips, -amount]);
    const log = () => console.log(balance);
  
    return Object.freeze({
      // Fields
      balance,
      
      // Methods
      deposit,
      withdraw,
      log,
    });
  };
  
  // Factory
  return (initialAmount = 0) => makeAccount([initialAmount]);
})();

/*
 * Customer
 *   Models a customer who may buy kittens.
 *
 * Fields:
 *   balance: number
 *     Current funds in dollars.
 * Methods:
 *   buyKitten: (price: number) => Customer
 *     Returns a Customer with one more kitten and ${price} less funds.
 *   log: () => undefined
 *     Side effect: Logs the current funds and number of kittens.
 *
 * Factories: 
 *   Customer: (funds: number) => Customer
 *     Creates Customer with no kittens and $${funds}.
 */
const Customer = (() => {
  const makeCustomer = (account, kittenCount) => {
    
    // Fields
    const {balance} = account;
    
    // Methods
    const buyKitten = (price) =>
      makeCustomer(account.withdraw(price), kittenCount + 1);
    const log = () => 
      console.log(`You have $${balance} and ${kittenCount} kittens!`);
    
    return Object.freeze({
      // Fields
      balance,
      
      // Methods
      buyKitten,
      log,
    });
  };
  
  // Factories
  return (funds) => makeCustomer(Account(funds), 0);
})();

/*
 * Shop
 *   Models a shop which sells kittens.
 *
 * Fields:
 *   price: number
 *     Current price for one kitten.
 * Methods:
 *   sellKittenTo: (customer: Customer) => {customer: Customer, shop: Shop}
 *     Returns Customer and Shop representing the state after selling one
 *       additional kitten to customer.
 *   log: () => undefined
 *     Side effect: Logs the current price for kittens.
 *
 * Factories: 
 *   Shop: (funds: number, price: number) => Shop
 *     Creates Shop with $ ${funds} selling kittens at $${price}.
 */
const Shop = (() => {
  const makeShop = (account, price) => {
  
    // Methods
    const sellKittenTo = (customer) => {
      return {
        customer: customer.buyKitten(price),
        shop: makeShop(account.deposit(price), price + 2),
      };
    };
    const log = () => 
      console.log(`The shop is selling a kitten for $${price}.`);
    
    return Object.freeze({
      // Fields
      price,
      
      // Methods
      sellKittenTo,
      log,
    });
  };
  
  return (funds, price) => makeShop(Account(funds), price); 
})();

/*
 * Iterable<R>, where typeof R !== 'function'
 *   Models the iteration of a loop.
 *
 * Fields:
 *   isFinished: bool
 *     Indicates whether this iteration is the final one.
 *   returnValue: R
 *     The value computed by the iteration (undefined if not finished).
 * Methods:
 *   next: () => Iterable<R>
 *     Function to perform iteration and return next Iterable.
 *
 * Factories: 
 *   Iterable: (nextOrReturnValue: () => Iterable<R>) => Iterable<R>
 *     Creates an iterable which is not finished.
 *   Iterable: (nextOrReturnValue: R) => Iterable<R>
 *     Creates an iterable which is finished.
 */
const Iterable = (nextOrReturnValue) => {
    
  // Fields
  const isFinished = typeof nextOrReturnValue !== 'function';
  const returnValue = isFinished ? nextOrReturnValue : undefined;
  
  // Methods
  const next = isFinished ? undefined : nextOrReturnValue;
  
  return Object.freeze({
    // Fields
    isFinished,
    returnValue,

    // Methods
    next,
  });  
};

/* ------------------------------ FUNCTIONS --------------------------------- */

/* 
 * iterateOnClick: (iterable: Iterable<R>) => undefined
 *   Side effect: Sets the onclick response of the 'buy' button to iterate
 *     through the Iterable and set its label to the return value of
 *     the iteration once it has finished.
 */
const iterateOnClick = (iterable) => {
    const element = document.getElementById('buy');
    element.onclick = () => {
      const nextIterable = iterable.next();
      if (nextIterable.isFinished) {
        element.onclick = undefined;
        element.innerHTML = nextIterable.returnValue;
      }
      else {
        iterateOnClick(nextIterable);
      }
  };
};

/* 
 * makePurchaseIteration: ({customer: Customer, shop: Shop}) => Iterable<string>
 *   Creates an Iterable which buys kittens until the money runs out.
 */ 
const makePurchaseIteration = ({customer, shop}) => {
  if (customer.balance < shop.price) {
    return Iterable(`You cannot afford more kittens :(`);
  }
  return Iterable(() => {
    const state = shop.sellKittenTo(customer);
    state.customer.log();
    state.shop.log();
    return makePurchaseIteration(state);
  });
};
  
/* --------------------------------- MAIN ----------------------------------- */
  
const main = () => {

  const customer = Customer(305);
  const shop = Shop(6000, 10);
  customer.log();
  shop.log();
  
  iterateOnClick(makePurchaseIteration({customer, shop}));
};

main();
<body>
  <button id='buy'>
     BUY MORE KITTENS!
  </button>
</body>

Desired Feedback

I am looking for feedback particularly on the documentation and whether this is the 'proper' way to solve this problem in functional programming.

I am not looking for alternative solutions that use mutable data or impure functions (apart from where it cannot be avoided).

\$\endgroup\$
  • \$\begingroup\$ Immutable state is not a great way to model an account balance. \$\endgroup\$ – Roamer-1888 Nov 5 '16 at 10:14
1
\$\begingroup\$

In terms of documentation, JSDoc is a formal syntax for documentation. Some IDEs even use the JSDoc syntax as a pseudo-type system, giving you warnings when your types don't match up.

Another problem is your excessive usage of arrow functions. One problem with arrow functions is that in some platforms, their names won't appear in the stack trace. They will appear as "anonymous function". Although you will be provided the file name with line and column numbers, having that name is a leg up when debugging. Use function declarations instead.

const Account = (() => {
  const makeAccount = (slips) => {
    ..
  };

  // Factory
  return (initialAmount = 0) => makeAccount([initialAmount]);
})();

I suggest putting makeAccount outside Account. You're getting no benefit from it, you're also not closing over some value. I believe you're only using it to default the function to a value.

Default values often make functions hard to explain. Instead of blowing up when nothing was given, it performs an operation as if something was given. Why not give the value in the first place to make the function straightforward?

return Object.freeze({
  // Fields
  balance,

  // Methods
  deposit,
  withdraw,
  log,
});

Object.freeze is unnecessary. Although it gives you extra protection from outside forces trying to alter your data, if it's just your code and you're writing it in a non-mutating fashion, then this is unnecessary.

Also, I'd like to think of functional programming as working with simple data structures and using functions to transform them. By this definition, log becomes out of place since it's not a value related to your data but a handy logging function.

Since the values are plain objects and arrays, just feeding your data to console.log, optionally serializing them with JSON.stringify should do. No need for a log method in your data.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.