Reputation: 73
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.
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
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.
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