Some gems for some useful functions I've made

I’ve made some gems for functions I’ve used in private projects that I found somehow useful.

They are small, focusing on a few functionalities.

Funny enough, I found myself using only a couple of those functions.

The main one (jgomo3-func) is then-if (or then-unless) which allows me to write patterns like this:

data.then_if(&:predicate?){ process(_1) }

Another one (ruby_case_struct) allows me to write case when expressions as Hash-like structures. Example:

cases = {
  (1..5) => :low,
  (6..10) => :moderate,
  (11..) => :high,
}.extend(CaseStruct)

cases[3] # => :low
cases[8] # => :moderate
cases[100] # => :high_brightness: 

Which I think is useful for writing simple rule engines.

The last one (procer) defines a default to_proc for all objects. So we can use all objects as blocks with some intuitive results:

[1, 2, '3', '4', 5, 6].filter(&Numeric)
# => [1, 2, 5, 6]

[-10, 100, -2, 3, 20, -33].filter(&(0..50))
# => [3, 20]

What do you think about such functionalities? Any feedback is welcome.

In some projects that I’ve worked on, I sometimes discover code like this that people think is clever:

I just rewrite like this:

process(data) if data.predicate?

Boom! That’s a lot simpler and more readable! I don’t understand why some people like to write weird code like that thinking it is useful. It’s not useful at all. Also, the new numbered block variables are only meant to be used for Ruby scripts that are replicating bash scripts with $1 variables. They are not readable at all in standard business applications since you’d have to go back while reading to remember what the _1 is. It would be way better if it is a variable name like value, customer, order, etc…

Just write idiomatic Ruby code! That’s what is demanded of collaborative Ruby software engineers who want to enable code to be readable and maintainable by others in a team, not just by themselves as individuals.

Here is another example:

cases = {
  (1..5) => :low,
  (6..10) => :moderate,
  (11..) => :high,
}.extend(CaseStruct)

cases[3] # => :low
cases[8] # => :moderate
cases[100] # => :high_brightness: 

Just write this:

def status(value)
  case value
  when 1..5
    :low
  when 6..10
    :moderate
  when 11..
    :high
  end
end

status(3) # => :low 
status(8) # => :moderate
status(100) # => :high_brightness

Boom! That’s a lot more Ruby-idiomatic and readable by newcomers to the codebase. It’s just standard Ruby.

There is no need for over-engineering or meta-programming if it only saves a couple lines of code. Usually meta-programming is only useful when it saves tens, hundreds or thousands of lines of code, or provides a language that is closer to reality (not farther as in the examples above). Otherwise, it’s considered over-engineering that makes code look foreign to newcomers to the codebase.

If you really want to use a hash though, you could do something like this:

RANGE_STATUSES = {
  (1..5) => :low,
  (6..10) => :moderate,
  (11..) => :high,
}

def status(value)
  RANGE_STATUSES.find {|range, status| range.include?(value)}[1]
end

status(3) # => :low 
status(8) # => :moderate
status(100) # => :high_brightness

This code is still basic Ruby. No special meta-programming was involved. If it is preferred to use a hash, consider this approach first before looking into meta-programming.

Now, if you want to generalize the solution to reuse many times (not just once), you can still avoid meta-programing by having users rely on a normal class instead of the weird extend:

# an incomplete naive implementation that does not handle edge cases
class RangeHash
  def initialize(hash)
    @hash = hash
  end

  def [](value)
    @hash.find {|range, status| range.include?(value)}[1]
  end
end

# This is used as a normal class now instead of extending hash
status = RangeHash.new(
  (1..5) => :low,
  (6..10) => :moderate,
  (11..) => :high,
)

status[3] # => :low 
status[8] # => :moderate
status[100] # => :high_brightness

Users can now read the code and understand it more simply given that RangeHash is a standard class that enables picking values based on Range keys, so there is no mysterious meta-programming. It’s just standard Ruby.

Everytime I catch over-use of meta-programming in my projects by other developers, I delete it and replace it with simple Ruby idiomatic code while lecturing those developers on how to write Ruby-idiomatic code that enables collaboration with others in a team.

