Crystal: Object#=~ and Object#!~

Created on 16 Mar 2018  路  8Comments  路  Source: crystal-lang/crystal

The methods Object#~= and Object#!~ are defined on Object but are not actually implemented (they return nil unless overwritten). This seems really odd, considering the intended purpose of matching a pattern is only useful to very select types. In the stdlib only Regex and String overwrite this method.
I don't think it makes sense to defined these methods for every Object, just to be able to use =~ and !~ as operators between arbitrary values (at least I guess this might be the reason for this).
Comparator methods for example are only defined on types including Comparable and that seems reasonable to me. You can only compare what's comparable. Equally, you should only be able to pattern match what's matchable. That is String and Regex.

Most helpful comment

The explanation is that a ~= b means "does a match b" (in some form) regardless of their type. That's why you can use it in Ruby with any two types, and for me it makes sense.

Same as why you can do a == b regardless of their type.

All 8 comments

That explanation makes sense I guess.

The explanation is that a ~= b means "does a match b" (in some form) regardless of their type. That's why you can use it in Ruby with any two types, and for me it makes sense.

Same as why you can do a == b regardless of their type.

@asterite I understand that this would be the reason. But what's the point for having this match method for every type? Wouldn't it be better to expose it only on types that actually implement it?

Isn't weird that while Crystal promotes the explicitness in lot of areas, having Object#=~ set by default and return nil, presents an open-ended implicitness instead?

Been thinking that from the errors raised by removing that, saw lot of silently using this around nil values.

I love how Crystal help me spot places where nil might potentially leak, but wonder why not in this case? After all, I'm doing a comparison (_does a match b_) against a nil, but what if nil was unexpected at this point?

Just some thoughts.

Thank you for your time.
Cheers.

@luislavena I've come to the conclusion that it is probably about how to understand =~. It is written as an operator and implemented as a method like all operators in Crystal. Conceptually I think it is somewhere in between: syntax wise it is an operator but the behaviour is what you could also express in a method.

You could see it more similar to a method #match?, which would make it more explicit and presume type-safety. I guess that's about how you and me view it.
But if you understand it more like a general comparison operator such as == and === it should fit between any types and just equal to false if there is no specific implementation provided between these types.

Yes, it's an operator:

  • 1 == true always returns false (never equals);
  • 1 =~ true always returns nil (never matches).

There are methods such as String#match(Regex) that enforce types.

The Object#=~ and Object#!~ definitions gives a default semantic for !~. I like that a subclass does not need to define both. That been said, if a Matcheable modules is defined that can be expressed there.

I like the idea of having a criteria for when the type should matter or not (operators / methods). It is a blurry line: < does compile between different types of numbers, but fails with numbers + string.

If we end up being to strict, operators over unions will need boring type checks.

But if we consider the idea of a strict =~ and !~ operators...

  1. The =~ operator is not reflexive, that makes it a bit cumbersome to have a nice definition in Object. And having !~ defined in Object but not =~ is a smell. So this push forward the idea of module.

  2. The other question if whether nil should be allows and return as not match or should not compile. Things like https://github.com/crystal-lang/crystal/blob/1a6d5977ecde831c14ad2496d6d03db0709b25da/src/compiler/crystal/codegen/codegen.cr#L336 would need a not nil check since the compiler won't be able to infer the non nil value of dump_llvm_regex. I'm ok with that 1 < nil also fails compiling.

It is needed that both == and === return false if type does not match in order to allow a nice experience with collection of unions and case over union types. But I fail to find examples where making =~ a bit more strict as < would be problematic.

The discussion here is similar to why 1 == false is allowed in the first place. Some people would like that to be a compile error, allowing you to only compare the same types, to catch errors at compile time. But equality is a generally applicable operation between any two types, and usually comparing different types will return false.

The operator =~ is exactly the same, but instead of equal is matches or "looks like". And the idea is that you can compare any two objects like this. I think the only real use cases (at least in the standard library, and what I've seen in Ruby) is just for String, Regex and Nil, but that's not a reason to restrict the operation to just those types.

This feature saves us from writing value && value =~ ... or value && ... =~ value, which is very tedious (and it would also be tedious if we applied the same rules to ==, only worse because you'll have to do value.is_a?(T) && ... == value.

Another such operator is ===, which is also generally applicable between two types.

Conclusion: operators that have = in them, like ==, !=, === and =~, are generally applicable.

If you with more strictness, use String#match or Regex#match as @ysbaddaden suggested.

Was this page helpful?
0 / 5 - 0 ratings