PointlessOne's latest activity

@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

@searls Cellular add buoyancy that effectively reduces weight. Obviously.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@nateberkopec even without doing that I know it’s my private Mastodon instance.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap I understand why this whole typing thing seems a bit foreign in Ruby. Basically for all the Ruby eternity we were writing unsound code from the typing perspective. Trying to adapt a more strict system to our lax code will fill awkward. Adapting our code to a proper type system is a more productive approach. That said, even a partial typing of our existing code is useful so we can start there. Get used to explicitly type our interfaces at least to some degree and maybe eventually we’ll adopt new ways of writing code that are more suited to stronger typing.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap Sure. It's not like I invented it. This is thoroughly discussed in a lot of material for newcomers to strongly typed languages.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@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

@tenderlove @john How do you even pick a number for IO? ncpu is easy and usually better than 1 anyway.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap All right, that was 3am during an air raid alert but now it’s morning so here it goes.

First of all, you do you. If it works for you, fine. If I ever lay my eyes on code like that I will judge it but that ultimately shouldn’t concern you or anyone really.

All-public is bad design. It’s basically Hyrum’s law accelerationism. Exposed bits are assumed to be available for use. Users will look through available API surface and will use those things even if they might’ve used something else instead. This also increases chances of incidental coupling. Thus locking not only the public API portion of the code but also internal—functional—part of it.

Testing argument, I believe, is misguided. Tests are not the product. Tests are developer confidence booster. They’re a poor design tool as they tend to expose every single bit. Presumably, you have an application you’re writing your service object, or a library, or whatever. That, presumably, models some domain process. Use that as your guide for exposed functions. You mention that it’s hard to test complex service objects that have only one public method. Use `send` if you feel that’s easier but also consider that every unit test on that extracted method might shift you further away from good coverage. You might have 100% coverage of individual methods but you might not have coverage on all the combinations they’re used together.

This testing of “private” methods can hide another issue. These methods might appear used (they’re hit by tests) but they might be not used anywhere in the application code. Smaller API surface can suffer from this, too. But it’s easier to spot dead code when most of the tests describe problem domain which tends to be the case with minimal API approach.

Now, you say that these methods might be useful to users. That’s true. And there’s a solution here: make them public when the use case actually arrives!

From my experience only personal scripts are kinda OK to be all-public: I do what I want, no one but me can stop me and it’s the only case where the developer and the user are in perfect agreement because both of them are me. In any case where those two can be different people I found it’s better to follow minimal public API approach. More so when any of those can be multiple different people.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap I don’t agree but I just don’t have energy to debate it. 😔

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

@noelrap API documentation.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap @alessandrofazzi @geeksam I hear what you're saying. However…

"Usually" means usually. It is a very specific thing in Ruby and its use is limited. That's all I read in that statement. I don't see it as "stay away”. It's more like “if you don't know the difference you probably want that other thing, if you do all power to you”.

As for “this is slow”… It might be slow. If it’s on a hot path you probably should benchmark it. As I said, it's a rather rare thing so it might not have been optimized. Or it might have been. That comment was added 8 years ago, in 2.5. A lot of things have changed since then so that particular part might not hold any more. And it's not like instance_eval docs say “this is fast”. ;)

I'm not blaming you for trusting documentation. After all, that’s the primary source. But documentation is not infallible and some parts of it might be more trustworthy than others.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap @alessandrofazzi @geeksam instance_eval certainly can achieve the same but it always feels like I’m hacking my way into it, while protected seems like an intentional feature provided by the original class.

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

@searls System prompt: you're a helpful assistant. Try your best to answer user's requests truthfully. Also roast user’s ideas at every opportunity.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@tenderlove :help $VIM

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@searls So… where's the job ad?

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@searls Lingon X is a great UI to help with it.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap @Ryanbigg It'd be nice if someone redesigned the API docs, too. I mean, even just visually it feels a little bit 200x. Even just adopting Yard's default template would bump it up a decade.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@noelrap @Ryanbigg Yeah, the page certainly can use a review. RSense had last release almost 10 years ago, for one. For reference, it’s when Ruby 2.2 was the latest.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@tenderlove Tenderloaf.

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin

@tenderlove Happy birthday! 🎂

0
Share
Share on Mastodon
Share on Twitter
Share on Facebook
Share on Linkedin
Replies