5 ms·
You are getting side tracked. Implementation details have nothing to do with it. It's about state and functions. He is saying rather then have an entity manage
by deltaonefour 4y ago
You are getting side tracked. Implementation details have nothing to do with it.
It's about state and functions. He is saying rather then have an entity manage both state and functions, Just have a function take in state. The implementation details are besides the point. Why? Because you can arbitrarily wrap that socket up with ANY type of wrapper if you don't like "implementation details". Wrapping it up in a lambda is probably the worst thing to wrap it up in.
He is saying that the trade off of a slightly larger function signature that takes in an extra "wrapped socket" is WORTH higher modularity and composability and lower complexity.
Example:
writeStringToWrappedSocket(s: WrappedSocket, s: str)
writeStringToFile(f: File, s: String)
print(s: str)
generateLogString(s: str) -> str
generateFilteredString(s: str, match: str) -> str
capitalize(str) -> str
appendSuffix(s: str, suffix: str) -> str
generateCapitalizedFilteredString(s: str, match: str) = capitalize(generateFilitedString(s, match))
logCapitalizedFilteredStringToSocket(s: str, match: str) = writeStringToWrappedSocket(wrappedSocket, generateCapitalizedFilteredString(s, match))
logFilteredWithDateToFile(s: str, match: str, f: File) = writeStringToFile(appendSuffix(generateFilteredString(s, match), time.date), f)
Boom look at that! How many different types of logging functions can you produce simply by composing some primitives together? I have achieved greater compositional flexibility then OOP in MUCH fewer lines of code and significantly less complexity. With OOP you need an ENTIRE ARTICLE to explain strategies trying to get around a problem caused by over complicating things.
There is a cost to just using normal primitive functions and avoiding classes. What's the cost? The function signature needs to take in a wrapper to a socket while the OOP version doesn't? No that's not much of cost. The actual cost is more subtle. The version I present here is propagating logical management of the lifetime of WrappedSocket to be handled by the parent context. It's not really a "cost" per say, it's just shifting the problem somewhere else. This is much less of a concern for most popular languages where garbage collection handles the lifetimes. Which is another way of saying if you're not using C++ the above method is the better way to go then OOP. And if you're using OOP, then the above is a library that can ONLY be used in the context of the class as some destructor has to call file.close().
Note I am not saying that OOP is bad. It works for many use cases. But it is also in general one of the LEAST modular and reusable patterns in programming. It is also over used and over popular. You use it when you have no choice. I am also NOT promoting functional programming.
- kgeist 4y agoSorry, I didn't understand your example. As a client, I want to say log("some error happened") without caring how it's done because it's not my responsibility. How would you do it without classes or closures?
- deltaonefour 4y agoIf you can't read my example I am sorry to tell you, you don't have the necessary skill set to work as a programmer. What I wrote is much simpler then ANYTHING in the article. If you can read the article you can read my code. If you can't then I'm sorry.
- kgeist 4y agoIn your example, logFilteredWithDateToFile accepts a file. That means, if a function wants to log an error, it needs to accept a file somewhere to pass to the log function (for example, we want the sockets/files to be opened once and be shared across several calls). So if function A depends on function B which depends on function C, then function A will need to explicitly state all the dependencies of B and C in its function signature. We end up with a very complex function signature of A where we have to list all possible dependencies of every function it makes use of recursively (also, some of the dependencies may not always be needed depending on control flow). As a client, all I wanted was to ask a function to do one simple thing but now I have to manage files, sockets and god knows what else just because somewhere deep inside someone uses them. And then in a new version we decide we don't want to log to a file, we want to use a third-party library. Now we need to change signatures of A, B and C, and modify our code at every call site. Is this what you are proposing?
- deltaonefour 4y agoPeople interpreted my initial reply as arrogance. While it is a bit arrogant, there's a reason for the condescension. Because your flippant reply, implied you didn't even read the code. Any programmer can OBVIOUSLY read the code. You didn't and claimed you didn't UNDERSTAND it. Well here now, you have a full retort and explanation... so were you lying when you said you didn't understand it? Obviously you were. So my equally flippant response was appropriate. But let's get that out of the way. I appreciate your second reply here. >In your example, logFilteredWithDateToFile accepts a file. That means, if a function wants to log an error, it needs to accept a file somewhere to pass to the log function (for example, we want the sockets/files to be opened once and be shared across several calls). This is the intention of how the library works. It actually does promote the sharing of files and sockets. See trivial example below: s = WrappedSocket(...) logStringToSocket(s, "hello") logStringToSocket(s, "world") See. Socket is shared across several calls. The only thing that's a bit of the eyesore is that extra socket being passed around. But this is a problem that can't be gotten rid of. You either pass the socket all over your code or in the OOP example you pass an instance of a logging class around. The only benefit of OOP is that the logging class manages the lifetime of the socket. If calls socket.close() for you during destruction. That's it. >So if function A depends on function B which depends on function C, then function A will need to explicitly state all the dependencies of B and C in its function signature. No you don't. This is not mandatory. I expose only parameters that are needed by the user and the API. If I want to give the user the ability to filter I SHOULD expose two string parameters, s: str, match: str. because without it the user can't do anything. But I don't have to, see below: logHelloWorldWithDate() = print(appendSuffix("hello world: ", time.date().to_string())) appendSuffix has two parameters required. But I expose zero parameters. >We end up with a very complex function signature of A where we have to list all possible dependencies of every function it makes use of recursively There's no recursion going on in my example and none of the logging functions manage any state. Again the function signature is as complex as you want it to be. If there are OTHER required parameters like "socket" and you want to reduce the complexity of the parameters coming in, you can wrap say a socket and a file in a single struct. Not a huge deal either way... Often this variable is called ctx or context, you see it in languages that tend to be more against OOP like golang. For C++ you can have state managers. Often this exists in the form of things like ThreadPool or **Manager objects. These manager objects are more of a necessary evil though because they lack the huge compositional ability of my example due to the fact that they unionize state and function into a single primitive. Managers overall segregate state away from the rest of your program such that you can have huge compositional ability in the rest of your program as shown in the example above. >As a client, all I wanted was to ask a function to do one simple thing but now I have to manage files, sockets and god knows what else just because somewhere deep inside someone uses them. Yes as a client you do need to manage this. I mentioned this problem with the lifetime of socket or file. From the perspective of say a programmer who uses garbage collection it's the same amount of crap being passed around. s1= socket() s2=socket() logWithSocket(s1, "hello") logWithSocket(s2, "world") versus the oop version l1 = LoggerThatOwnsASocket() l2 = LoggerThatOwnsASocket() l1.log("hello") l2.log("world") Either you're passing a socket everywhere or your passing the log manager everywhere. If you want something that logs to both socket and a file (multiple dependencies) simple package both of those dependencies in a single struct. This struct can be used for both the OOP version and the non-OOP version. So from the perspective of a programmer who uses garbage collection, the problem presented by both examples here is the SAME, but the 1st example is superior because it is MORE composable and modular. >As a client, all I wanted was to ask a function to do one simple thing but now I have to manage files, sockets and god knows what else just because somewhere deep inside someone uses them. Either you're managing a bunch of LogManagers with all the dependencies, Or you're managing a struct with all the dependencies. The problem is the SAME. >And then in a new version we decide we don't want to log to a file, we want to use a third-party library. Now we need to change signatures of A, B and C, and modify our code at every call site. Explain this problem with an example. It's too vague. What is the nature of the third party library, what is it replacing? >Is this what you are proposing? I've hit all your points pretty thoroughly sentence by sentence. The only thing I haven't resolved is the third-party library thing. Feel free to explain that part and also offer counter arguments to my arguments.