Ruby Lamdas For Clear Code

date: "2024-11-18"
Categories: ["Development", "ruby", "rubylang"]
Tags: ["Development", "Ruby", "Functional"]

Basics

at = -> k, a { a[k] }.curry
get = -> meth, a {a.send(meth)}.curry
first = get.(:first)
last = get.(:last)
to_sym = get.(:to_sym)
to_s = get.(:to_s)
map_k = -> f, h { h.transform_keys(f) }.curry
map_v = -> f, h { h.transform_values(f) }.curry
map_h = -> f, h { h.map(&f).to_h }

Auto-Vivify

grow = -> leaf_factory { -> { Hash.new { |h, k| h[k] = leaf_factory.() } } }

a = grow.(grow.(-> { 0 })).call

a[:user][:age] += 1
a[:user][:score] += 5

puts a.inspect
# => {:user=>{:age=>1, :score=>5}}

Calendar

require "date"

# generate month calendar struct from a date_in_month
month_calendar = ->(date_in_month) {
  year = date_in_month.year
  month_number = date_in_month.month
  first_day_of_month = Date.new(year, month_number, 1)
  last_day = Date.new(year, month_number, -1)

  start_date = first_day_of_month - first_day_of_month.wday
  end_date = last_day + (6 - last_day.wday)

  weeks = (start_date..end_date).each_slice(7).map(&:to_a)
  [first_day_of_month, weeks]
}

# year struct
year_calendar = ->(date) {
  year = date.year
  (1..12).each_with_object({}) do |month_number, hash|
    first_day, weeks = month_calendar(Date.new(year, month_number, 1))
    hash[first_day] = weeks
  end
}

