Object#presence

I noticed there are these two functions in the std lib

Nil#presence
String#presence

wondering if we could not expand this to all object types. Something like this

class Object
  # returns its receiver if it's not nil? or empty?
  def presence
    _self = self
    if _self.responds_to?(:empty?)
      _self if !_self.empty?
    else
      _self
    end
  end
end

# this is the existing implementation on String and Nil
class String
  def presence
    self if !blank?
  end
end

class Nil
  def presence
    self
  end
end

This way you can do things like:

my_var = user_defined_array.presence || ["default", "array"]

This is already done all through the standard library with strings, just feels like a pattern that could be useful on anything that responds to empty?

Wouldn’t be a breaking change, but would allow all container types to use the pattern

1 Like

I think we could introduce blank? for all objects, then present? would be the opposite and presence would rely on blank?.

In the beginning I was reluctant to add blank? but eventually we did it for String. Maybe it wouldn’t hurt to add it to other types, I don’t know. presence if useful in the example you provide, I think it’s the reason why it’s there in String and maybe Nil.

1 Like

I would be completely down for both blank? and present? across all objects
I’ll make a pull request

1 Like

Thank you! Just note that there’s no guarantee it will go in, the above was my personal opinion and others should also comment on this.

1 Like

I like it.

1 Like

We discussed this when introducing String#presence/Nil#presence, see https://github.com/crystal-lang/crystal/pull/1299 and the discussions referencing it, especially the ones within the PR that added String#presence.

I’ll do a pull for Object#presence as I suggested originally - I think it makes sense and presence got a much better reception than blank?

What is the use case for this?

Asking because I see this exact concept misused everywhere in Rails apps, where people treat empty arrays, hashes, and strings as equivalent to nil throughout the entire app instead of the one layer that interfaces with HTML form input (where there is no distinction between empty and omitted).

Versus doing things like (which is littered all through my code)

my_var = user_defined_array
my_var = ["default", "array"] if my_var.nil? || my_var.try(&.empty?)
my_var = my_var.not_nil!

The same use case as String: Use String/Nil#presence in a few places throughout stdlib (#8508) · crystal-lang/crystal@0bb3fe9 · GitHub

But where are you using this? Where are the values coming from? Is it a web form submission? JSON? A config file?

NoSQL databases storing JSON configurations mainly, parsing binary network protocols, also web front-ends (these are pretty common input interfaces these days).
i.e. https://opcfoundation.org/about/opc-technologies/opc-ua/ as an example network protocol

Right now it’s writing an application that interacts with Google Calendar API - so lots of JSON
It feels like something that comes up once a day and after looking at the link @jhass shared, the number of pull requests made, it’s probably not just me.

The problem is not one of usecases but one of semantic. What kind of values are considered “present” is quite domain specific and doesn’t have to be the same across domains. In that regard String#presence is already a big stretch for stdlib. I feel it okay pretty much only because it’s not defined for other types but Nil. We can claim a definition within the domain of a “string” rather than the domain of what the string represents in the user code. But this distinction falls apart when we start to add things like Array#presence. What’s the common domain of a string and an array, what’s the common reasoning between their presence definitions?

My link served to link the existing discussions, please don’t just ignore them and start from scratch on the topic.

3 Likes

Seems simple to me.
Something has presence if

  1. it is not nil
  2. it is not empty or blank (blank being a superset of empty)

If you think of an array in C, it’s just a pointer to memory. If there is nothing in the array, size zero, there is nothing present in memory for that array. In C land you literally want this to be null pointer.

A hash has similar semantics to an array when empty - there is nothing present in the hash.

An integer is present no matter its value, it exists and is present as an integer.

So from this I would argue if a thing is not empty then it has something to present, hence presence.
I would also say that things don’t have to be semantically perfect to be useful. A save icon is a floppy disk, no semantic value to a large group of people, but still a useful button

The floppy icon actually made perfect sense when it was introduced, people just learned to associate a particular concept to it as the exisiting association started to become weaker. Introducing a concept with weak associations from the start is a whole different story.

You just delegated the ambiguity to “blank” in your definition. When is an object blank? Why is an all whitespace string blank but an array consisting of just nils not? Or if we follow your C analogy, why is an array of 0x20s not blank?

The answers to these questions are specific to the application domain and not the data structure domain. This is why in the Ruby world these methods continue to live in ActiveSupport, a collection of helpers for a web framework.

https://bugs.ruby-lang.org/issues/5372

Similar to us, they’re open (but still undecided because performance is not a primary concern for Ruby, unlike Crystal) to a String#blank?: https://bugs.ruby-lang.org/issues/8206 / https://bugs.ruby-lang.org/issues/12306

2 Likes

Strings are typically designed for human consumption. So a string with nothing visible to a human eye is blank to us if it has 1000 characters or none.

However Arrays are for computer consumption. So an array of nils, possibly represented by an array of 0’s in memory has meaning to a computer.

So semantically it makes perfect sense that a string of nothing has no presence but an array of 0’s is present. Ruby has the unfortunate situation of using strings for binary data storage whereas crystal has slices - so we can take this semantic differentiation in our stride.

Finally I point to the literal motto of crystal lang

I don’t see that being brought up as an objection in the Ruby issue tracker entries I linked about this topic, so maybe that’s not their reason for being hesitant about it? ;)

I agree with @jhass. Specially because coding a shard with blank?, present? and present is very easy, maybe just 50 lines of code. Then you can use it in your project, and if that semantic doesn’t fit your domain then you can define them in a different way.

2 Likes

I agree with the idea of making this a shard or even just adding it to your own app. I’ve found that counting an empty array/hash the same as nil is not a good general practice. IME it has always led to sloppy code, where treating the two as equivalent propagates outside the one layer of the app where that equivalence makes sense.

2 Likes

Also agree with @jhass and @jgaskins .
In particular, better avoiding the use of not_nil!, and even .as if possible.
@stakach In my opinion it is cleaner to do

unless my_var && !my_var.empty?
  my_var = ["default", "array"]
end

# Instead of
my_var = ["default", "array"] if my_var.nil? || my_var.try(&.empty?)
my_var = my_var.not_nil!

Already quite neat without #presence!

I would argue it is less readable. More lines of code, easier to make a mistake and for everyone who hasn’t seen your example has all the opportunities to introduce a less than ideal implementation.

unless my_var && !my_var.empty?
  my_var = ["default", "array"]
end

#vs

my_var = my_var.presence || ["default", "array"]

This may be a futile discussion but I still haven’t seen a very compelling argument not to introduce it.
The linked Ruby issues are mostly discussing Object#blank? whereas Object#presence as I defined it above seems simple enough to get one’s head around

Further, if the more complicated example above is super clear why don’t we use this pattern for strings?
It is more confusing to have a special case for String and nothing else - especially when a function of String is leaking into the implementation of Nil - I makes way more sense to have String and Nil special cases of Object