Back to Labs

Functional Programming with Discounts

Adding Functionality to your Ruby Code

Many programmers start out with languages that favor procedural programming. Because of this, it is tempting to use procedural constructs (while and .each loops) when writing an algorithm in Ruby, even though there may be clearer ways to express the algorithm. This blog post will describe several problems, and compare a procedural implementation and a functional implementation.

Overview

Let’s start with some definitions and a simple example. The example we will use below is the common, but simple, problem of summing a list of numbers. We begin with a list of numbers and the result is the number that is equal to adding all elements of the list together.

Procedural programming

Procedural programming uses a list of instructions to tell the computer what to do step-by-step

— http://study.com/academy/lesson/object-oriented-programming-vs-procedural-programming.html

A procedural implementation to sum a list of numbers would look something like this.

sum = 0
[1, 2, 3].each do |number|
  sum += number
end

We start out by initializing a local variable to keep track of the sum. We then iterate through the list of numbers and add each number to the sum.

Functional programming

Functional programming is a programming paradigm … that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data

— https://en.wikipedia.org/wiki/Functional_programming

A functional implementation for the list summation problem would be to “reduce” the list by adding each number to an accumulator.

sum = [1, 2, 3].reduce(0) do |sum, number|
  sum + number
end

The difference between the two implementations is that functional programming does not require the sum variable to be altered as the algorithm progresses.

This is a very simple example, so it might not be clear yet what the advantages are of a functional approach. Let’s look at a more complicated example to see how functional and procedural programming compare in a practical application.

Free Item Discount Example

Background

Discounts are essential on ecommerce websites, and WebLinc’s platform supports many different kinds. One of the more popular discounts is the free item discount. The specific free item discount we will discuss is a “Buy 2 of X and/or Y, Get 1 Z”. The discount can be stated more generically as “Buy N from a list of products and Get 1 of the free product”.

jefferspet-dev-free-discount-admin
Free Item Discount Setup in WebLinc Admin

 

jefferspet-dev-free-discount-cart
Qualifying for Free Item Discount in Cart

Implementation Overview

A discount is applied to a cart which is a collection of items. We will represent an item in the cart with this item class.

Item = Struct.new(:product_id, :quantity)

A real cart item has many more fields (price, product name), but for this example we will only focus on the product ID and the quantity. The product ID will be used to represent the X, Y and Z in “Buy 2 of X and/or Y Get 1 Z”. The quantity is necessary because multiples of the same product can contribute to the discount.

Each implementation of the free item discount will conform to the following abstract class.

class FreeItemDiscount
 
  # @param free_item_product_id
  #   Product ID for the Free Item
  #
  # @param product_ids
  #   The Product IDs that qualify for the discount
  #
  # @param quantity
  #   The quantity of product_ids that must be in the list of items
  #     in order to qualify for the discount
  #

  def initialize(free_item_product_id, product_ids, quantity)
    @free_item_product_id = free_item_product_id
    @product_ids = product_ids
    @quantity = quantity
  end

  def free_item(items: [])
    raise "#{self.class.to_s} must implement #free_item"
  end

  def qualifies?(items: [])
    raise "#{self.class.to_s} must implement #qualifies?"
  end

  private

  def item_qualifies?(item)
    @product_ids.include?(item.product_id)
  end

end

In addition to the two abstract methods free_item and qualifies?, we define a helper method, item_qualifies?, which is used in each implementation. This method returns true if the item’s product ID is in the list of the discount’s eligible product IDs. We will focus our discussion on the implementation of qualifies?.

Procedural Implementation

The procedural implementation of qualifies? creates two local variables to keep track of whether or not the discount qualifies (does_qualify), and the quantity of items that qualify for the discount (quantity_of_qualifying_items)

While iterating through the items we check to see if the item qualifies for the discount. If the item does qualify for the discount, we increment the quantity_of_qualifying_items counter by the quantity of that item. Once we have the updated quantity_of_qualifying_items, we check to see if that quantity is greater than the quantity of items needed to qualify for the discount. If this quantity is greater, the cart qualifies for the discount and we can stop iterating.

class ProceduralFreeItemDiscount < FreeItemDiscount

  def qualifies?(items: [])
    does_qualify = false
    quantity_of_qualifying_items = 0
    items.each do |item|
      if item_qualifies?(item)
        quantity_of_qualifying_items += item.quantity
      end

      if quantity_of_qualifying_items >= quantity_needed_per_discount
        does_qualify = true
        break
      end
    end
    does_qualify
  end

  private

  def quantity_needed_per_discount
    @quantity
  end 

end

Functional Implementation

The functional implementation of qualifies? checks to see if the discountable_quantity is greater than 0. The discountable_quantity is equal to the number_of_qualifying_items in the cart divided by the discount’s eligible quantity. We calculate the number_of_qualifying_items by adding together the quantity of all qualifying items.

class FunctionalFreeItemDiscount < FreeItemDiscount

  def qualifies?(items: [])
    discountable_quantity(items) > 0
  end
  private

  def discountable_quantity(items)
    number_of_qualifying_items(items) / @quantity
  end

  def number_of_qualifying_items(items)
    qualifying_items(items).sum(&:quantity)
  end

  def qualifying_items(items)
    items.select do |item|
      item_qualifies?(item)
    end
  end

end

Comparison of Implementations

The procedural and functional approaches implement the same method and return the same results, but the code is quite different. The procedural implementation modifies several state variables while iterating through the list of items, and it will break when it determines that it qualifies for the discount. The functional implementation does not use any state variables and, at each step of the algorithm, returns an object (either an integer or an array).

The advantage of the procedural implementation is that it takes a step-by-step approach that is intuitive for many developers. However, it is difficult to understand each step independent of each other since each step of the algorithm requires something being done before it.

The functional implementation steps are easier to distinguish from one another since they are separate methods. The code is self-documenting with method names. The code is easier to debug since you can use a single method call to evaluate the result at each step.

Recommendations

It is important to remember that each approach is useful in different situations. One approach that I’ve been successful with is starting out by implementing an algorithm procedurally. This is often the most intuitive way to write the algorithm, especially if you are following test-driven development. After finishing the implementation, I try out a functional approach. I’ve found that when I am returning to look at my own code, or reading someone else’s, it is easier to read and re-use functional code.

See GitHub for source code of the discounts used in this post.

Leave a Comment

Your email address will not be published. Required fields are marked *

Top of Page