DEV Community

Riccardo Odone
Riccardo Odone

Posted on • Originally published at odone.io

How would I do it in Haskell?

You can keep reading here or jump to my blog to get the full experience, including the wonderful pink, blue and white palette.


Since I started playing with functional programming, not only I've dot-chained the hell out of object-oriented code, but I often catch myself thinking: how would I do this in Haskell?

Here's a couple of examples that come to mind where I contaminated Ruby with functional intuitions.

Nested Loops and Filtering

During the Global Day Of Code Retreat, together with Joanna, we were writing a method to find the eight neighbors of a cell. In other words, given [ 1, 1 ] as an input we wanted to generate the following output:

[ [ 0, 0 ], [ 0, 1 ], [ 0, 2 ]
  [ 1, 0 ],         , [ 1, 2 ]
  [ 2, 0 ], [ 2, 1 ], [ 2, 2 ]
]
Enter fullscreen mode Exit fullscreen mode

We started with a simple implementation:

def neighbors(cell)
  [ -1, 0, 1 ].map do |y|
    [ -1, 0, 1 ].map do |x|
       [ cell.first + y, cell.last + x ]
    end
  end
end

# [1, 1]
# [[[0, 0], [0, 1], [0, 2]], [[1, 0], [1, 1], [1, 2]], [[2, 0], [2, 1], [2, 2]]]
Enter fullscreen mode Exit fullscreen mode

To remove the double nesting in the output, we reached for flatten:

def neighbors(cell)
  [ -1, 0, 1 ].map do |y|
    [ -1, 0, 1 ].map do |x|
       [ cell.first + y, cell.last + x ]
    end
  end.flatten # <=
end

# [1, 1]
# [0, 0, 0, 1, 0, 2, 1, 0, 1, 1, 1, 2, 2, 0, 2, 1, 2, 2]
Enter fullscreen mode Exit fullscreen mode

Oops, that was one flatten too much. We tried again with flat_map:

def neighbors(cell)
  [ -1, 0, 1 ].flat_map do |y| # <=
    [ -1, 0, 1 ].map do |x|
       [ cell.first + y, cell.last + x ]
    end
  end
end

# [1, 1]
# [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]]
Enter fullscreen mode Exit fullscreen mode

Yay! The last piece was to remove the current cell from the output:

def neighbors(cell)
  [ -1, 0, 1 ].flat_map do |y|
    [ -1, 0, 1 ].map do |x|
       [ cell.first + y, cell.last + x ]
    end
  end.reject { |neighbor| neighbor == cell } # <=
end

# [1, 1]
# [[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]]
Enter fullscreen mode Exit fullscreen mode

There was something inelegant about that code, though. To hell with the exercise, we wasted the rest of the session refactoring the method.

The nested loop looked ugly. Not to count we were generating an invalid neighbor, only to filter it out later. Here's what we came up with:

def neighbors(cell)
  [ -1, 0, 1 ]
    .repeated_permutation(2)
    .reject { |permutation| permutation == [ 0, 0 ] }
    .map { |y, x| [ cell.first + y, cell.last + x ] }
end

# [1, 1]
# [[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]]
Enter fullscreen mode Exit fullscreen mode

A Semigroup in the Wild

A few days ago, I stumbled upon the following code in a production codebase:

def group_by_product(items_grouped_by_template)
  items_grouped_by_template.select { |item| item.fetch(:variation_type) == 'quantity' }
                           .group_by { |item| item[:product_id] }
                           .each_with_object([]) do |(_product_id, order_items), array|
    array << item_grouped_by_product(order_items)
  end
end

def items_grouped_by_template
  @order_items_grouped_by_template.each_with_object([]) do |(template, order_items), array|
    array << item_grouped_by_template(template, order_items)
  end
end

def item_grouped_by_template(template, order_items)
   {
     id: template.id,
     variation_type: template.product_template.template_type,
     variation_name: template.product_template.name,
     product_id: template.product.id,
     product_name: template.product.name,
     total_quantity: product_template_total_quantity(template, order_items),
     reporting_category: reporting_category(template),
   }
end

def item_grouped_by_product(order_items)
   {
     id: order_items.first.fetch(:id),
     variation_type: order_items.first.fetch(:variation_type),
     product_id: order_items.first.fetch(:product_id),
     product_name: order_items.first.fetch(:product_name),
     total_quantity: product_total_quantity(order_items),
     reporting_category: order_items.first.fetch(:reporting_category),
   }
end
Enter fullscreen mode Exit fullscreen mode

If you don't understand it, don't worry, I'm there with you. I speculate the author didn't understand it, either.

However, after massaging the code for a while, I noticed one thing: there's a hidden data structure that gets combined with itself: I found a semigroup!

I created a Summary that can be concated with another one:

Summary = Struct.new(
  :id,
  :variation_type,
  :variation_name,
  :product_id,
  :product_name,
  :total_quantity,
  :category,
  keyword_init: true
) do
  def concat(other)
    Summary.new(
      id: id,
      variation_type: variation_type,
      variation_name: "#{variation_name} + #{other.variation_name}",
      product_id: product_id,
      product_name: product_name,
      total_quantity: [self, other].map(&:total_quantity).reduce(:+),
      category: category
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

With that in place, I could refactor to something similar to what follows:

product_product_templates
  .map { |product_product_template| to_summary(product_product_template) }
  .reduce { |acc, summary| acc.concat(summary) }

def to_summary(product_product_template)
  Summary.new(
    id: product_product_template.id,
    variation_type: product_product_template.product_template.template_type,
    variation_name: product_product_template.product_template.name,
    product_id: product_product_template.product.id,
    product_name: product_product_template.product.name,
    total_quantity: product_template_total_quantity(product_product_template),
    category: category(product_product_template)
  )
end
Enter fullscreen mode Exit fullscreen mode

Still ugly, but at least I can reason about it.


Get the latest content via email from me personally. Reply with your thoughts. Let's learn from each other. Subscribe to my PinkLetter!

Top comments (0)