Weird Ruby: Nil Conversions
metaredux.com
metaredux.com
A place where we would use nil conversions is especially in logging and error handling — you can cast anything, including nil, to a string, and that is useful in debugging.
Probably more often than nil conversions, we use the safe navigation operator &. as a way of working with "might be nil" values without having to add too much verboseness.
I do miss the way languages like Swift think about Optional values, where you have to be much more explicit about whether or not you are dealing with `nil`. It's easier to reason about and leads to clearer codepaths. And in general, I miss stronger type systems in Ruby.
But in a professional environment with relatively high developer expertise in the language and relatively clear style guides, we're still very productive in Ruby and don't suffer hugely from the typing issues. I guess eventually you learn to work around the warts in whatever language you use.
But this kind of API where you have 'safe' operators that don't throw is a pretty good convenience, since forgetting that a variable might be nil is pretty common (or passing nil into a method that previously never accepted a nil and where the API was never tested for nil).
There's a better way to do it like what Rust does with Some()/None enums and forcing you to unwrap the value and handle the None value explicitly, which is much better than using 'unsafe' operators that just blow up and throw/panic if you don't think about the edge condition (So I wouldn't consider the Python alternative to be any better, you're just trading one possibly bad behavior of an edge condition for another possibly bad behavior of an edge condition and forcing programmers and code reviewers to constantly have them in their mind, and blaming them for when it goes wrong--with one group or the other feeling superior about their choice of when to blame the programmer for forgetting to handle the behavior). I think this is also roughly equivalent to languages like C# 8.0+ that have nillable/non-nillable types and static checking to make sure you're using them right.
I'm not surprised you found Ruby uncomfortable when you describe Ruby as not having a type system. It might just not be the right language for you. That's fine.
> Not knowing if the variable you were dealing with was a String or a URI was great until it very suddenly became not so great.
Nothing stops you from declaring that you expect a given type in Ruby; it's just code, and not mandatory, though you can certainly write code to impose mandatory types with Ruby - there was a while when writing those seemed a rite of passage for Ruby developers, but the reality was that they buy far less than people think until they've tried writing one and/or spent some time using them.
> There's a better way to do it like what Rust does with Some()/None enums and forcing you to unwrap the value and handle the None value explicitly
Nothing stops you from doing that in Ruby, and in fact there are several monad implementations for Ruby that provides building blocks for those kinds of patterns if/when you want to use them.
Thought we were supposed to "respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize" around here and not nitpick misstatements.
The sheer amount of code I wrote, just to protect against nil-errors is staggering. Every third function or method had guard clauses and/or tests. Just to ensure that the data was in the expected shape (that it quacked and walked like a duck). I spent, I think, hundreds maybe thousand hours debugging actual production "nil-errors" and weird conversion errors. That happened in production. Sometimes legacy database records, sometimes weird race conditions causing corrupt data, sometimes (most often) just stupid mistakes by me or another dev.
All of these -all!- would have been caught by a typing system and type checker like e.g. Rusts'. All.
Which then leaves just the huge category of stupid bugs that I'll make in the business logic.
But now, working with rust, the LSP tells within ms that I overlooked something that In Ruby would've been a data-error. Often my Ruby test suite would find these after minutes of running. Sometimes we would find them after deploying to prod and seeing havoc. Sometimes they'd take weeks before popping up.
I'm convinced rust makes me develop much faster because of this.
If that's not enough for you, Ruby has optional type checkers available.
It implies a lack of conventions around interfaces and variable names. The surrounding statement also implies that it was not easy to figure out quickly (hard to exercise that code synthetically).
Highly successful workflows in loosely typed environments like this depend upon a strict culture to ensure both of these aspects are under constant improvement.
This is difficult to scale, and improving these aspects in other languages also helps other workflows, but their impact in these environments is much higher. It’s part of the reason why you would hear _so much_, density wise, about code patterns, code health, testing and so on in the ecosystem in its peak era, all of those things were really just mechanisms to try to achieve the above factors.
Lots of languages can format anything to a string though. Even Java will let you write
"foo " + null
And logging / error handling / debugging seems like the context where you would least want nil to become an empty string (and thus be confusable with an actual empty string, or not appear in the output).For example, you can write something like:
bananas = 4
puts "you have #{bananas} banana#{'s' if bananas != 1}"
And this works because: `if` is an expression, `if` evaluates to `nil` if the condition is not met and there's no `else`, and `nil.to_s` is an empty string.> And logging / error handling / debugging seems like the context where you would least want nil to become an empty string
Yep, 100% agree. And i think Ruby's designers agree too, since the language has the shortest method possible, `p`, for print-debugging. You can do `p something` and that'll print a useful string for debugging (`""` for an empty string, `nil` for nil, `[]` for an empty array, etc).
What is your point of that example, then? Absolutely nobody imagines any languages has no way of converting any value to a string.
To me it came across as implying you were implying this was equivalent to what Ruby will do with the equivalent expression.
I don't tend to use it myself because between safe navigation and things like Array() etc. it's rarely needed.
Which example from the list is weird?
I would not write
var_that_might_be_nil.to_h
I find this more understandable
var_that_might_be_nil || {}
But I find both acceptable and fine.
var_that_might_be_nil.presence || default
reads better imoIn particular it very specifically does not return the default only when the object is nil, and I have plenty of code where people thinking they're the same would cause serious breakage.
Some things in Ruby are patterned directly after Lisp. You notice it here and there, like the #<...> notation for unreadable objects, or calling small integers fixnum, ...
In the world of static reference OOP like Java and C++, there is the Null Object Pattern:
https://en.wikipedia.org/wiki/Null_object_pattern
Because nil is the empty list, one use for specializing on the null class is for handling the empty case of a list.
(defmethod do-some-list-thing ((list null))
;; empty list case
...)
(defmethod do-some-list-thing ((list cons))
;; non-empty case
...)In Ruby nil is a representation of emptiness, so when using the to_ conversion methods you will get an empty instance of a type. If you don’t want this behavior you use a constructor instead.
FYI For a long time there was no nil.to_h and there was some consternation about adding it. But finally it was embraced and this emptiness notion solidified into the language for good.
There’s nothing dangerous about any functionality in any language if you know the behavior. Not that I’m saying this is a particularly dangerous functionality of Ruby (coming mostly from other dynamic/scripting languages, it seems refreshingly explicit if nothing else), but I don’t think expertise in usage of a footgun is what should determine whether it’s a footgun.
I can agree that the behavior of casting to a number type should probably be undefined. But a null becoming an empty set seems fine to me.
For example, consider explicitly casting the string "abc" to an int. Python throws a ValueError. Ruby silently ignores the problem and gives 0. I consider the Ruby behavior to be a gotcha.
If you want an Integer or an exception you would call Integer(). If you want an Integer or nil, you would call Integer.try_convert(). If you call to_i, you're explicitly asking for a best-effort conversion, and shouldn't be surprised that's what you get.
At the same time it might be nice if they had names that were differentiated to indicate the behavioural difference, but I don't know what a better, similarly short option would be.
We also do have "Integer.try_convert" and "Array.try_convert" which both consistently will refuse to convert "nil", though returning nil on failure instead of raising an exception, but I very rarely see it used.
How would that work, though?
A value of 0 is conceptually different from not having a value
nil = nil
nil.to_i = nil.to_h
0 = {}Processing...