Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I still don't get how people can claim that functional programming is inituative when mutating a nested struct requires a separate package and metaprogramming hacks.


| mutating a nested struct requires a separate package and metaprogramming hacks.

But it doesn't. And you would have known this if only you had read the fucking article.

    data Position = Position { x :: Int, y :: Int }
    data Player = Player { pos :: Position }
    
    moveRight :: Int -> Player -> Player
    moveRight n (Player (Position x y)) = Player (Position (x + n) y)


Every time you add a new property to Player, wouldn't you be forced to update all such functions to pass along the extra properties?


Nah. Not if you use an appropriate form to extract the desired member while leaving the rest alone.

    data Person = Person { name   :: String  ,
                           age    :: Integer ,
                           height :: Integer ,
                           weight :: Integer
                         }
                deriving (Show)
    
    makeOlder p@(Person { age = current }) increase = p { age = current + increase }
    
    main = let b = (Person { name = "Bob", age = 50, height = 72, weight = 190 })
               c = b { age = (age b) + 10 }
               d = makeOlder b 10
           in do
             putStrLn (show b)
             putStrLn (show c)
             putStrLn (show d)
    
    -- ~/test/E$ ./M
    -- Person {name = "Bob", age = 50, height = 72, weight = 190}
    -- Person {name = "Bob", age = 60, height = 72, weight = 190}
    -- Person {name = "Bob", age = 60, height = 72, weight = 190}


Because player and position are records, the function could actually be written:

    moveRight :: Int -> Player -> Player
    moveRight n p@(Player {pos = curPos@(Position {x = curX})) = 
        p {pos = curPos {x = curX + n}}
In which case it isn't making any assumptions about extra properties. The record pattern-matching syntax isn't the prettiest, though, and I find myself having to look it up every time to remember where the accessor name (pos), parameter name (curPos), and intended pattern (Position...) go relative to the "=" and "@".


In the most naive way of doing such things, there is indeed such a tradeoff. So let's say that my Player is now allowed to collect Rice, which he will use in certain eat() functions.

If Player is a data structure, I could add the rice counter to the Player and then modify all of my functions to "pass through" the rice. If the game is small enough, this makes the most sense. This is not as bad as it sounds because the typechecker will get angry about any Player functions in the code which do not have rice. So I've got the language on my side when I want to "make sure I've got all of them," it's not just a haphazard grep for the token "Player".

I could also write everything dogmatically with a pattern matching syntax which passes through all of the rest of the data. This is embodied in the examples below so I won't re-write it.

On the other hand, I can treat the game as a module, create a new data structure `PlayerWithRice {player :: Player, rice :: Int}`. Then I can define `addRice :: (Player -> Player) -> PlayerWithRice -> PlayerWithRice` which takes a function for modifying players without rice, and turns it into a function for modifying players with. Then there's just a list of new declarations in a new module which add rice to existing code.

This all sounds complicated! Surely there's something simpler? There are a few simpler ideas: one is called a Monad Transformer. The Player can be stored in a slightly more complicated way at first, so that it "wraps" another object which holds "all future data-to-be-added". All of your existing code written in this context will just automatically pass this 'future' data through. For PlayerWithRice you simply fill in the 'future' data as a data structure which holds rice and also more future data.

Second, there are lenses, which the above article discusses. If you write every set/get with lenses, then updating the data structure only requires updating those lenses, and all the rest of the code will follow. Again, you'll have a type-checker on your side to guarantee that you didn't miss anything crazy when you update those lenses.


Lenses are a way to try to minimize that effect, because it does "kind of" happen. This is one end of what Wadler termed the expression problem, essentially most languages are structured such that either the nouns or the verbs are complex since if they both are then you have n^2 interactions.

Haskell chooses to make functions complex to enable simpler data types. This means there's a lot of drive to have useful, abstract interfaces, but it also means that you have to protect against the kind of complexity you get when expanding your objects.

Lenses help to do this by abstracting "views" on objects so that you only have to update the lens... and that can be done automatically.


There is no metaprogramming here. You may not be familiar with a style of programming, but that doesn't entitle you to spread FUD about it.


To uses lenses with record types usually one would generate the lens methods using template haskell.


Why do you need to mutate things in order to write a program? Operational programming may have damaged your brain.

(With regards to lenses, note that: This isn't mutating anything, and it's not a metaprogramming "hack" either. Functions as first-class entities instead of opaque blobs of code is one of the big wins of functional programming.)


> Why do you need to mutate things in order to write a program?

To change the state of the universe from one in which my program is not written to one where it is.

Furthermore, if the program I write mutates nothing upon execution there is no point in running it.


It's a trade-off. You can either use direct mutation using some sort of impure reference type, or use a pure mutation using first class accessor labels.

Impure references are probably easier to use, but are harder to reason about and allow for more subtle bugs in your code. Accessor labels require a bit more trickery, but give a more principled approach and result in code that is less error-prone and easier to compose.

Haskell doesn't force you in any of those directions. Pick the approach that bests fits your domain and likings. Personally I'm convinced pure code is a way better default, however for certain domains I certainly resort to using `IORef`s, `TVar`s, etc.




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

Search: