Hacker Newsnew | past | comments | ask | show | jobs | submit | mrkeen's commentslogin

What concrete interests would you like such a union to protect?

Should a strike happen if devs are told to use Claude, or should a strike happen if devs aren't given access to Claude?


Here are some easy ones:

- End H1B abuse and demand limits on offshoring

- Compensation transparency

- “Value capture,” as called out in the article. If new tools make engineers 10x more productive, that should be reflected in compensation

- End employment law workarounds like “unlimited PTO,” where your PTO is still limited in practice, but it’s not a defined or accruing benefit

- Protection against dilution of equity for employees

- A seat at the table for workers, not just managers, in the event of layoffs

- Professional ethics and whistleblower protections. Legally-protected strikes if workers decide to refuse on pursuing an ethically or legally dubious product or feature.

I could go on. There are a lot of abuses we put up with because of relatively high salaries, and it is now abundantly clear that the billionaire capital-owning class is dead set on devaluing the work we do to “reduce labor costs.” We can decide not to go along with that.


Suppose you're receiving bytes representing a User at the edge of your system. If you put json bytes into your parser and get back a User, then put your User through validation, that means you know there are both 'valid' Users and 'invalid' Users.

Instead, there should simply be no way to construct an invalid User. But this article pushes a little harder than that:

Does your business logic require a User to have exactly one last name, and one-or-more first names? Some people might go as far as having a private-constructor + static-factory-method create(..), which does the validation, e.g.

  class User {
    private List<String> names;
    private User(List<String> names) {..}
    public static User create(List<String> names) throws ValidationException {
       // Check for name rules here
    }
  }
Even though the create(..) method above validates the name rules, you're still left holding a plain old List-of-Strings deeper in the program when it comes time to use them. The name rules were validated and then thrown away! Now do you check them when you go to use them? Maybe?

If you encode your rules into your data-structure, it might look more like:

  class User {
      String lastName;
      NeList<String> firstNames;
      private User(List<String> names) throws ValidationException {..}
  }
If I were doing this for real, I'd probably have some Name rules too (as opposed to a raw String). E.g. only some non-empty collection of utf8 characters which were successfully case-folded or something.

Is this overkill? Do I wind up with too much code by being so pedantic? Well no! If I'm building valid types out of valid types, perhaps the overall validation logic just shrinks. The above class could be demoted to some kind of struct/record, e.g.

  record User(Name lastName, NeList<Name> firstNames);
Before I was validating Names inside User, but now I can validate Names inside Name, which seems like a win:

  class Name {
      private String value;
      private Name (String name) throws ValidationException {..}
  }

If you spend your life talking about bool having two values, and then need to act as if it has three or 256 values or whatever, that's where the weirdness lives.

In C, true doesn't necessarily equal true.

In Java (myBool != TRUE) does not imply that (myBool == FALSE).

Maybe you could do with some weirdness!

In Haskell: Bool has two members: True & False. (If it's True, it's True. If it's not True, it's False). Unit has one members: () Void has zero members.

To be fair I'm not sure why Void was raised as an example in the article, and I've never used it. I didn't turn up any useful-looking implementations on hoogle[1] either.

[1] https://hoogle.haskell.org/?hoogle=a+-%3E+Void&scope=set%3As...


What were you expecting to find? A function which returns an empty type will always diverge - ie there is no return of control, because that return would have a value that we've said never exists. In a systems language like Rust there are functions like this for example std::process::exit is a function which... well, hopefully it's obvious why that doesn't return. You could imagine that likewise if one day the Linux kernel's reboot routine was Rust, that too would never return.

> What were you expecting to find?

> functions like this for example std::process::exit


I'm not astonished this isn't provided that way in a language like Haskell but I suppose I would also not be surprised if it were provided.

My Claude instance informs me of this. There's a banner up the top telling me to invoke "/extra-usage".

When I do so, it tells me it's an unknown skill, and keeps going with the usual "let me know if I can help you with anything else" spiel.


I lean on types heavily as my vehicle for change.

My UserService doesn't know that it's talking to a UserDB (ironically I learned this from Uncle Bob).

All UserService knows is it has a dependency which, if passed a UserId, will return a User (+/- whatever the failure mode is .. Future<User>? Promise<User>? ReaderT m User?)

When I change my mind about what UserService requires or what UserDB provides (which I frequently do), I immediately look at all the red underlines that my compiler & static types tell me about.


I was recently switched from Java to C# at work.

Initially I was impressed by the null detection. Then I found out about defaults. Way worse than null.

C and Go can demand a bit of ceremony with manual error checks. Things get bad if you forget to do the checks.

Java and Checked exceptions forced error-checking, which is a little verbose, and most of the time you can't 'handle' (for any meaning other than log) them, so you just rethrow.

C# went with unchecked exceptions. But with default values, there's no need to throw! Avoid Java's messy NPE encounters by just writing the wrong value to the database.


Default value is only relevant for structs. You can setup an analyzer rule to ban this for structs which have, say, an empty constructor. It’s a similar problem to Go and C++ except it’s worse in the latter two.

I can't make a Mappable interface, and have my classes implement map(f). Because map(f) will necessarily return Mappable, not my class itself. So no method chaining for me.

Also null. Yeah I know it's contentious. People don't want to let go of it. Since learning to hate null, I've also lost any nuance in my ability to explain why it's bad. Because I know longer see it as 'subtly-bad' or 'might lead to bugs'. It's just plain, on-the-surface-wrong. One might as well have named it 'wrong' rather than 'null'.

'Null' is the thing which it isn't. I can write business logic that says every Person has a Name. Once you admit null into the mix, I can no longer make that simplest of statements. My autocomplete now lies to me, because person may or may not implement the method .name().

"But how will I half-arse instantiate a Person? I don't have a Name, yet I want to tell the computer I have a Person?" It makes me happy that you can't.

"I wrote a function that promises to return a Person. I was unable to return a Person. How can I tell the computer I'm returning a Person even though I'm not?" Glad that you can't.


Avoiding null is one of those things the FA complains about — you'll make your language three times as complicated to avoid it, and that might not be a good trade-off.

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....


> 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? <- (UserRepository -> PostgresConnectionPoolFactory -> PostgresConnectionPool -> PostgresConnection)
      // Can't find a user for whatever reason? return 404, or whatever your coding style dictates

      result <- UserService.isUserSubscriptionActive(user)

      return 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.


  Adam Jacob
  It’s not slop. It’s not forgetting first principles. It’s a shift in how the craft work, and it’s already happened. 
This post just doubled down without presenting any kind of argument.

  Bruce Perens
  Do not underestimate the degree to which mostly-competent programmers are unaware of what goes on inside the compiler and the hardware.
Now take the median dev, compress his lack of knowledge into a lossy model, and rent that out as everyone's new source of truth.

"I don't need to know about hardware, I'm writing software."

"I don't need to know about software engineering, I'm writing code."

"I don't need to know how to design tests, ____ vibe-coded it for me."


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: