Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

Preface

Goal: A practical case to collect unique record fields using Ruby.


Preface

Goal: Continue Part Two


7: Approach in Solving Unique

A custom example, that you can rewrite.

An Algorithm Case

Why reinvent the wheel?

My intention is writing a custom pattern matching example, so later I can rewrite other algorithm based on this example. You can read the detail on the first article. The record overview.

Consider going back to simple data. We need to discuss about solving unique list.

x:xs Pattern Matching

The original x:xs pattern matching is made of two functions:

  1. Exclude
  2. Unique

Pattern matching in Ruby, is not as fancy as haskell, or even OCaml. But what we have in Ruby is enough, to solve our little problem.

Exclude Function

The exclude function is just a filter with below details:

tags = ['60s', 'jazz', '60s', 'rock',
        '70s', 'rock', '70s', 'pop']

def exclude(val, tags)
  tags.map{ |tag| tag unless tag == val }
    .compact
end

puts "#{exclude('rock', tags)}"

With the result as below array:

❯ ruby 21-exclude.rb
["60s", "jazz", "60s", "70s", "70s", "pop"]

Ruby: Exclude Function

Exclude Using Delete

Ruby has delete function, to make our life simpler.

def exclude(val, tags)
  tags.delete(val)
  tags
end

Recursive Unique Function

With exclude function above, we can build unique function recursively, as below code:

tags = ['60s', 'jazz', '60s', 'rock',
        '70s', 'rock', '70s', 'pop']

def exclude(val, tags)
  tags.map{ |tag| tag unless tag == val }
    .compact
end

def unique(tags)
  if tags.length() <= 1 then
    tags
  else
    head, *tail = tags
    return [head] + unique(exclude(head, tail))
  end
end

puts "#{unique(tags)}"
  • Although the syntax looks is, not exactly the same as functional programming in haskell, the algorithm is the same.

With the result as below array:

❯ ruby 22-unique-a.rb
["60s", "jazz", "rock", "70s", "pop"]

A Simpler Unique Using Delete

With rubys delete function, the function is even simpler.

We can also put this into its own helper module.

module HelperUnique
  def unique(tags)
    if tags.length() <= 1 then
      tags
    else
      head, *tail = tags
      tail.delete(head)
      return [head] + unique(tail)
    end
  end
end

Ruby: The Helper Module Containing Unique Function

Now we can apply to out test array.

require_relative 'MyHelperUnique'
include HelperUnique

tags = ['60s', 'jazz', '60s', 'rock',
        '70s', 'rock', '70s', 'pop']

puts "#{unique(tags)}"

Ruby: Solving Unique Song

Applying Unique to data Structure

Now we can apply the method to our unique song challenge.

require_relative 'MySongs'
require_relative 'MyHelperUnique'
include HelperUnique

tags = Songs::SONGS
  .map { |song| song['tags'] }
  .compact.flatten

puts "#{unique(tags)}"
  • With the same result as below

Ruby: Solving Unique Song

❯ ruby 22-unique-d.rb
["60s", "jazz", "rock", "70s", "pop"]

8: Concurrency with Channel Agent

Ruby support Actor Model pattern. We can rewrite previous process through go-like channel. One I found in search engine is agent.

Reference

Flatten

Before we go discussing agent, we need an alternative approach to flatten array.

Just go straight, examine the code below:

require_relative 'MySongs'
include Songs

def flatten(songs)
  tags = []
  
  songs.each { |song| 
    unless song['tags'] == nil
       song['tags'].each { |tag| tags << tag }
   end  
  }
  
  tags
end

puts "#{flatten(SONGS)}"

With the result as below array:

❯ ruby 24-flatten.rb
["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]

Ruby: Alternative Array Flattening

The Skeleton

We should be ready for the real concurreny. This is only consist of one short file.

def sender(channel, songs)
  # ...
end

def receiver(channel)
  # ...
end

# entry point ...
  1. Producer: sender()
  2. Consumer: receiver()
  3. Program entry point

Producer and Consumer

We should prepare two functions:

  1. One for Producer that send to channel,
  2. And the other one for Consumer that receive from channel.

Producer that send to channel.

def sender(channel, songs)
  songs.each do |song| 
    if song['tags'] != nil
      song['tags'].each do |tag|
        channel << tag
      end
    end
  end
  channel << nil
end

Ruby: Concurreny with Agent: Producer

Consumer that receive from channel.

def receiver(channel)
  tags = []

  while true
    message = channel.receive.first
    if message != nil
      tags << message.to_s
    else
      puts "#{tags}"
      break
    end  
  end
end

Ruby: Concurreny with Agent: Consumer

Running Both Routine

So much go like.

Consider gather both function in main entry point.

require_relative 'MySongs'
include Songs

require 'agent'
chan = channel!(Object)

go! do
  sender(chan, SONGS)
end

receiver(chan)

With the result as below array:

❯ ruby 25-channel.rb
["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]

Ruby: Concurreny with Agent: Running Both Thread


9: Concurrency with Fiber

There is, however, another way in Ruby. Fiber using yield just like in generator, but lighter than Channel.

The Skeleton

The skeleton is similar to above.

def sender(channel, songs)
  # ...
end

def receiver(channel)
  # ...
end

# entry point ...
  1. Producer: sender()
  2. Consumer: receiver()
  3. Program entry point

Producer and Consumer

We should prepare two functions:

  1. One for Producer that yield to fiber,
  2. And the other one for Consumer that resume from fiber.

Producer that yield to fiber.

def sender(songs)
  Fiber.new do
    SONGS.each do |song|
      if song['tags'] != nil
        song['tags'].each do |tag|
          Fiber.yield tag
        end
      end
    end
    Fiber.yield nil
  end
end

Ruby: Concurreny with Fiber: Producer

Consumer that resume from fiber.

def receiver(fiber)
  tags = []
  while true
    message = fiber.resume
    if message != nil
      tags << message
    else
      puts "#{tags}"
      break
    end
  end
end

Ruby: Concurreny with Fiber: Consumer

Running Both Routine

So much go like.

Consider gather both function in main entry point.

require_relative 'MyStructSongs'
include StructSongs

fiber = sender(SONGS)
receiver(fiber)

With the result as below array:

❯ ruby 27-fiber.rb
["60s", "jazz", "60s", "rock", "70s", "rock", "60s", "jazz"]

Ruby: Concurreny with Fiber: Running Both Thread

This is the end of our ruby journey in this article. We shall meet again in other article.


What is Next 🤔?

Consider continue reading [ PHP - Playing with Records - Part One ].