# render to text
render_month_text = ->month, fn = ->date { date.day.to_s.rjust(2) } {
  first_day_of_month, weeks = month
  month_name = Date::MONTHNAMES[first_day_of_month.month]
  year = first_day_of_month.year
  month_number = first_day_of_month.month

  title = "#{month_name} #{year}"
  lines = []
  lines << title.center(20)
  lines << "Su Mo Tu We Th Fr Sa"

  weeks.each do |week|
    line = week.map do |date|
      if date.month == month_number
        fn.(date)
      else
        "  "
      end
    end.join(" ")
    lines << line
  end

  lines.join("
")
}
red = ->a { "#{a}" }
render_date = ->date { date.day.to_s.rjust(2) }

# Example usage
month = month_calendar.(Date.today); 1
puts render_month_text.(month)

#      March 2025
# Su Mo Tu We Th Fr Sa
#                    1
#  2  3  4  5  6  7  8
#  9 10 11 12 13 14 15
# 16 17 18 19 20 21 22
# 23 24 25 26 27 28 29
# 30 31

make select queries

cast_val = ->type_cast, value {
  if type_cast.respond_to?(:serialize)
    # rails 5
    type_cast.serialize(value)
  else
    # rails 4
    type_cast.type_cast_from_database(value)
  end
}.curry

cast_row = ->result, rec {
  types = result.column_types
  rec.map { |k, v|
    type = types.fetch(k)
    [k, cast_val.(type).(v)]
  }.to_h
}.curry

conv_result = ->res {
  res.to_a.map(&cast_row.(res))
}
query_db = ->db, sql {
  res = db.connection.exec_query(sql)
  conv_result.(res)
}.curry
quote = ->db, a { db.connection.quote(a) }.curry

db = IsptDb::Base
q = quote.(db)
query = query_db.(db)
query.(%{select "AccountName" from "Customers" WHERE created_at > #{q.(Time.utc(2023, 3, 3))} limit 10})

### Usage

q = quote.(ISPTBase)
query = query_db.(ISPTBase)
query.(%{select "AccountName" from "Customers" WHERE created_at > #{q.(Time.utc(2023, 3, 3))} limit 10})

Diff structs

require "set"
diff_o = diff_h = diff_a = nil  #

# Diff two ruby structure
diff_o = ->a, b {
  return true if a == b

  if a.is_a?(Hash) && b.is_a?(Hash)
    diff_h.(a, b)
  elsif a.is_a?(Array) && b.is_a?(Array)
    diff_a.(a, b)
  else
    [a, b]
  end
}

diff_h = ->h1, h2 {
  return true if h1 == h2

  delta = (h1.keys + h2.keys).uniq.each_with_object({}) do |k, diff|
    diff[k] = diff_o.(h1[k], h2[k])
  end.reject { |k, d| d == true }.to_h
  delta.empty? ? true : delta
}

diff_a = ->a1, a2 {
  return true if a1 == a2

  delta = ([a1.size, a2.size].max).times.reduce({}) do |diff, i|
    diff[i] = diff_o.(a1[i], a2[i])
    diff
  end.reject { |k, d| d == true }.to_h
  delta.empty? ? true : delta
}

usage

hash_a = {foo: {bar: 1}, baz: [1,2,3]}
hash_b = {foo: {bar: 2}, baz: [1,2,4]}
p diff_o.(hash_a, hash_b)
# => {:foo=>{:bar=>[1, 2]}, :baz=>{2=>[3, 4]}}

array_a = [1, {foo: "bar"}, [1,2]]
array_b = [1, {foo: "baz"}, [1,3]]
p diff_o.(array_a, array_b)
# => {1=>{:foo=>["bar", "baz"]}, 2=>{1=>[2, 3]}}

Progress

# @param [a->a] printer: is a function that takes a string and prints it
# @param [Integer] slice: is the number of elements to process before printing
# @return [Enumerable]: is the collection to process

view_progress = ->printer, slice, enum {
  start_time = Time.now
  index = 0
  obj = Object.new
  total = enum.count
  obj.define_singleton_method(:each) do |&block|
    enum.each do |a|
      index += 1
      if (index % slice) == 0
        elapsed = Time.now - start_time
        avg = index / elapsed.to_f
        total_time = (total.to_f / avg).to_i

        printer.("#{index} / #{total}. Elapsed: #{elapsed.to_i}sec. Rate: #{"%0.2f" % avg} entry/sec. Estimated completion time #{start_time + total_time}")
      end
      block.call(a)
      a
    end
  end
  obj.extend(Enumerable)
  obj
}.curry

overwriter = ->a { print("\r" + a) ; a }
my_progress = view_progress.(overwriter)

log = ->a { logger.info(a); a}
my_progress = view_progress.(log)

# Usage
10.times.then(&my_progress.(10)).each{|a| sleep 0.05}

Naming block code

Ruby lambdas can be used for documenting code by adding a name to an algorithm.

users.map { |u| u.first_name = " " + u.last_name }

You can use a lambda to make it a bit more clear.

full_name = -> a { a.first_name + a.last_name }
users.map(&full_name)

In this case & calls the to_proc method on lambda.

Here’s another example:

users.select { |a| a.age > 18 }

It can be refactored to

adult = -> a { a.age > 18 }
users.select(&adult)

You can then reuse this adult lambda in combination with other lambdas.

male = -> a { a.gender == :male }
users.select { |a| adult.(a) && male.(a) }

Aliasing method names

Lambda can also be used for aliasing a method with a name more close to the current context.

If we look at this code:

key = Digest::MD5.hexdigest(Digest::MD5.hexdigest(login) + Digest::MD5.hexdigest(user.keyword))

It can be hard to read, but lets extract the md5 calculation in a lambda:

md5 = -> str { Digest::MD5.hexdigest(str) }
key = md5.(md5.(login) + md5.(user.keyword))

This makes the code clearer and more concise. The signal to noise ratio is much higher this time.

Another way of doing this would be to create a method on the current class. However this is more code and the md5 method definition would be far from its call location even if the method would be use only on the caller method.

Using lambdas for creating very simple DSLs

When we say simple we don’t mean less powerful.