7 ms·
But if you're passing a state object around, you might as well use a class, no? Admittedly simplifying a bit, an instance method is a function that implicitly t
by cataphract 6y ago
But if you're passing a state object around, you might as well use a class, no? Admittedly simplifying a bit, an instance method is a function that implicitly takes "this" as the first argument.
- amw-zero 6y agoWhy? You would never suggest this if OO wasn’t the predominant paradigm. What is the benefit of implicitly passing the parameter? I prefer to see it passed explicitly, so the implicit passing is a downside to me, not a benefit.
- hamandcheese 6y agoThe benefit is encapsulation.
- _0ffh 6y agoDon't need a dedicated OOP lang for that benefit. It's an age-old discussion. As old school software engineering became sufficiently advanced, some people bundled their favorite "state of the art" concepts, and called it OOP. (Also: Multiple people, multiple different concepts of what "OOP" actually precisely means.) It became a horrible hype, and from then on out everyone was taught to see all these concepts as "OOP concepts" that elevate OOP over other programming paradigms. But they're not and they don't. The only real "advantage" is, OOP languages are opinionated and will try to force their interpretation of these concepts on the coder. Which is nice, if you happen to like it that way. Just not everybody does.
- fnord123 6y agoEncapsulation's a weird one. It's a cornerstone of OO but most/all OO languages have rubbish support for ABIs so changing your object in one library means you need to recompile your code using the library. This is the opposite situation that encapsulation seems to promise (in my view).
- thom 6y agoIs encapsulation different in either case? Nobody should be operating directly on the state, any more than they should be reflecting on private fields in a class.
- fauigerzigerk 6y agoThe canonical answer is encapsulation. But in my view it's rarely a good idea to couple state related to an algorithm's invocation (e.g a cache) with the data structure itself.
- noobermin 6y agoIt's almost as if a hard and fast rule doesn't work, ie., the point of the article.
- jabirali 6y agoIn many languages, it is an explicit parameter though. For instance, in Python, one would define a function with `def fun(state)` or a class method with `def fun(self)`. In the function body, one would use respectively `state.var` or `self.var` to refer to a variable `var` in the state object. Running it is then done using `fun(state)` or `state.fun()`. There are some minor syntactic differences between the two, but I find both these examples to be just as explicit...
- still_grokking 6y agoSo you would not use type-classes in Haskell as they rely on implicitly passed parameters to functions?
- kqr 6y agoI can't speak for the GP, but what makes typeclasses in Haskell work for me is that there's a very strong cultural force ensuring that they are truly about consistency boundaries and maintaining invariants/laws. Haskell developers don't just create a typeclass to bunch related functionality together. Doing so is a common beginner's anti-pattern. Typeclasses exist to wrap up general operations that satisfy some laws. When objects in OOP are used in the same way, I don't mind it as much. It's when they're not that I don't find the syntactic inconvenience worth it.
- js8 6y agoIn Haskell typeclasses, it's the type of the parameter that is implicitly passed, not the parameter itself.
- uryga 6y agoi don't think that's correct, afaik typeclasses roughly desugar to passing a record of functions (vtable/"dictionary") at the call site
- taejo 6y agoAnd that vtable depends on the type, not the value.
- uryga 6y agoi guess there's really two implicit arguments - the value's type, `a` (quantified via forall - the System F "Λ") and its vtable `Show a`... though i guess the vtable is uniquely determined by the type bc in Haskell instances are globally unique
- mcv 6y agoIf it's already explicitly part of an object, do you need to be explicit about the same thing twice?
- kqr 6y agoThere's one big difference in terms of convenience in many common implementations of classes and methods. There are essentially three different pieces involved: In OO notation, we have A.B(C) In almost all languages, you can store C in a variable and supply it "later": z = C A.B(z) You can also store A in a variable and supply it "later": x = A x.B(C) However, it is much more rare to be able to store B in a variable and supply it "later". Often, you must store A.B: xy = A.B xy(C) This means that at the time you select which method to call, you must also have at your disposal the instance on which you want to call it! With functions, things get easier. If you have B(A, C), you can generally just store B in a variable on its own: y = B y(A, C) Some OO languages allow you to store plain methods that aren't yet bound to a particular instance (JavaScript, modern Java) but far from all do, without resorting to inconveniences like y = lambda o: o.B Please note that what this workaround accomplishes is effectively to turn the method into a function! I suspect the main argument here is that it just doesn't make sense notationally to treat the first argument to a function specially. It only introduces wierd edge cases that have to be worked around. Plain old function notation has stood the test of time for good reason: it's flexible and handles most of the common cases we want.
- DarkWiiPlayer 6y agoI'd just like to add that you can still have the `A.B(C)` syntax, if you just define it as syntactic sugar for `(class of A).B(A, C)`
- edflsafoiewq 6y ago> This means that at the time you select which method to call, you must also have at your disposal the instance on which you want to call it! That is also its advantage, since the method to call can depend on the instance. Not just for dynamic dispatch either. A statically dispatched call will look up B on classof(A).
- vanderZwan 6y agoI like the approach Wouter van Oortmerssen uses in Lobster: class Animal: alive = true class Cat : Animal def hello(): print "meow" class Dog : Animal barked = 0 def hello(d::Dog): print "bark!" barked++ let d = Dog {} d.hello() let a:Animal = d a.hello() In other words, A.B(C) is just syntactic sugar for B(A, C), and class definitions are just syntactic on top of that. The relation between OO and regular functions and stateful objects is completely transparent (the only real gotcha I can see is that a.hello() still works here because it was assigned a subclass where this method is defined). This makes it easy to store B for later usage, like you wanted to do in your example, and reason about its behavior. [0] http://strlen.com/lobster/ http://strlen.com/lobster/
- fnord123 6y agoLet's use C and C++ as an example. What does the code look like? myobj.cool_func(1,2,3); // C++ cool_func(myobj, 1, 2, 3); // C Is this better? If you use a vanilla C++ style class then the class definition is in the header. All changes to the class require a recompilation and break the ABI of the class. This means you basically cannot release patch versions of your library because consumers cannot link against it if they have the old headers. Contrast with C where you would forward declare a struct in the header and pass that around while the definition is hidden in a .c file. This means changes to the size of the structure don't matter because all calling code ever sees is a pointer. (True data hiding!) It's called an opaque pointer and many many libraries use this pattern. e.g. libcairo. There is then a pattern to do this in C++ called pimpl: https://en.cppreference.com/w/cpp/language/pimpl https://en.cppreference.com/w/cpp/language/pimpl This solves it for C++ but the alleged convenience is gone because now you're coding C++ with the supposed convenience but still your class implementation has to trampoline all the calls to the pimpl which is an extra function call (if you're writing C or C++ this might matter). But RAII, though. Not having [standardized] RAII sucks.
- nicoburns 6y ago> This means you basically cannot release patch versions of your library because consumers cannot link against it if they have the old headers. This is pretty much only a problem in C/C++ though. Everyone else is using static linking (which you can of course also do in C++), or a VM of some form. IMO relying on a library having stable ABI is a bit of anti-pattern. It's like relying on the implementation details of a function/class rather than it's API: sometimes it's necessary, but it should be avoided if possible.
- fnord123 6y agoJava has the possibility of maintaining ABIs as well. And I imagine that's what .Net assembles are about too. I think you're heavily over stating reliance on libraries having stable ABIs as an anti-pattern. If a mistake is made in the ABI promise, can it get hairy? Absolutely. If you work on an application and are somewhere in the middle of it and need to recompile it to test a fix, do you want to wait for the compilation of the entire application to test it? Obviously not.
- fluffything 6y agoWhy? The main advantage of passing a state as an argument is letting the user control the state. If you need an array as state, the user might want to put the array on the stack, or the heap, or in static memory, or protected behind a mutex somewhere... It doesn't really matter that much how things are actually implemented: most languages are flexible enough that you can do this with a class or a function or even implement the algorithm as a generic interface. But if you want the algorithm implementation to be reusable, you have to remove as many assumptions about the state as possible because otherwise some users won't be able to use your algorithm. If you pick up a class, and the class owns the state, now you need to make the class generic to allow the user to customize the state, you need to allow the user to break the state invariants, to be able to unsafely read and write from it, because otherwise you are forcing the user to read into a separate buffer, and then make a copy to your class, etc. Somebody that knows how to avoid all these issues when writing their algorithm as a class, probably also knows that by just using a function most of these issues just cannot happen anymore (or are much harder to introduce). You can also write the algorithm as a function, that takes some generic state, and provide a "class wrapper" for convenience, so that those who don't want to customize anything don't have to. But then your class doesn't implement the algorithm anymore, it just wraps it.
- jmull 6y agoIf we’re talking about an algorithm as a class, the state managed by the class is algorithm state, not the data the algorithm acts on. The data the algorithm acts on should probably be passed as a parameter regardless of whether the algorithm is implemented as a class or top-level function.
- fluffything 6y ago> If we’re talking about an algorithm as a class, the state managed by the class is algorithm state, not the data the algorithm acts on. I was only talking about algorithm state here. Check, for example, how the C++ string searching algorithms work to get a feeling of how these APIs work in the wild. For an example of this gone wrong, look no further than C++ stable_search, which does not let the user reuse the merge sort buffer across searches.
- logicchains 6y ago>But if you're passing a state object around, you might as well use a class, no? A lot of your functions aren't going to need access to the whole state, only to some of it. If we're not using a class, we can just make these functions take only the needed arguments, rather than the whole state. This makes them easier to reason about, as we can know from the function signature that it only looks at the params we pass in, not the whole state. It also makes them easier to reuse elsewhere in the code, where we might not have the full state but we do have the arguments to the function. For any function that doesn't need to access every member variable of a class, making it a class member function essentially creates an unnecessary coupling between that function and the class members variables it doesn't use. This makes it unecessarily harder to reuse elsewhere, as anyone who wants to use it needs to create an instance of the whole class, including any variables that aren't needed by that function.
- lmkg 6y agoThis sounds like exposing internal implementation details. You're pushing some complexity onto the caller, and breaking up the uniformity of the interface. Sometimes the caller will want to manage that complexity for the reasons that you describe, but it's not always good to force the caller to wrangle that complexity. Design trade-offs, as always. I get why you would do it your way, and it makes sense in some applications. But for some applications the fact that you're hiding which specific bits of state each function depends on, is actually the point of using a class.