15 ms·
The use of ‘class’ for things that should be simple free functions (2020)
- fevangelou 4y agoReminds me of the "Hello World" module in Drupal 9+...
- MeteorMarc 4y agoAlthough I agree with the sentiment, one has to consider: - in the JVM classes are an obligation - using functions instead of classes puts stronger requirements on sensible naming of modules and functions. E.g. in python functions from imported modules easily litter your namespace - not using unneeded classes makes your code harder to read for many colleagues. E.g. if I have a python module universe my colleagues hate it if I import the module and call a function like universe.create() [instead of Universe().create()]
- diatone 4y ago> JVM forces classes Fair enough but it does suck that it has to be that way. But yeah, fair enough. > Classes allow namespacing This is the same mode of issue as the above: when your language doesn’t support arbitrary namespacing or (hello Python, hello Java) then yes classes can get used as namespaces in disguise. It sucks that it has to be this way in these languages, but we play the hand we’re dealt. Great point > My colleagues have stylistic preferences I’m struggling to understand this one. Are you saying that your colleagues prefer instantiating classes over functions, just because, or is there a technical reason for that convention?
- mjburgess 4y agoI dont think `class` as a module keyword, in python, sucks. It just requires learning being open to python as python. Eg., class EG: NORTH, SOUTH, EAST, WEST = 1, 2, 3, 4 def move(a, b): return a-b EG.move(EG.NORTH, EG.WEST) etc.
- zelphirkalt 4y agoThat would be a case for an enumeration, not a plain class. This usage would make me think: "And where does it get instantiated?" It is a fake class and this usage should be avoided.
- mjburgess 4y agopython -- syntax doesnt just correspond to one idea, the class keyword has a semantics entirely independent of an OO interpretation the whole point of design patterns is, exactly, to reuse "class etc." syntax for essentially non-OO purposes
- stickfigure 4y ago> It sucks that it has to be this way in these languages Not really? I don't see why it matters whether the namspacing keyword is `namespace` or `module` or `class`. If all you want is modularity, they work pretty much the same. This feels like a bikeshed issue. There's nothing fundamentally wrong with `Math.min()` or just `min()` with a static import.
- Twisol 4y ago> in the JVM classes are an obligation You're always free to use a final class with a private constructor as a namespace for static methods. Many classes in the standard library are of this kind, including `Math`. It's not ideal (see my next point), but it's a far cry from adding extra object instantiations everywhere. > E.g. in python functions from imported modules easily litter your namespace I think this is more a problem with the modularity features in a language than free functions themselves. In Python, Haskell, and Agda, I make sure that every symbol I use is explicitly imported (or defined in the same file). This makes it way easier to trace where each symbol I use is defined. You're basically forced to do this in Java, too, with exception of wildcard static imports, which I very rarely use. > if I have a python module universe my colleagues hate it if I import the module and call a function like universe.create() [instead of Universe().create()] They hate it? I can understand if your colleagues aren't developers by trade, but I expect anyone who maintains code for a living to understand the language they're using. There will always be dark corners, but this ain't one.
- watwut 4y ago> You're always free to use a final class with a private constructor as a namespace for static methods. Many classes in the standard library are of this kind, including `Math`. It's not ideal (see my next point), but it's a far cry from adding extra object instantiations everywhere. And you literally gained nothing. You do exact same thing with slightly different syntax.
- sagarm 4y agoA version of Math that had non-static methods instead of static ones would be strictly worse IMO. Extra allocations, extra line noise from instantiations, very likely many users would start keeping Math objects around as local, member, or even static (lol) variables.
- Asraelite 4y ago> in the JVM classes are an obligation In Java, not the JVM. Some JVM languages like Kotlin and Clojure allow for classless functions. You could argue that it still gets compiled to bytecode with classes, but then that bytecode gets JIT'd to not have classes again anyway so it's kind of a meaningless distinction.
- kaba0 4y agoWell, the JVM is very vocal about having classes as a basic abstraction. Sure, everything is just bits at the end, but the additional abstraction here is Kotlin and Clojure hiding the classes, in my opinion. But sure, it’s not a fruitful topic to argue on :D
- pvg 4y ago'class' is a first class concept in the JVM. You can't really argue that it isn't by saying it's not in the layer below the JVM or the the fact a layer above it can hide it from you. You can't have a JVM without classes.
- Asraelite 4y agoI mean, the original post was about programming languages, but The JVM is a virtual machine. It does not have "classes" in the programming sense because it takes bytecode, not source code. A bytecode class isn't the same as a source code class. It would be like saying that x86 has if statements because it has branch instructions. I interpreted the comment to mean "JVM languages" but if you wanna discuss the JVM itself (which is nonsensical comparison here imo) then yeah sure, it needs "classes".
- pvg 4y agoYou don't have to get all talmudic about it, you can just open the JVM spec to almost any page. https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokevirtual https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.ht... The semantics of Java classes 'in the programming sense' are largely specified and implemented in the JVM.
- Delk 4y ago> not using unneeded classes makes your code harder to read for many colleagues. E.g. if I have a python module universe my colleagues hate it if I import the module and call a function like universe.create() [instead of Universe().create()] This sounds like a bit of a weird argument to me. Of course convoluted or strange solutions should be avoided, but `import universe; universe.create()` doesn't sound particularly strange in Python, and it's hard to see how there could be a problem reading it unless it's simply due to being used to a different style. People being used to a particular style could be seen as an argument for that style if that style is indeed a common convention in the language, but if `Universe().create()` is the style they're used to, that sounds more idiosyncratic than idiomatic in Python to me. Do your colleagues perhaps have a background in a different language? Local conventions at a particular organization may of course also be valuable to follow even if they're not global conventions for the language. But that doesn't really change what is and isn't a good style more generally. (`from universe import Universe; Universe.create()` would sound like a typical style to me if a class for universes were warranted in the first place, but in that case the class isn't unneeded.)
- plonk 4y ago> E.g. in python functions from imported modules easily litter your namespace `import *` isn't recommended. I only use it in `__init__.py` when I want a flat package that exposes symbols implemented in multiple submodules.
- civilized 4y ago> not using unneeded classes makes your code harder to read for many colleagues This would be really depressing for me but ultimately team limitations sometimes win out. If your team can only "understand" code that looks like Java, then maybe all code has to look like Java. In white collar environments, "don't understand" can be a sneaky, faux-polite way of saying "don't like and refuse to be open to". But it might add up to the same thing in the end.
- IshKebab 4y agoI'm not sure what your point about imports in Python means. Normally you shouldn't import *, you just import the functions/classes you need, so nothing pollutes your namespace. And for your last example did you mean `Universe.create()`? It should be a static or class method. Those are perfectly fine when they're closely related to the class itself, as with factory methods.
- kgeist 4y agoA class also acts as a namespace. If computation is complex and requires multiple subfunctions that you don't want to leak to the global namespace, I think it's totally fine to use a class instead of a bare function. Some algorithms also require a lot of context to be passed between such subfunctions, and it's sometimes quite handy to store such context as private fields of the class to avoid passing it from function to function everywhere. Another thing is dependency injection, if the algorithm requires several dependencies, it's sometimes better to inject them in the constructor once instead of passing them from function to function, or using global state. But I agree that for small functions it's an overkill to create a whole class. It also depends on the language you use and what's idiomatic in it and what is not.
- creatonez 4y agoSounds like a great use case for closures https://en.wikipedia.org/wiki/Closure_(computer_programming) https://en.wikipedia.org/wiki/Closure_(computer_programming)
- DeathArrow 4y agoHow? You use the closure to capture the variables, but how can you make the variables visible in another function?
- kgeist 4y agoYes, this kind of approach is equivalent to closures/currying, in the style of OOP. However, using closures for this may not look idiomatic in some languages.
- Twisol 4y ago> If computation is complex and requires multiple subfunctions that you don't want to leak to the global namespace In cases like this, I still use a top-level free function, but it just instantiates a (private) class and invokes those (private) subfunctions itself. There's usually no need to leak the implementation's strategy for managing complexity to callers. Of course, if your computation required interactivity with the caller, then it's not a one-and-done batch deal anyway, so free functions would not have been as appropriate in the first place.
- mojuba 4y agoI once, a long time ago, had a similar argument (somewhere online) with a bunch of Java coders. I asked them to create a library for solving quadratic equations. Asking to create a library itself was a trap of course. Everybody came up with some solver classes in Java, some stateful, some stateless. One guy presented the most monstrous solution with 100+ lines of code where you could individually assign the coefficients, individually modify them, trigger recalculation, get the number of roots, get the roots. That guy was very proud of what he did. Oh, I found bugs in his code. But that was considered normal. People make mistakes, right? I showed them a two-line solution with a single function (where you'd try hard to make a mistake) and nobody liked it. I was ridiculed right there, for not solving the problem "properly". It struck me how someone can overengineer a solution to the simplest problem and be proud of it. I thought at the time this industry is screwed if this is the norm. In fact probably some 90% of so called library code on GitHub is overenginnered crap. 90% is arbitrary but from my experience every time I look for some solution let's say for some GUI effect on iOS, find a library or a framework, take the source, analyze it, start simplifying it and I end up with a version that is orders of magnitude shorter and can be just copied into my project it's so trivial. You realize that it was so trivial that there was no need for a library in the first place. I liked the post about minimaism the other day here on HN [1]. It still amazes me how minimalism is not the norm, is not taught as the only way you should solve problems in software engineering. There's no "proper" way other than the most minimalist one, period. However, minimalism requires some extra effort to achieve, and that's the whole point of engineering. [1] The post was about minimalism in programming languages, but the author had another, more general post: https://pointersgonewild.com/2018/02/18/minimalism-in-programming/ https://pointersgonewild.com/2018/02/18/minimalism-in-progra... Edit: mandatory favorite essay: http://www.paulgraham.com/power.html http://www.paulgraham.com/power.html
- agumonkey 4y agoLack of constraints.. I'm sure having no memory, no cycles or not even computer access time made people aim at diamonds, not tar pits. One thing I'd add over the 'screwed industry', at least in the java,model era is that the more they added, the more "tools" they needed and it was seen as a quality. Basically quadratic complexity at the cultural level.. ps: as usual, https://duckduckgo.com/?q=stop+writing+classes+pycon&t=ffab&ia=web https://duckduckgo.com/?q=stop+writing+classes+pycon&t=ffab&...
- mihaigalos 4y agoI've had the pleasure of multiple trainings from Klaus Iglberger. Here's a talk he gave concerning free functions at CPPCON'17 [1]. It's high-level enough to be understood by non-C++ devs and I believe it can be applied to any OO language. [1] https://www.youtube.com/watch?v=zB_bhZCoX5M https://www.youtube.com/watch?v=zB_bhZCoX5M
- xthrowawayxx 4y agoIn Typescript I've started doing interface IHasSaveProject { saveProject(...): ... } class HasSaveProject implements IHasSaveProject { saveProject(...): {...} } class Controller { constructor(private readonly IHasSaveProject) {} post(...) { this .hasSaveProject .saveProject(...) } } And basically have a different class for every function In cases where it really makes sense to have multiple methods on a single class, I do this interface IProjectService extends IProjectService.IHasGetProject ,IProjectService.IHasGetProject {} namespace IProjectService { export interface IHasGetProject { getProject(...): ... } export interface IHasSaveProject { saveProject(...): ... } } class ProjectService implements IProjectService { getProject(...): {...} saveProject(...): {...} } class Controller { constructor(private readonly projectService: IProjectService) {} // or // constructor(private readonly projectService: // IProjectService.IHasGetProject // & IProjectService.IHasaveProject) {} // or // constructor( // private readonly hasSaveProject: IProjectSercice.IHasSaveProject // private readonly hasGetProject: IProjectSercice.IHasGetProject // ) {} ... } Seems to be working well for me. Lots of flexibility. Not sure how others like it though. Please forgive my formatting
- lionkor 4y agoI dont think this makes much sense - a lot of them would likely share functionality and you could group them, like ISavable and just give that all save functions
- xthrowawayxx 4y agoIn my experience I prefer grouping by domain over grouping by abstraction An ISaveable interface would need to be generic but "save" functions might take different numbers of arguments, and of different types. I've found the advantage of interfaces to be dependency injection, where i can inject a different implementation of an interface without breakijg anything (eg save to s3, to google cloud storage, to filesystem, to memory), or a test version of the function/class. Abstractions like ISaveable on the service interface just make the code more fragile in my experience in an attempt to save some extra but simple lines of code. Granted it may make sense on active record model classes though.
- cudgy 4y agoPerforming the calculation in the constructor may not go well if many objects are created, but not very many are computed. The example seems more of a functor than a full blown class. Also, classes along with static methods can be used to create a namespace to avoid polluting the global namespace, grouping similar operations together.
- lionkor 4y agoI may be missing something, but free functions + namespaces do namespacing better than classes with static methods, no?
- cudgy 4y agoAssuming there is no state shared between them, they are basically equivalent in function. Perhaps there is an advantage to namespaces regarding call overhead but that would be implementation dependent. Some languages do not provide namespaces but do provide classes, so static methods are all that is available.
- 0xcoffee 4y agoCan someone give an example of how (automatic) dependency injection works in functional programming? This to me is the only real benefit of OOP, that I can just add another service into the constructor and not have to update call sites of other signatures which would happen if I passed everything as parameter arguments. I work in Asp Net land, and most of my work is linking services together and not so much solving mathematical problems.
- withinboredom 4y agoYou pass a partially applied function. So, example of php: $f = fn($x) => f($x, …$deps); Then I can pass $f to whatever function with its dependencies satisfied. Edit: fixed example
- SebastianKra 4y agoI wonder: Is automatic DI even helpful? You save some lines of boilerplate but sacrifice control over the initialization-order and get a flat, messy, implicit dependency graph. The decision of what goes where must be made somehow. So you either do it through the type (eg. `@inject(MySuperSpecificServiceThatOnlyExistsOnceAnyways.self)`), or write provider functions. Isn't the second solution basically the same thing as doing it manually?
- kgeist 4y ago>I wonder: Is automatic DI even helpful? You save some lines of boilerplate but sacrifice control over the initialization-order and get a flat, messy, implicit dependency graph Initialization order doesn't matter if your services are stateless. At least in our codebase, all of them are stateless, as it greatly simplifies reasoning about concurrent code (both in-process and between servers). Yes, it's easy to end up with a very convoluted dependency graph under the hood, but I don't think it's a problem you really should care about. I mean, your code most likely already compiles to a very convoluted mess of machine code under the hood (with all the optimizations, ABI quirks etc.) and I doubt it matters to you much, as long as it does its job well and doesn't hinder your productivity. If you are talking about messy dependency graphs from the architectural standpoint (someone can easily add a dependency in the constructor without thinking about the consequences), we use deptrac for our PHP monolith which can validate your architecture is clean at build time [0] However, for our microservices written in Go, we decided to use manual DI to stimulate developers to prefer simpler design, otherwise our microservices could quickly turn into monoliths again. [0] https://github.com/qossmic/deptrac https://github.com/qossmic/deptrac
- elanning 4y agoThis reminds of a principle I use. Always use the weakest type for the problem at hand. I don’t use a struct/record where a simple dictionary or tuple would do. I don’t use a class when it could be a plain record. I don’t use an interface when it could simply be a lambda. When the time comes, I promote the types as necessary.
- kgeist 4y agoHow do you decide that the time has come?
- bitwize 4y agoObjects are a poor man's closures. And closures are a poor man's objects: http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/m...
- TekMol 4y agoThe most awesome way to bundle data and functions would be what I call "standalone modules". So in Python you could do: import greeter standalone greet1 import greeter standalone greet2 greet1.name = 'Joe' greet2.name = 'Sue' greet1.greet() greet2.greet() And greeter.py would look like this: name = 'nobody' def greet(): print ('Hello '+name) Output: Hello Joe Hello Sue So just like it is now, but with a "standalone" keyword that turns an imported module into an "instance".
- CGamesPlay 4y agoIs this a real thing? What if greet imports a module, is that one shared or instanced twice?
- TekMol 4y agoThat would be shared unless it imports it with the "standalone" keyword.
- messe 4y agoYou can sorta do this with Zig, but the file you're importing has to be written with that in mind. "Modules" in Zig are just structs, so you can create instances of them (I'm handwaving a little bit here, but that's the gist of it). // Greeter.zig const std = @import("std"); const Self = @This(); name: []const u8, pub fn greet(self: *Self) void { std.debug.print("hello {s}!\n", .{self.name}); } The above file is equivalent to the struct: pub const Greeter = struct { name: []const u8, pub fn greeter(self: *Greeter) void { std.debug.print("hello {s}!\n", .{self.name}); } }; --- // main.zig const Greeter = @import("Greeter.zig"); const greeter1 = Greeter{ .name = "Joe", }; const greeter2 = Greeter{ .name = "Sue", }; pub fn main() !void { greeter1.greet(); greeter2.greet(); } A lot of data structures in the stdlib are in written in this style. EDIT for comparison, a non-standalone version would be written like this: // greeter-nonstandalone.zig: const std = @import("std"); pub var name: []const u8 = "nobody"; pub fn greet() void { std.debug.print("hello {s}!\n", .{name}); } // main-nonstandalone.zig const greeter = @import("greeter-nonstandalone.zig"); pub fn main() !void { greeter.name = "Joe"; greeter.greet(); }
- gorgoiler 4y agoIf you find yourself regularly writing functions that return more than one closure, you should consider using a class instead. That’s an ironic joke of course — who does this? — but it’s a helpful way to then think about the inverse. If you have a class with one function then it could probably just be a closure. Even better, if it closes one thing, one thing only, and uses that thing while never modifying it then your class is really just a group of functions all of which share the same first argument. Quite a lot of “Database” abstractions are like this, with methods built around a single internal reference to a database connection. The downside of returning a function instead of an object is your caller has to give it a name: lol = build_handle() lol() …versus the class-as-sensible-name-enforcement version: lol = Handle() lol.sensible_name() As always, quite a lot of these problems are stylistic (or cultural, if you work in a team) rather than technical. Coding is, amongst other things, an exercise in design.
- samatman 4y agoI do the pair of closures fairly frequently, it's a good match for a producer/consumer pair. For something like a saturated counter an object with a getter and setter is more natural.
- kvonhorn 4y agoAnother way I've heard it put is that any class ending in -er or -or is an antipattern.
- eurasiantiger 4y agoUnlikely to be true, even if some such classes are indeed antipatterns. Which of these classes are antipatterns? Observer FileManager HTTPServer CircuitBreaker ObjectBuilder ServiceProvider SchemaValidator StringTranslator Vector
- jpindar 4y agoCustomer Player User Printer
- kgeist 4y agoIt's quite interesting that some say it's an anti-pattern while in Go it's the standard way to name interfaces.
- ehnto 4y agoI like to use helper classes to organize things, it's just syntactic sugar for me, and a little easier to manage long term. I think of them like little toolkits. Standard libraries in many languages are essentially delivered as helper classes, I don't think it's a nefarious anti-pattern. The difference between `setup_environment_variables` and `Environment.setupVariables` is just ease of use, auto-complete and organization. Not some scary over-engineering creeping in. Tracking down related helper functions is far easier like this too, if I want to see what environment related helper functions I have, I just type Environment. and it shows me everything available to autocomplete, or I click/press into Environment to go see all the related code. Anyone who's ever opened the "functions" file of a long running codebase to find 50+ free-form functions knows it's not a great way to be organized.
- eptcyka 4y agoUse modules and namespaces.
- selfmodruntime 4y agoThey are for this usecase equivalent to a static class
- robonerd 4y agoThen use them.
- marcellus23 4y agoNot every language has those.
- eptcyka 4y agoThe context here is C++. Yes, if you are bound to write Java/C/php, do whatever.
- 4y ago
- billpg 4y agoBesides the point, but I would hope that for the version found mid-way along the article, (compute in constructor, return value via the count function) the compiler would first optimize away the container class because the container only has one member, then optimize the call to count away. If having a class like this makes sense, do it and leave optimizing to the compiler.
- deleted 4y ago[deleted]
- knorker 4y agoThis is one of the reasons I don't like Java. I feel like the language gets annoyed with me when I just want a pure function, and goes "fine, i guess i support that. Still needs to be in a class though... Because... I like classes"
- Viliam1234 4y agovar f = x -> x + 2; Yes, this is a syntactic sugar for a class. (And the class is a syntactic sugar for machine code.)
- knorker 4y agoStill needs to be in a class, too,
- benibela 4y agoThat makes sense when the function has two arguments With more arguments the function call is hard to read countDominoTilings(4, 7), what is 4, what is 7? Some languages have named parameters. Then you can write countDominoTilings(width = 4, height = 7), which is very readable. But without named parameters, DominoTilingCounter tc; tc.width = 4; tc.height = 7; tc.count() would be more readable
- Zababa 4y agoEvery IDE I've used would show you the name of parameters when you hover the function. These days there are even inlay hints that will transform countDominoTiling(4, 7) to countDominoTiling(width: 4, height: 7). Another option would also be to just pass a struct: struct dimensions dim = { .width = 4, .height = 7}; countDominoTiling(dim); My C++ is rusty so this might not compile but you get the idea. There isn't a big difference with just one simple thing, but when you're having 10 of these things, with some classes having classes in themselves, it quickly becomes a mess.
- cpurdy 4y agoObligatory +1 for trying to argue against OOP without mentioning "cats" or "dogs".
- 8note 4y ago> it becomes the caller’s problem, for better or worse This is the real crux of the article and the tradeoff you are weighing; it's too bad that the consequences of this change aren't explored
- DeathArrow 4y agoI find this speach very relevant: https://m.youtube.com/watch?v=ZSRHeXYDLko https://m.youtube.com/watch?v=ZSRHeXYDLko
- DeathArrow 4y agoThe solution is not "enterprisey" and using "clean code" until it uses at least 20 design patterns. Uncle Bob would not like it!
- iex_xei 4y agoIt seems like an example to what I call "Object-Oriented Brain Damage." https://emresahin.net/post/object-oriented-brain-damage/ https://emresahin.net/post/object-oriented-brain-damage/