@noelrap OK, I have some opinions.

Static typing does prevent simple errors, but in even a moderately complex case, you can’t count on it for all your data validation needs.

I think this is the key contention point. A type system totally can express valid data but this is just not how we're used to write Ruby/Rails code.

In Rails (AR) you're supposed to represent both exiting but invalid and valid object by the same type. For example, we have a User class. A user record is always a User instance regardless of the data validity. Valid and invalid user records should have different behaviour and conceptually they do but we still keep them in the same class.

A proper typed approach would separate the two. We’d have UnvalidatedUser and User. UnvalidatedUser would allow partial data, invalid data, etc. And it would not allow doing anything that requires a valid user. E.g. checkout wouldn't accept an UnvalidatedUser instance. So a typical Rails flow would first dump params into UnvalidatedUser, then run all the validations and if everything is fine we get a proper User out of it:

class UnvalidatedUser
def validate() -> Either(User, Array<ValidationError>)
end

I use Either here because RBS, unfortunately, doesn't encode exceptions but in a typical Ruby it might as well raise a ValidationError or return a User.

So instead of a typical

def create
user = User.new(user_params)
if user.valid?
user.save
else
render :new, locals: { user: }
end
end

We'd have something like this:

def create
unvalidated_user = UnvalidatedUser.new(user_params
user = unvalidated_user.validate
rescue ValidationError
render :new, locals: { user: unvalidated_user }
end

So the issue, arguably, is that we write code that is not properly structured for easy typing. In essence, we have a type that is actually a superposition of two types and we can not separate them at any given point in the code.

That said, I don't think your example is completely hopeless.

Under the post’s premise we want to make sure that the user has an address before we can checkout. The solution is rather simple:

class HandleShipping
def send_item_to_(address: Address) -> void)
end

The issue is that now all users have to have an address at all times. Which bring us back to the unrepresented UnvalidatedUser type.

1
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin
Replies