4 ms·
At the last two places I worked, I gradually led my teams from traditional OOP Ruby or Python behaviors to at least partially functional (if you'll pardon the p
by z9znz 4y ago
At the last two places I worked, I gradually led my teams from traditional OOP Ruby or Python behaviors to at least partially functional (if you'll pardon the pun).
The immediate value I was able to demonstrate was in testing. Three basic (not too scary) principles can get you a long way:
1. push mutations and side effects as far toward the edges as possible (rather than embedded in every method/procedure)
2. strive for single responsibility functions
3. prefer simple built-in data structures (primarily hashes) over custom objects... at least as much as possible in the inner layers of the system
If these steps are taken, then tests become so much simpler. Most mocking and stubbing needs evaporate, and core logic can be well tested without having to touch the database, the api server, etc. Many of the factories and fixtures go away or at least become much simpler. You get to construct the minimal data structure necessary to feed to a test without caring about all the stuff that you normally would have to populate to satisfy your ORM rules (which should be tested in their own specific tests).
Once devs see this, they often warm up quickly to functional programming. Conversely, the quickest way to get an OOPist to double down and reject any FP is to build complex chains of collection operations which build and pass anonymous functions everywhere. Those things can be done where appropriate, but they don't provide as much early bang for the buck... and they likely prevent FP from getting a foot in the door to that team.
The only real downside is that naming things is hard, and good single responsibility practices result in a lot more functions that need good names.
- blep_ 4y ago> prefer simple built-in data structures (primarily hashes) over custom objects I've found this is actually one of my biggest problems with functional code as currently written: people seem afraid to just declare a struct/record, in lots of cases where it's obviously the right thing. If everything is a hash, you've just made all arguments optional and now you've invented a bad type system inside your good type system. If everything is a tuple (more common in my experience, from reading Haskell), now you know (int, int) is actually a pair of ints, but you've thrown away variable names and nominal typing: is it a 2D vector, a pair of indices into an array, or something else entirely? Defining custom types is the elegant solution to this: you have `struct range { start: int, end: int }` and now your functions can take a range and everything is great.
- z9znz 4y agoIt's not about being afraid to define a struct/record (rigid type). It's about being afraid of being stuck with something that doesn't meet your needs later or elsewhere, if only by a little bit. That Range type is great until you have cases where you want variations on a Range, such as a MeasuredRange (same start and end, but with a new field called step_cost). Original functions which take a Range can't cope with this new completely different struct called MeasuredRange. So now we have to change them all to accept Range or MeasuredRange, or we need some kind of type heirarchy to relate them in appropriate ways. The alternative is to accept the runtime risk of receiving a thing which doesn't have the start and end that you needed. Or of course if it were a very important piece of logic where failure was not an option, you just make your do_rangy_thing() require explicit start and end parameters. Then it's up to the caller to call do_rangy_thing(my_range[:start], my_range[:end]). Ultimately that just moves your failure risk up one level, though. Likewise, you can use hash destructuring in Clojure like (do_rangy_thing [{:keys [start end]}] ... or pattern matching in Elixir like def do_rangy_thing(%{start: s, end: e}) ..., both of which will blow up at runtime if the hash passed in doesn't have the required keys. Many people accept the runtime risk because it greatly simplifies code at the cost of runtime safety. The worst thing, in my view, is when languages try to bolt on some kind of type strictness later and end up solving the Range vs MeasuredRange problem by listing all possible accepted types or just throwing up their hands and saying Any. I don't know Haskell, but from what I've heard about it I find it surprising that (int, int) is as common as you say. I thought they were very much about detailed specific types, with the theory "if it compiles, it works". My guess is that generic tuples are just an indication that some of the participants didn't want to deal with the complex type system and have a thousand different narrow case types defined.
- blep_ 4y agore Haskell: I just looked at a few arbitrary files in arbitrary Haskell projects on Github, and I retract that part. I was either thinking of another language, or had seen a lot of bad Haskell code before that I cannot find now. ---- Isn't this just the nullability thing again that most people have decided was a bad idea, but with a different name? Instead of two ints, now your function takes two maybe-ints and crashes at runtime if they're not there. I do think the thing you're asking for (interfaces but for fields instead of functions) is a missing feature in most languages. I understand why it's there, but it's still annoying. The most well-known language I can think of that explicitly supports it in an otherwise static type system is TypeScript, where a type can be "object containing non-nullable ints called start and end, but with no constraints on other fields that may be there"[0]. It's technically a hash map at runtime, but it's a compile-time type-checked hash map. In other languages, you could bodge it with a bunch of Java-esque getX/setX functions. I don't blame people for not doing so. [0] https://www.typescriptlang.org/docs/handbook/2/objects.html https://www.typescriptlang.org/docs/handbook/2/objects.html