Don’t listen to public blog posts about writing “functional”-style Ruby code without applying your own software engineering reasoning to the problem. Many blog posts on the Internet are written by uneducated people who do programming through monkey-see-monkey-do because they never got a full 4-year Computer Science (or computer related) university degree (they are either self-taught or only went to a short bootcamp), so their knowledge is very weak (they never mastered Object-Oriented Programming), and they tend to repeat things they heard from others that they deem “important” without them truly understanding the words they repeat. You can easily reveal their lack of depth of knowledge by simply asking them a question outside the words they repeat, and those programmers will always break down and fail at answering your question.

Functional programming is only useful for implementing highly mathematical logic, but for most business applications, Object-Oriented Programming results in the most productive and maintainable code that is closest to reality. Ruby is a hybrid OO/FP language. You should take advantage of that and use the right tool for the job instead of trying to force functional style everywhere like what bad unpragmatic programmers do (who are 60% of programmers nowadays sadly).

In conclusion, write Ruby-idiomatic code, don’t re-invent the wheel, think of other developers that might join your project team, use the right software paradigm for the job (like OOP), and avoid meta-programming (e.g. CaseStruct) unless it is absolutely needed, which is very rare.

3 Likes

Thank you very much for your honest feedback @AndyMaleh . That’s precisely what I was looking for, some honest opinion about these ideas.

I foresaw rejection as these ideas contrast with those principles you list (shared by most of the Ruby community) at first sight. I’m conscious of those, that’s why I was looking for feedback.

There is obviously justification for each of the functions, but as with everything in our field (and everywhere)… “it depends” :smiley:.

I’ll present those functions’ justifications as I know these are easy points to miss, but I would like to know if those are still overengineering from your point of view.

then-if

First a small correction, the equivalent form should be a ternary expression, not the simple guarded by an if expression, because thien-if yield self if condition is false. But your point is still valid, you would prefer:

data.predicate? ? process(data) : data

(or something similar),
Over:

data.then_if(&:predicate?){ process(_1) }

Now the justification.

The idea of then-if is to be something similar to the standard Ruby function then. If then has it’s place, why not then-if? That’s a first one. But the main point is (as it is for then) to be used in chaining expressions.

So if you have some code in pipeline style:

data.then{}.then{}...then{}

You can add a new step with:

data.then{}.then_if(Condition){}...then{}

It could be useful in the same places where then is or could be useful.

There is a technique that I’ve been thinking that could make this function unnecessary, and it is that then without block returns an Enumerator that could be used for that conditional part, but I can’t find an easy way to express the idea of “Apply the block only if the condition is true and let the object flow otherwise”. For completion, check this example from “then” documentation which illustrates the idea:

# meets condition, no-op
1.then.detect(&:odd?)            # => 1
# does not meet condition, drop value
2.then.detect(&:odd?)            # => nil

CaseStruct

The point that you could have missed here is the dynamic nature of this construct. That dynamism is the justification over plain case .. when ... end, obviously, when needed.

One example use case is a simple rule engine, to which you can add or remove rules dynamically, something you can’t achieve with static text (case...when expressions).

CaseStrcut could be an alternative to create such Rule Engines with its own API for adding, inspecting, and removing rules. Obviously, this is valid only if the Hash API is good enough for your needs.

If it is not, then one must write a complaint system with whatever API and capabilities are needed. The traditional way would be a class RuleEngine, etc.

Conclusion

Even so, I know how I justified them, but I still want to find alternative ways to achieve the same with plain Ruby to compare and see the actual value of the gems.

then_if is for attaching stages in a pipeline expression. Is it cleaner than then.<enumerator_method>(...), or than then{ condition(_1) ? process(_1) : _1 } ?

CaseStruct, dynamic case ... when expression (for when you need the dynamism): is there something in the Ruby ecosystem doing the same thing?

1 Like

Update

@AndyMaleh, I see you got the dynamism idea before noticing my comments. Good, you get the point.

I also get that you find the style of extending objects directly with modules not pleasant. Indeed it is not something of common use in the Ruby ecosystem.

