Deciding like a robot. NOT! (Boolean Expression in Ruby)

Javier Segovia
lemontech-engineering
4 min readSep 29, 2016

Sometimes we should create annoying “rules” to make decisions when we are coding things like this

if foo & bar & a.eql?('foo') || foo_bar == bar || ... # and so on

So to keep it “legible” we should do some improvements, like creating methods to re-use some statements or simply nesting statements to avoid long lines of code. But, if we think as a “machine”, we have to interpret “true” and “false” values (binary values 1,0)

Let’s see how a circuit works

http://www.upcrost.site/boolean-expression-to-logic-circuit/logic-gates-converting-nand-to-nor-boolean-expression-boolean-circuit-software-y0tdr/

We got 3 switches S, X and Y, and there’s a circuit to turn on a bulb, as a programmer we can see the logics gates as “If Statements” because, that’s what they are, a physical implementation of boolean functions, so our function is (S’X + SY)

If S & !X || S & Y
# Do stuff
end

Not a big deal right?, but what if we have a lot of inputs and a lot of logics gates?

Or even bigger?

So, let’s try to code that

What about if we have the same input “switches” but different circuits for different outputs?, then we’ll have to code other circuit methods or if statements

def foo_circuit(s, x, y)
# logic stuff
end
def bar_circuit(s, x, y)

end
# ... Imagine if it's more complex...

This can work for a lot of purposes, personally, I was thinking about how to create a To Do list as a decision tree, where every node can contain a decision tree as well, it would be a pain in the ass if we wanted to develop/maintain it

But, why don’t we use meta-programming to do this task? To avoid similar methods or block codes within the same types of input / output. That’s why I coded a little gem thinking about it, let’s take a look

Easy right? OK maybe you’re thinking, it’s easier to do

return (A & B)

Let’s create a TO DO class, that have an array of tasks and a name to identify them:

class ToDo
# A to do list should have an array of tasks
attr_reader :tasks, :name
# let’s define our tasks
def initialize(name, tasks)
@name = name
@tasks = task
end
# o we can simply add more task to our array
def add_task(task)
@tasks << task
end
# Retrieve our task list as a hash
def task_list
@tasks.map { |task| [task.name, task.completed] }.to_h
end
end

Now create a Task class that has two attributes, name and completed, which will behave like a hash with a Key (name) and a Value (completed)

class Task
attr_accessor :name, :completed
def initialize(name, completed)
@name = name
@completed = completed
end
end

To evaluate rules and decisions, let’s create a class “Rule” that has a boolean expression and a to do model that will return if the evaluation is positive

class Rule
attr_reader :expression, :to_do
def initialize(expression, to_do)
@expression = expression
@to_do = to_do
end
end

And finally, lets create a class called “Flow”, that will have the business logic to decide in which to do list we’re working

class Flow
attr_accessor :current_todo
def initialize(rules)
@rules = rules
# initialize a decider class
@decider = Undecided::Decider.new
end
def start(to_do)
@current_todo = to_do.nil? ? next_to_do : to_do
end
# Iterate every rule to match the true one
def next_to_do
@rules.each do |rule|
next if !next?(rule)
@current_todo = rule.to_do
break
end
# return the same if none of the rules are true
end
# check if the rule is met
def next?(rule)
@decider.decide(rule.expression, @current_todo.task_list, false)
end
end

To test it, let’s instance some classes

# let's define our clases# tasks
task_a = Task.new(:a, false)
task_b = Task.new(:b, true)
task_c = Task.new(:c, false)
task_d = Task.new(:d, true)
task_e = Task.new(:e, false)
# todos
todo_a = ToDo.new('todo_a', [task_a, task_d])
todo_b = ToDo.new('todo_b', [task_a, task_b, task_e])
todo_c = ToDo.new('todo_c', [task_c, task_d])
todo_d = ToDo.new('todo_d', [task_e, task_b])
# rules
rules = [
Rule.new('!a&!b', todo_b),
Rule.new('a&b&c!d|(d|c)', todo_c),
Rule.new('!a&!c', todo_d),
Rule.new('b&e', todo_b)
]
# flow
flow = Flow.new(rules)

Let’s start with “todo_a”

flow.start todo_a
# #<ToDo:0x00000001c61118 @tasks=[#<Task:0x00000001a2d9c8 @name=:a, @completed=false>, #<Task:0x000000023d63f8 @name=:d, @completed=true>], @name="todo_a">

Now let’s move to another to do list check that the first rule should be positive cuz “todo_a” doesn’t have a ‘d’ task, so it will be false, and the ‘a’ task is false

flow.next_to_do
##<ToDo:0x0000000215b7e0 @tasks=[#<Task:0x00000002265550 @name=:a, @completed=false>, #<Task:0x00000002247b68 @name=:b, @completed=true>, #<Task:0x000000021c4998 @name=:e, @completed=false>], @name="todo_b">

If we check the new todo list

flow.current_todo.name
# ‘todo_b’

So it works! the rule we evaluated was Rule.new(‘!a&!b’, todo_b), then if !a&!b with values a = false, d = false, and according to

# decide function has an default argument 'stric' = true, to match the total values in the expression
# if we want to avoid this, we simply pass it as false
expression = 'A&B&C'
values = { A:1, B:1 }
decider.decide(expression, values)
# Return false cuz A&B&C means that (A==true & B==true & C==true), but C is nil, then is false

The evaluation is positive! and it will return the new to do list “todo_b”

You can keep playing with the remaining values, but I wrote it randomly, so maybe not all of them matches the remaining todo list.

Here’s the whole code:

To evaluate these functions, I’m using eval to perform the functions as a code block, but before you freak out, there’s a lot of validation to avoid malicious code injection.

Feel free to use, fork, rewrite, fix, or whatever you want the gem, I hope it can be useful for someone.

--

--

Javier Segovia
lemontech-engineering

Software Engineer, Game Dev, AI enthusiast (In Skynet We Trust), hobbyist photographer and sarcasm native language speaker. CTO at sosafeapp.com