The example from the blog post is a classic error we've all made. There seems to be an obvious opportunity for Rails do something creative about that.
I like the String? sugar and believe there is probably some opportunity in Rails to address it. For example make a new NillableObject class that wraps the real object or delegates method_missing.
The idea would be to throw a warning, raise an error, etc. when accessing a nillable attribute. This could get obviously more robust than the below example, hook into read_attribute, leverage validations, log a warning instead of raise a runtime error, etc... but hopefully the point is made.
class NillableObject
def initialize(obj = nil);@obj = obj;end
def method_missing(name, *args, &block)
raise "I'm nillable, don't call methods against me directly"
end
def try(&block)
return if @obj.nil?
block.call(@obj)
end
end
ns = NillableObject.new
ns.gsub("f","g") # RuntimeError: I'm nillable, don't call methods against me directly
ns.try{ |o| o.gsub("f","g") } # => nil
s = NillableObject.new("food")
s.gsub("f","g") # RuntimeError: I'm nillable, don't call methods against me directly
s.try{ |o| o.gsub("f","g") } # => "good"
Yeah the runtime thing is a disadvantage over type systems for sure. It might be possible with a NillablObject to do some kind of sanity checks at Rails initialization time. Also some other safety nets could help for example use of a ViewModel object to dictate some constraints on Views (type constraints and others).
I like Lucky’s table definition in the model class. A similar construct in AR might be useful in implementing some sanity checks. I often wish there was a way to get a table definition into AR while keeping the goodness of Migrations.
I wonder if the culture of "Optional" with ".and_then" / ".or_else" has any foothold in the Ruby or Crystal communities. Functional approach is so much simpler and more elegant; with Ruby's blocks, it can be made to also look natural.
With promises and things like array.map being widely accepted in e.g. JS community, I'd hazard to say that mainstream industrial programming finally starts to embrace the use of monads. It would be great to embrace the most badly missing of them all, Option / Maybe, instead of the "billion dollar mistake" of null. ("Nullable" is a half-step in the right direction.)
I believe that while monads are great, crystal obviates the need for option because String? doesn't mean "nullable string", it means (String | Nil) or "the type which is the union of String and Nil". This is a much more powerful and generic concept than nillable types, as you can call any method in this object which is defined both on String and Nil. One such method (which happens to be defined in every type) is the method try. This is equivalent to a map in monad optional. I'm sure you can see that this generalises to make type unions possible to represent the optional type (with no overhead) and all methods which you could implement on it. Furthermore, its actually more powerful as this is implemented at the type system level, meaning flow typing works, which makes code much cleaner as you can use normal if statements with boolean conjugates to "unwrap" these types (really just removing types from the union) which is cleaner.
Unfortunately this is not likely second nature and will continue to bite us, especially where our expectations about AR models are not well thought out. A wrapping class for nillables might force safe access of AR attributes.
I like the String? sugar and believe there is probably some opportunity in Rails to address it. For example make a new NillableObject class that wraps the real object or delegates method_missing.
The idea would be to throw a warning, raise an error, etc. when accessing a nillable attribute. This could get obviously more robust than the below example, hook into read_attribute, leverage validations, log a warning instead of raise a runtime error, etc... but hopefully the point is made.