So now I think it is a good idea to provide the class (like the RangeHash of your example) to be used to construct the CaseStrcut objects.

As it is now, the module could be included by any class, all it needs to provide is an implementation of find. So my plan is:

  1. Rename the module to AsCaseStruct
    1. Update the dependent code (this is backward incompatible change, but I like this whole idea).
  2. Create a class CaseStruct.

I think the implementation would be then:

require 'forwardable'

class CaseStruct
  include Forewardable
  include AsCaseStruct

  def_delegator :@hash, :find

  def initialize(hash)
    @hash = hash
  end
end

Then users could use the class, include the module, or extend the module directly by objects. Similar to what Forewardable and SingleForewardable do.

Anyways, thank you very much for your feedback and time.

Update

I missed the point now… what about the Hash API? to get it, I have to make the class extend Hash. I don’t know about that? What are the caveats of having a class that inherits from Hash? Some languages sometimes have this problem, that it is not possible to simply extend base data structures without problems.

1 Like

You are right. There are different options to implement the range hash (or case struct), like inheritance through extending Hash or delegation through decorating Hash (Decorator Pattern forwarding all methods to Hash). Like you said, “it depends” on the requirements. If all what is needed is the [] method, then there is no need to extend a Hash or even decorate it. But, if there is a need to use the object as a regular Hash as well, then you could extend it or decorate it. The benefit of decoration is enabling runtime change of an existing Hash’s behavior. But, if the behavior is wanted always, then inheritance in a CaseStruct might be good enough.

1 Like

Concerning this code:

I am glad you explained it because that reveals an issue with the weird use of then_if as I did not understand its purpose correctly upon first seeing it. That is usually a sign of a weak software design and a code smell.

In Object-Oriented Programming, you could easily eliminate the condition you have by using polymorphism, which puts the responsibility of processing data on the data itself, and depending on its type, different things would happen, so the could just becomes this:

data.process

The data will know itself whether to process itself based on its own hidden predicate or not. To the outside observer, we have fully delegated the responsibility of processing to the data, and do NOT have to worry about the logic anymore. This is proper Object-Oriented Design. As mentioned before, needing something as convoluted as then_if is a code smell that was pointing out the bad design of the code. Of course, weaker programmers might try to address the weakness of design by inventing brand new unnecessary concepts when they could have better designed their code following proper OOP.

In any case, there are other options if we do not want to shift the process responsibility to the data itself (although that is usually recommended in Object-Oriented Programming). Alternatively, we could shift the responsibility of the predicate to the process method like this:

def process(data)
  return data unless data.predicate?

  # do work and return processed data
end

Now, the main code becomes this:

process(data)

Again, in this case, we hid the details of the predicate completely, thus resulting in much simpler code than the one doing then_if, thus removing the need for that foreign construct completely by relying on standard Ruby code techniques.

1 Like

Regarding your inquiry about what is possible to achieve this:

The proper Object-Oriented Design for handling this is to do this (no need for then at all):

object.process

The object will handle all the details. You should not have to worry about them. If you are worrying about the details of processing from the outside, then it is a code smell and a sign of a weak software design, as mentioned earlier.

But, if you have an enumerable, like an array, then the code becomes simply this:

objects.map(&:process)

Again, the details of the predicate are hidden behind the process method on the object itself. And, you can chain things if needed (but if you want all objects, then the approach above is sufficient):

objects.select(&:odd?).map(&:process)

then is generally not needed. It is basically an alias for yield_self, which is trying to force a functional paradigm on a single element inside a block, which is not truly needed given you could invoke all the functions on the object directly if it is one. The only case I could see it needed is when there is a need to process a single object the same way you would process an enumerable array of objects, and you want to reuse the same code block for the array, but on a single object. Otherwise, there is no need for then in general. I would try to avoid it most of the time except for the few rare cases it is needed. Unforutnately, it is over-used and abused by some programmers in the Ruby community who try too hard to force a functional paradigm when it doesn’t apply. Do not copy their example. Always analyze whether a certain approach results in simpler code to read and maintain by others or not.