3 ms·
So the example manages to evade any need for encapsulation. It's viable because the internal hidden state is so cheap to compute (the full URL) that it can affo
by zmmmmm 6y ago
So the example manages to evade any need for encapsulation. It's viable because the internal hidden state is so cheap to compute (the full URL) that it can afford to reconstruct it within every call.
But imagine if constructing the url was a more expensive operation. Perhaps it has to read from disk or even make a database call. Now you really need to cache that as internal state. But doing that means external manipulation of root_url or url_layout will break things. Those operations need to be protected now. So do you make "set_url_layout" and "set_root_url" functions? And hope people know they have to call those and not manipulate the attributes directly? Probably you'd feel safer if you can put that state into a protected data structure that isn't so easily visible. It makes it clear those are not for external manipulation and you should only be interacting with the data through its "public interface".
Of course this brings about all the evils of impure functions, mutated state etc. But if you are going hardcore functional in the first place then it becomes rather obvious OO is not going to fit well there, so its a bit of a redundant argument then.
- brundolf 6y agoI don't think the OP makes very good points, but one thing to point out is that Python only has access-control by convention. This creates a wrinkle for any argument based on using "protected data structures"
- theamk 6y agoWe use python extensively, and I think python's "access control by convention" is pretty great. The reason for access control is not to protect from malicious programmers -- it is protect from programmers who don't know better and from honest mistakes. So all you need is a simple rule ("don't access private methods") and now everyone knows which fields are public (and thus are OK to use) and which fields can disappear any moment. For extra enforcement, you can add pylint to your CI so it flags all the protected accesses. But really, a good team culture is often enough.
- Blikkentrekker 6y ago> So the example manages to evade any need for encapsulation. In Scheme and many other lisps there is no difference between “objects” and “records” and “fields” and “methods”. Defining a record with a number of fields also defines accessor functions, such that: (define-record point (fields (mutable x) (mutable y))) defines the following procedures: make-point point? point-x point-y point-set-x! point-set-y! All these names can optionally be changed to something else. Encapsulation is simply enforced by not exporting any of these procedures from the module wherein they are defined. Any arbitrarily more complicated functions can be built on top of these which can be exported. As such, there is no meaningful difference any more between an object and a record, there is no special “dot notation” for field access and method calls, and normal procedure calls are used. This follows the general lisp design principle of avoiding special syntax as much as possible and using ordinary procedure calls for everything. There is no special “subscript notation” for `vector[i]` either, and `(vector-ref vector i)` is used instead.
- leontrolski 6y agoHi, what I'd really like is to test my conjecture - would you (or anyone reading) be able to email me a (< 500 line, ideally not super-deep-library-code) bit of code (constructed or otherwise) that you think is "so innately OO friendly that the conjecture won't hold". Given some examples, I'll do a follow-up blog post.
- zmmmmm 6y agoYou could take the example as-is with the new requirement that the remote end point may require up front authentication. How the authentication works depends on the service, and it needs to be done on a per-URL basis. The authentication protocol may return some kind of token or key needed to use with future requests. However what this is and exactly how it is used is service dependent (for example, it might be supplied verbatim in an auth header or it might be a signing key and it might even be salted with some credentials etc). So challenge that is introduced is that you now have hidden state that is dependent on the public state but which cannot be computed by users of the client or the parent class. In fact, code in the parent class can't even know ahead of time what state might be needed to be retained. I am guessing you will probably be able to propose that authentication be supported by some kind of pluggable authentication interface, but it will still be difficult to deal with the hidden state without introducing assumptions about the auth protocol or the type / nature of the state retained and without having that protrude into the parent interfaces.
- curryst 6y agoYou can just define them as functions that are part of the class. Something like: @dataclass class AuthMethod: credentials: typing.Any cache: typing.Any apply_to_request: typing.Callable[[typing.Any, typing.Any, Request], None] @dataclass class AuthMapping: method: AuthMethod matches: typing.Callable[[str], bool] class Client: root_url: str url_format: str auth_methods: typing.List[AuthMethod] def auth_request(client: Client, req: Request): applies = filter(client.auth_methods, lambda x: x.matches(req)) if len(applies) > 1: raise Exception("multiple auth methods found") elif len(applies) == 1: method = applies[0] method.apply_to_request(method.credentials, method.cache, req) Then you would statically define your service's auth methods by URL, so users can just do: my_client = Client(auth_methods=MyServicesAuthMethods) or you can create a helper function like: def create_client_a(base_url: str): return Client(base_url, auth_methods=[...]) Alternately I think you can use closures, but I'm not totally positive about the scoping. AuthMethod = typing.Callable[[Request], None] def auth_method_a(username, password) -> AuthMethod: my_token = None def __auth_method_a(req: Request): if my_token is None: my_token = do_login(username, password) req.headers['token'] = my_token return __auth_method That creates a standard interface (a function that accepts a Request), without assuming anything about the underlying protocol.