- 3mo ·
- 1m read ·
-
Public·
-
status.pointless.one
@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.