5 ms·
I don’t disagree with the overall message or choice of examples behind this post, but one paragraph stuck out to me: > Martin says that it should be possible t
by nicklecompte 5y ago
I don’t disagree with the overall message or choice of examples behind this post, but one paragraph stuck out to me:
> Martin says that it should be possible to read a single source file from top to bottom as narrative, with the level of abstraction in each function descending as we read on, each function calling out to others further down. This is far from universally relevant. Many source files, I would even say most source files, cannot be neatly hierarchised in this way.
The relevance is a fair criticism but most programs in most languages can in fact be hierarchized this way, with the small number of mutually interdependent code safely separated. Many functional languages actually enforce this.
As an F# developer it can be very painful to read C# programs even though I often find C# files very elegant and readable: it just seems like a book, presented out of order, and without page numbers. Whereas an .fsproj file provides a robust reading order.
- dmurray 5y ago> "with the level of abstraction in each function descending as we read on, each function calling out to others further down." ... > Many functional languages actually enforce this. Don't they enforce the opposite? In ML languages (I don't know F# but I thought it was an ML dialect), you can generally only call functions that were defined previously. Of course, having a clear hierarchy is nice whether it goes from most to least abstract, or the other way around, but I think Martin is recommending the opposite from what you are used to.
- nicklecompte 5y agoHmm, perhaps I am misreading this? Your understanding of ML languages is correct. I have always found “Uncle Bob” condescending and obnoxious so I can’t speak to the actual source material. I am putting more emphasis on the “reading top-to-bottom” aspect and less on the level of abstraction itself (might be why I’m misreading it). My understanding was that Bob sez a function shouldn’t call any “helper” functions until the helpers have been defined - if it did, you wouldn’t be able to “read” it. But with your comment, maybe he meant that you should define your lower-level functions as prototypes, implement the higher-level functions completely, then fill in the details for the lower functions at the bottom. Which is situationally useful but yeah, overkill as a hard rule. In ML and F# you can certainly call interfaces before providing an implementation, as long as you define the interface first. Whereas in C# you can define the interface last and call it all you want beforehand. This is what I find confusing, to the point of being bad practice in most cases. So even if I misread specifically what (the post said) Bob was saying, I think the overall idea is what Bob had in mind.
- Izkata 5y ago> I am putting more emphasis on the “reading top-to-bottom” aspect and less on the level of abstraction itself (might be why I’m misreading it). My understanding was that Bob sez a function shouldn’t call any “helper” functions until the helpers have been defined - if it did, you wouldn’t be able to “read” it. But with your comment, maybe he meant that you should define your lower-level functions as prototypes, implement the higher-level functions completely, then fill in the details for the lower functions at the bottom. Neither really, he's saying the higher-level abstractions go at the top of the file, meaning the helpers go at the bottom and get used before they're defined. No mention of prototypes that I remember. Personally, I've never liked that advice either - I always put the helpers at the top to build up the grammar, then the work is actually done at the bottom.
- tsimionescu 5y agoIt seems your idea is precisely the opposite of Robert Martin's. What he is advocating for is starting out the file with the high-level abstractions first, without any of the messy details. So, at the top level you'd have a function that says `DoTheThing() { doFirstPart(); doSecondPart();}`,then reading along you'd find out what doFirstPart() and doSecondPart() mean (note that I've used imperative style names, but that was a random choice on my part). Personally I prefer this style, even though I dislike many other ideas in Clean Code. The requirement to define some function name before you can call it is specific to a few languages, typically older ones. I don't think it's very relevant in this context, and there are usually ways around it (such as putting all declarations in header files in C and C++, so that the actual source file technically begins with the declarations from the compiler's perspective, but doesn't actually from a programmer perspective (it just begins with #include "header").
- valenterry 5y agoML languages are not functional ("functional" in the original sense of the word - pure functional). They are impure and thus don't enforce it.
- nicklecompte 5y agoAs someone who almost exclusively uses functional languages: don’t do this. This kind of pedantic gatekeeping is not only obnoxious... it’s totally inaccurate! Which makes it 100x as obnoxious. “Functional” means “functions are first-class citizens in the language” and typically mean a lot of core language features designed around easily creasing android manipulating functions as ordinary objects (so C#, C++, and Python don’t really count, even with more recent bells-and-whistles). Typically there is a strong emphasis on recursive definitions. But trying to pretend “functional programming languages” are anything particularly specific is just a recipe for dumb arguments. And of course, outside of the entry point itself, it is quite possible (even desirable) to write side-effect-free idiomatic ISO C. The “original” functional language was LISP, which is impure as C and not even statically-typed - and for a long time certain Lisp/Scheme folks would gatekeep about how a language wasn’t a Real Functional Language if it wasn’t homoiconic and didn’t have fancy macros. (And what’s with those weird ivory tower Miranda programmers?) In fact, I think the “gate has swung,” so to speak, to the point that people downplay the certain advantages of Lisps over ML/Haskells/etc.
- sedachv 5y ago> for a long time certain Lisp/Scheme folks would gatekeep about how a language wasn’t a Real Functional Language if it wasn’t homoiconic and didn’t have fancy macros This never happened. You are confusing functional with the 2000s "X is a good enough Lisp" controversy, which had nothing to do with functional programming. > “Functional” means “functions are first-class citizens in the language” No, the word function has a clearly defined meaning. I don't know where you get your strange ideas from - you need to look at original sources. The word "functional" did not become part of the jargon until the 1990s. Even into the 1980s most people referred to this paradigm as "applicative" (as in procedure application), which is a lot more appropriate. The big problem with the Lisp community is that early on, when everyone used the words "procedures" or "subroutines," they decided to start calling them "functions," even though they could have side effects. This is probably the reason why people started trying to appropriate "functional" as an adjective from the ML and Haskell communities into their own languages. A lot of people assume that if you can write a for loop as a map, it makes it "functional." What you end up with is a bunch of inappropriate cargo-culting by people who do not understand the basics of functional programming.
- marcosdumay 5y ago> In ML languages, you can generally only call functions that were defined previously. Hum... At least not in Haskell. Starting with the mostly dependent code makes a large difference in readability. It's much better to open your file and see what are the overall functions. The alternative is browsing to find it, even when it's on the bottom. Since you read functions from the top to the bottom, locating the bottom of the function isn't much of a help to read it. 1 - The dependency order does not imply on any ordering in abstraction. Both can change in opposite directions just as well as on the same.
- throw1234651234 5y agoWe follow this approach closely - the problem is that people confuse helper services for first-order services and call them directly leading to confusion. I don't know how to avoid this without moving the "main" service to a separate project and having `internal` helper services. DI for class libraries in .NET Core is also hacky if you don't want to import every single service explicitly.
- nicklecompte 5y agoIs there a reason why private/internal qualifiers aren’t sufficient? Possibly within the same namespace / partial class if you want to break it up? As I type this out, I suppose “people don’t use access modifiers when they should” is a defensible reason.... I also think the InternalsVisibleTo attribute should be used more widely for testing.
- throw1234651234 5y agoYou can't make a helper private if you move it out to a separate service. The risk of making it public is if people think the helper should be used directly, and not through the parent service. Internal REQUIRES that the service be moved out to a class library project, which seems like overkill in a lot of cases.