Hacker News new | ask | show | jobs
by coldtea 2451 days ago
How is #4 even a problem to begin with?

If anything, it's the inverse: "isolation of side effects" means the architecture is more flexible and lego-like -- as opposed to a spaghetti mess.

And if you need to change interfaces, you are in for one of the smoothest rides, as the compiler will guide you every step of the way to implementing the change wherever it's needed.

2 comments

> How is #4 even a problem to begin with?

Lets say you are making an RPG where you can equip items. You write a lot of pure functions and behavior for stat adjustments etc. Then the designer comes to you and say that items are no longer constants, every item needs a durability which goes down on use. In Java this would be a few line change, just add a new field in items and add a line to subtract it in relevant places. In Haskell you would need to bubble up the new item change to the global state and properly set it in each part of the code where you use items. If you aren't careful you can easily miss to properly bubble it up somewhere and the item doesn't get updated, or you might have the same item referenced in several places and now you'd have to refactor it to have a global item pool to since tracking down all places that should be updated is not feasible.

> In Java this would be a few line change, just add a new field in items and add a line to subtract it in relevant places.

And then find out that since items used to be constants all instances of a given type of item are actually the same item object, so modifying one unexpectedly affects them all. Or worse, sometimes items are copied and sometimes they're shared by reference, so whether they're affected depends on each item's individual history.

One of the benefits of Haskell is that it forces you to think through these aspects of the API in advance. A mutable object with its own distinct identity and state is a very different concept from an immutable constant and the design should reflect this. Changing one into the other is not an action to be taken lightly.

Not a Haskell expert, but I think I know what you mean -you are passing items by value to many functions and complain it's not modified in others, when you call them, or you need to compose flow so updates are properly "bubbled".

But with assumption that code was working before and you had for example function: use :: Item -> Item, and you change duration in this function, what else do you need to change to "bubble" new state? I don't get this.

BTW What's the problem with global store and passing only IDs ? I feel like this is probably valid approach and anyway ECS are implemented in similar manner AFAIK - https://en.m.wikipedia.org/wiki/Entity_component_system

As I understand it, a function use :: Item -> Item in Haskell guarantees that it can't change any property of any item;even more so if the function were use :: Item -> Effect, as could be done on the initial assumption that Items can't change.
In general you can't mutate records. Nothing prevents you from making a new one that's slightly different though. It just won't change the already existing one.

In practice this is not actually a problem. It just takes a little getting used to to get yourself out of the 'memory-address as identity' mindset that procedural languages have.

Can you provide an example (even in pseudocode, syntax doesn't matter here)?

I don't think that's how you'd handle a state change in idiomatic Haskell. What you propose sounds very error-prone.

> And if you need to change interfaces, you are in for one of the smoothest rides, as the compiler will guide you every step of the way to implementing the change wherever it's needed.

This is the same in any statically typed language. Actually the quality of Haskell/GHC's error messages are limited by constraint-based type inference and languages that have flow-based type inference do a better job IMO.

> This is the same in any statically typed language

Surely not in any statically typed language. Languages like Java cannot encode the same useful information (or rather, you cannot force them to) as languages like Haskell. Specifically, you cannot make them enforce lack/presence of IO in their types. Most mainstream statically typed languages cannot do that, in fact.

But it's considered good practice in Haskell to keep IO out of the main logic of the program and basically use it as little as possible. Haskell is certainly not an IO-focused language like Go and Rust. The IO monad is almost more of a deterrent than a tool.
That's only partially true. Of course a Haskell program needs to do IO to be useful. The IO Monad is also not a deterrent, where did you get that idea? Haskell is very much IO focused, in fact it's been jokingly called "the world's best imperative language"!

Even then, in Haskell you can say "this doesn't do IO" which you cannot in most languages!

It's also just an example of the expressiveness of the type system, which "most statically typed" cannot enforce or sometimes even express.