WuMing2
WuMing2

Reputation: 73

How to organize Procs in Ruby?

Translating a script from OOP to FP, I am trying to understand how to replace "bags of methods" (classes) with "bags of procs". Procs are those composable (<<), anonymous micro-objects with Binding but I haven't seen an example using more than a couple of #call. My modest script has few tens of methods: replacing them with procs requires some kind of container for code organisation.

Tried to store Procs into hashes. But Hash is not designed with this application in mind. As example, calling a proc from another proc inside the same hash is awkward.

Tried to store Procs into classes, associated to variables. But encapsulation reduces my capability to access outside variables (losing one key benefits of closures) and complicates access to procs (either with instance variables have to instantiate the class or with class variables have to create class accessor methods).

Tried to store Procs into methods but then where's the gain.


require 'net/http'
require 'json'
require 'iron_cache'
require 'discordrb'

class Config
      # config vars
end

data_space = {
    :message => ""
    }

bot = Discordrb::Bot.new(token: Config::DISCORD_TOKEN, ignore_bots: true)
channel = bot.channel(Config::DISCORD_CHANNEL_ID)
cache = IronCache::Client.new.cache("pa")

bot_control = {
    :start => proc {bot.run(:async)},
    :stop => proc do
        bot.stop
        exit(0)
    end,
    :send => proc {data_space[:message].then {|m| bot.send_message(channel, m)}},
    "messaging?" => proc do |wish|
        [ Config::MESSAGING[wish.first] , Config::NIGHTTIME_FLAG ].compact.first
    end
}

cache_control = {
    :get_flag => proc {|flag| cache.get(flag)&.value},
    :set_wish => proc do |wish, exp_sec|
        cache.put("messaging", wish.to_json, :expires_in => exp_sec)
        data_space[:message] << "#{wish.first} got it!"
    end,
    :exp_sec => proc do
        t = Time.now
        (t-Time.new(t.year,t.month,t.day,Config::EXP_REF,0,0)).to_i
    end,
    :get_wish => proc do
        msg = proc {cache_control[:get_flag].call("messaging")}
        msg ? JSON.parse(msg) : [nil, nil]
    end
}
bot_control[:start].call
data_space[:message] << "ah"
bot_control[:stop].call if (bot_control["messaging?"]<<cache_control[:get_wish]).call
(bot_control[:stop]<<bot_control[:send]).call

actually delivers.

But isn't the kind of code I consider readable. Or an improvement over OOP. Please advise.


Edit

Looks like my attempt with a Hash to group procs was in the right direction. As described elsewhere a hash within a Proc adds instance methods as #count, #keys and #store to access and manipulate it. Therefore I can write something like:


bc = proc { |args|
  {
    start: proc {...},
    stop: proc {...}
  }
}

bot_control = bc.call
bot_control[:start].call
bot_control[:stop].call

With some caveats. Values returned by the hash are procs and composition methods (<< and >>) are added but they don’t work. Also I haven’t found how to reference other values from within the hash. Though it’s possible to add methods (private by default but can be declared public) and reference them from within values.

So I obtained nested closures (variables leak in all the way down) but code at this point isn’t much different from a regular class. #new has become #call (Proc inherit from Method after all) but with not much improvement in FP direction. Please advise.

Upvotes: 3

Views: 194

Answers (1)

Max
Max

Reputation: 22325

Use modules and constants.

module BotControl
  Start = proc {bot.run(:async)}
  Stop = proc do
      bot.stop
      exit(0)
  end
  Send = proc {DataSpace::Message.then {|m| bot.send_message(channel, m)}}
  Messaging = proc do |wish|
      [ Config::MESSAGING[wish.first] , Config::NIGHTTIME_FLAG ].compact.first
  end
end

Note that this code won't work as-is: the bot variable is not visible! There are a couple approaches to fixing that, but they're beyond the scope of this question (my 2¢, it should be a parameter to those functions).

Usage would look like

(BotControl::Messaging<<CacheControl::GetWish).call

Your functions should be constants, so this protects you from reassignment (which a hash wouldn't). Also modules can't be instantiated/inherited so this avoids any kind of OOPness. Another benefit is that modules can be included, so if you have a context where you want some functions to be top-level, you can easily do that.


Edit

You might be tempted to do this using anonymous modules e.g.

BotControl = Module.new do
  Start = proc {bot.run(:async)}
  ...
end

But that won't work because the scope of constants in Ruby is lexical i.e. it matters what module they're inside at the parsing stage, not at runtime. In the above example, at the parser level, Start is simply at top-level, because it is not inside any module (Module.new is just a method call, which is evaluated at runtime). So this approach doesn't actually group anything together.

Upvotes: 2

Related Questions