Live data from Hacker News

Clean Coder: The Dark Path (2017)

blog.cleancoder.com

21–30 of 71 posts

Re: Clean Coder: The Dark Path (2017)

#21
post #9

> Every time there’s a new kind of bug, we add a language feature to prevent that kind of bug. That's why learning more academic, 'non-practical' aspects of computer science is sometimes beneficial. Otherwise very few will naturally develop the abstract thinking that allows them to see uncaught exception and null pointer are exactly the same 'kind of bug.' Anyway the author got it completely upside down. The stricter…

I suppose it's somewhat accurate to claim that Haskell and Ocaml historically preceded Java (or even Objective-C). But Java wasn't inspired by those academic languages, but C: a then widely used real-world language with only partial static types.

(Not saying Java's attempt to remedy C's problems wasn't half-assed — it was.) The trend to plug holes is primarily motivated by empirical evidence of bug classes. Not by elegance of academic research.

As Bjarne Stroustrup famously quipped:

> “There are only two kinds of languages: the ones people complain about and the ones nobody uses.”

Swift, Kotlin, Rust, C++ are attempt to become languages that everyone complains about, not Haskell or Ocaml.

Re: Clean Coder: The Dark Path (2017)

#22

What I read between the lines: “I have such a fragile ego that I feel offended when a tool points out a mistake I made. I feel intellectually rewarded by doing the same busywork over and over again. I don’t want to change the way I do my work at all. I feel left behind when people other than me have great ideas about language design.”

What I read between your lines: "I don't want to think about the code at all. It should only compile if it has no bugs. I don't like rapid prototyping. I feel stupid when people other than me feel they can program effectively with fewer safeguards."

Re: Clean Coder: The Dark Path (2017)

#23
> For example, in Swift, if you declare a function to throw an exception, then by God every call to that function, all the way up the stack, must be adorned with a do-try block, or a try!, or a try?.

Funnily enough, Uncle Bob himself evangelised and popularised the solution to this. Dependency Inversion. (Not to be confused with dependency injection or IOC containers or Spring or Guice!) Your call chains must flow from concrete to abstract. Concrete is: machinery, IO, DBs, other organisation's code. Abstract is what your product owners can talk about: users, taxes, business logic.

When you get DI wrong, you end up with long, stupid call-chains where each developer tries to be helpful and 'abstract' the underlying machinery:

  UserController -> UserService -> UserRepository -> PostgresConnectionPoolFactory -> PostgresConnectionPool -> PostgresConnection
(Don't forget to double each of those up with file-names prefixed with I - for 'testing'* /s )

Now when you simply want to call userService.isUserSubscriptionActive(user), of course anything below it can throw upward. Your business logic to check a user subscription now contains rules on what to do if a pooled connection is feeling a little flakey today. It's at this point that Uncle Bob 2017 says "I'm the developer, just let me ignore this error case".

What would Uncle Bob 2014 have said?

Pull the concrete/IO/dependency stuff up and out, and make it call the business logic:

  UserController:
      
      user?  PostgresConnectionPoolFactory -> PostgresConnectionPool -> PostgresConnection)
      // Can't find a user for whatever reason? return 404, or whatever your coding style dictates

      result 
The first call should be highly-decorated with !? or whatever variant of checked-exception you're using. You should absolutely anticipate that a DB call or REST call can fail. It shouldn't be particularly much extra code, especially if you've generalised the code to 'get thing from the database', rather than writing it out anew for each new concern.

The second call should not permit failure. You are running pure business logic on a business entity. Trivially covered by unit tests. If isUserSubscriptionActive does 'go wrong', fix the damn code, rather than decorating your coding mistake as a checked Exception. And if it really can't be fixed, you're in 'let it crash' territory anyway.

* I took a jab at testing, and now at least one of you's thinking: "Well how do I test UserService.isUserSubscriptionActive if I don't make an IUserRepository so I can mock it?" Look at the code above: UserService is passed a User directly - no dependency on UserRepository means no need for an IUserRepository.

Re: Clean Coder: The Dark Path (2017)

#24

While I consider Uncle Bob a bad programmer, there is some merit to this article. This paragraph was particularly prescient: >But before you run out of fingers and toes, you have created languages that contain dozens of keywords, hundreds of constraints, a tortuous syntax, and a reference manual that reads like a law book. Indeed, to become an expert in these languages, you must become a language lawyer (a term that…

He has a point in that paragraph, but as a C++ developer I couldn't help being agitated by his passive aggressive comment:

> [...] (a term that was invented during the C++ era.)

...like it's some sort of relic, or was in 2017

Re: Clean Coder: The Dark Path (2017)

#25

Earlier quoted context omitted.

There are many, but one particular example is the type syatem.

Explain how

Not OP, and not sure about OCaml and Haskell, but one example where Java's type system is unhelpful/incorrect is mutable subtyping.

E.g. Java assumes Cat[] is a subtype of Animal[]. But this only holds when reading from the array. The correct behavior would be:

- `readonly Cat[]` is a subtype of `Animal[]`

- `writeonly Cat[]` is a supertype of `Animal[]`

- `readwrite Cat[]` has no relationship with `Animal[]`

But Java doesn't track whether a reference is readable or writable. The runtime makes every reference read-write, but the type checker assumes every reference is read-only.

This results in both

- incorrect programs passing the type checker, e.g. when you try to write an Animal to an Animal[] (which, unbeknown to you, is actually a Cat[]), you get a runtime exception

- correct programs not passing the type checker, e.g. passing a Animal[] into an writeCatIntoArray(Cat[] output) function is a type error, even though it would be safe.

(Although all that is assuming you're actually following the Liskov substitution principle, or in other words, writing your custom subtypes to follow the subtyping laws that the type checker assumes. You could always override a method to throw UnsupportedOperationException, in which case the type checker is thrown out of the window.)

Interestingly, AFAIK Typescript makes these types both subtypes and supertypes at the same time, in the interest of not rejecting any correct programs. But that also allows even more incorrect programs.

Re: Clean Coder: The Dark Path (2017)

#26

While I consider Uncle Bob a bad programmer, there is some merit to this article. This paragraph was particularly prescient: >But before you run out of fingers and toes, you have created languages that contain dozens of keywords, hundreds of constraints, a tortuous syntax, and a reference manual that reads like a law book. Indeed, to become an expert in these languages, you must become a language lawyer (a term that…

Yeah once you get into Monofunctor or 'PorcelainPile ' lawyering you know you went too far

TIL about Monofunctor.

Pretty nifty. It's for cases where your type is a container of something (as opposed to anything).

I.E. you can .map (or .Select if you're .NET-inclined) over the Chars in a String, but not some other type, because String can't hold another type.

https://hackage.haskell.org/package/mono-traversable-1.0.21....

Re: Clean Coder: The Dark Path (2017)

#28

I disagree with the article, but also some of these examples are complete straw-men. In Kotlin you have nullable types, and the type checker will complain if you use it as a non-nullable type. But you can always just append !! after your expression and get exactly the same behavior as in Java and get a null pointer exception, you don't have to handle it gracefully as the author is suggesting. Tests checking that you…

Your last sentence makes a very good point. And Uncle Bob's tool is to have loads of tests.

I suppose it's all just a balance: simplicity versus expressiveness, foot guns versus inflexibility, conciseness versus ceremony, dev velocity versus performance in production.

I'm okay with shifting some of the burden of common errors from developer into the language if that improves reliability or maintainability. But Martin has a point in that no guard rails can ever prevent all bugs and it can be annoying if languages force lots of new ceremony that seems meaningless.

Re: Clean Coder: The Dark Path (2017)

#29
post #25

Earlier quoted context omitted.

Explain how

Not OP, and not sure about OCaml and Haskell, but one example where Java's type system is unhelpful/incorrect is mutable subtyping. E.g. Java assumes Cat[] is a subtype of Animal[]. But this only holds when reading from the array. The correct behavior would be: - `readonly Cat[]` is a subtype of `Animal[]` - `writeonly Cat[]` is a supertype of `Animal[]` - `readwrite Cat[]` has no relationship with `Animal[]` But Jav…

Did you mean arrays instead of lists? Arrays behave as you describe (with ArrayStoreException when you write a wrong value to an array). List is invariant WRT its type parameter.

Re: Clean Coder: The Dark Path (2017)

#30

While I consider Uncle Bob a bad programmer, there is some merit to this article. This paragraph was particularly prescient: >But before you run out of fingers and toes, you have created languages that contain dozens of keywords, hundreds of constraints, a tortuous syntax, and a reference manual that reads like a law book. Indeed, to become an expert in these languages, you must become a language lawyer (a term that…

I agree, but i think his point applies more to haskell than, say, kotlin. There is a balance between type strictness and productivity and if you go too far in one direction you get horribly buggy code and if you go too far in the other direction you have a language that is grindingly slow to develop in.

Another thing I dont think a lot of people appreciate either is that types have sharp diminishing returns catching the kind of bugs tests are good at catching and vice versa.

Post reply on HN