15 ms·
Counter-point: while in many situations this isn't the right approach, it's worth recognizing when this is the right approach, because they can look awfully sim
by dataflow 6y ago
Counter-point: while in many situations this isn't the right approach, it's worth recognizing when this is the right approach, because they can look awfully similar. An example of this is graph searching, e.g. BFS or Dijkstra's algorithm. The typical implementation is a function. But if you make Dijkstra a class, with (say) a function to iterate through nodes, it lets you do several things that would be difficult with a function: (a) you can now re-use the same object for computing distances to multiple vertices, which allows incremental search, (b) you can now fork/copy the object and continue searching on different extensions of the original graph, (c) you can now save & restore the searcher state to pause the search & continue it later, (d) you can do multiple searches across one or more graphs in lockstep. IMHO these are very much not obvious, and to someone who doesn't recognize what's going on, a class for something like BFS will look exactly like an "OO antipattern", when in reality it's actually providing significant additional functionality for more complicated situations.
- Laremere 6y agoI believe you describing at least one of two cases: 1. There are multiple methods on the object (not counting constructor) 2. There are arguments passed to the methods. Either of these cases are different from the provided example: The example will only ever compute a single value per object, always returning that value from a single method with no arguments (other than self).
- mehrdadn 6y agoNot necessarily. What I said can apply to even a class with with 1 method that takes no arguments... though you might often recognize it as a more sophisticated kind of iterator: class BFS(object): def __init__(self, origin): self.origin = origin; ... def next(self): ...
- twic 6y agoRight. I think the article is talking about one very specific case, which is apparently common amongst novice C++ programmers, where there is a single computation, whose only inputs come from the constructor, and which gets done in a method. There are plenty of cases where it makes sense to wrap a function in an object, but the point of the article is that this isn't one of them!
- deleted 6y ago[deleted]
- amw-zero 6y agoNone of these situations require an object to do. You can always have a function that takes in an extra ‘state’ argument. You can then do everything you said by passing in the corresponding state values.
- rocho 6y agoPrecisely. And that's the way it's done in functional languages.
- millstone 6y agoNo, it's done inverted in functional languages! In a FPL your graph will be an ADT. The advantage is clients can pick it apart, but there is no easy way to extend it. If you wanted to "use the same object for computing distances to multiple vertices", you're screwed: the ADT is laid bare, and there is no data hiding. In an OO language, your graph will be an opaque object, and you can write things like CachingDijkstra or ParallelDijkstra and make these work transparently for your clients. The price is that you are bound to your APIs: a client cannot pick apart your data structure, because it's opaque.
- danielscrubs 6y agoWe do have data hiding in functional programming, even if we would not have modules, which we do, we do have type classes, and for example in OCaml you can hide constructors if that's your jam. It's not like when I use a sorting function that the implementors couldn't swap it out for a caching one or a parallel one. One of the amazing things I remember is changing an int to a float in a very well used "struct" that was touching thousands of functions but because the functions in Haskell usually so extremely general I didn't have to change a single other line (Number type is heavily used and doesn't incur an extra performance cost, which it very much does in Java). In fact we can be even more precise or loose in what assumptions need to be made via dependent types in functional programming languages like Agda. I encourage you to take a look at any popular Haskell-framework to see how it's done in practice.
- 6y ago
- anonymoushn 6y ago(a), (c), and (d) seems to be about exposing the algorithm's state and a function that performs a single step of the algorithm, rather than about having a class. (b) seems like quite a bit of trouble, since the clone function must either copy the underlying graph by default or provide a mechanism for swapping out the underlying graph that fixes up the internal map of (node->distance) and the internal priority queue to refer to things in the new graph.
- quietbritishjim 6y agoIf you do use a class for this, of course you can provide a wrapper function that constructs the object, calls the "run" method on it, and returns the result. This can be in addition to the processing class, or it can be the entirety of the public API while the processing class is defined privately in an implementation file. I've done this before for Munkres, also called The Hungarian Algorithm, which assigns jobs to workers. Logically the interface is a simple function, but internally it's very useful to have a class with multiple private methods.
- mehrdadn 6y agoYeah, I almost suggested a static method for the common case too. It's nice to have a convenient interface for the simpler use cases.
- chrischattin 6y agoI get how this works and the reason for it. It just doesn't feel DRY to me. Like we're adapting to satisfy a pattern vs the pattern serving us and our dev goals. Aesthetically unpleasing, imo.
- encoderer 6y agoWe suffer so many bad abstractions at the church of DRY.
- mehrdadn 6y agoThere's nothing repeating here... you define a procedure to achieve a higher-level task, which is a combination of other tasks. Kind of like File.ReadAllBytes() vs. FileStream. Later you use it when you need to perform that high-level task.
- throwaway894345 6y agoIsn’t this all just different approaches to currying? It’s often convenient to have some function that you can call without passing the same subset of parameters to each invocation. Whether to curry depends on how much repetition is involved and how “expensive” it is for the programmer to curry. In the case of “curry by creating a whole named class”, the expense is relatively large, while a functional currying approach is much cheaper.
- crimsonalucard1 6y agoYou still hit the same issue and you can still solve it the same way by converting that class with saved state into just a function. Below are two short examples of two different ways to do this: type Node = Null | {value: int, left: Node, right: Node} //a Node is NULL or a value that is an int with left and right nodes //procedural with mutation, saved state is placed in parameter: found def find_n_and_print_all_DFS (node: Node, search_value: int, &found: Node) -> Void: if node == Null: return else: print(node.value) *found = node.value == search_value ? node : Null find_n_and_print_all_DFS(node.left) find_n_and_print_all_DFS(node.right) //functional, no mutation, saved state is returned def find_n_and_print_all_DFS (node: Node, search_value: int, found: Node = Null) -> Node: if node == Null: return found else: print(node.value) found = node.value == search_value ? node : Null left = find_n_and_print_all_DFS(node.left, found) right = find_n_and_print_all_DFS(node.right, found) return left != Null ? left : right I would argue that if I used classes the code would be way harder to read and much more verbose. You can essentially use tail recursion on that DFS and pass in some accumulator as a parameter into your DFS or whatever. That accumulator can be some pointer representing some saved state or anything you want. There is still no need to group everything together into an Object. It's a bit trickier but in the second example you can see that you don't even need to mutate anything at all. What you described can be achieved without redundant OOP classes and without mutating state at all! Additionally setting found to have a default value of Null negates the need for the extra accumulator parameter during function calls as you can just call it like this: saved_node = find_n_and_print_all_DFS(x, 5) I would still say OOP is an anti pattern because it overall promotes adding these mutating parameters in your code even when you don't need save states or anything of that nature. In OOP, the existence of getters and setters and methods for accessing variables outside of the definition of the method itself heavily promotes this type of coding style regardless of whether or not it is needed. The procedural style forces this additional "saved" feature to be evident as an extra parameter. If you never use that parameter it becomes obvious that the parameter is redundant while in OOP mutating external state is an intrinsic part of the style and like the OPs example you have to go through several logical leaps to see how redundant it is.
- jvanderbot 6y agoYou're describing a classic use of iterators, which is itself a classic use of Objects-as-contracts.
- wwright 6y agoIMO it’s even simpler to use a monad-type extension to functions to suspend state. (This can be coroutines/generators, explicit Haskell monads, some macros encapsulating longjmp or whatever.) queue <- { root } while queue is not empty node <- dequeue one node # One of the following: # for a simple function approach… if predicate(node) return node # or for a monadic approach, can be overloaded generically… yield! node for child of node enqueue child The monad-style extensions fits with the other pieces of the language much more naturally IMO. You could easily use a predicate monad to regain the functionality of the naive implementation, or a more complex monad if you want a complex BFS traversal, but the function looks almost identical either way. Python and Ruby and Lua and JavaScript even do this! C++20 will probably make this somewhat more common too. (The word “monad” may set off sirens here, but it really just refers to a feature similar to but somewhat more generic than “yield” in a coroutine/generator.)