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

This article is a litany of issues that pure functions solve, despite the backhanded dismissal:

> Much as we like our platonic ideals, our pure functions, we ultimately have to acknowledge that every function exists in a context. The most basic context is the physical hardware on which your code is running

(Note: pure functions set such a high bar that it's a stretch to criticise them. Might as well blame the hardware instead. Which is fine, just be equally dismissive of other 'solutions'.)

> when one fails, it’s up to the engineer to figure out why. In the case of race conditions or other rare occurrences it might not even be possible to reliably reproduce the issue in a debugger,

Pure functions will return the same input for the same output. It will reproduce.

> In particular, they should be better at detecting cases where two separate units conflict with each-other.

Pure functions do not conflict with each other - no detection needed. ("Conflicts" can only arise by "passing the wrong thing" from function to function, which I don't think is the idea here, e.g. in `getPetsName = (getName . getPet)`, getName and getPet cannot conflict with each other, but if getName accidentally returns an address, then of course getPetsName will return the pet's address.

Deadlocks:

> One of the simplest and most pernicious programming problems is the humble deadlock.

> Unfortunately deadlocks are not always so easy to spot, especially as more layers of abstraction are added.

> Deadlocks are common, hard to debug, and can crash an entire app — yet our best defenses against them, integration tests, are porous and blunt.

Pure functions don't deadlock.

> The symptoms are less severe than a deadlock, but the root cause is often quite similar: a function calls a function that calls a function that calls a function that does something inappropriate.

> Compilers don't even try to help here.

It is a compile-time error to call an impure function from a pure function.

> When I call getTimestamp(user.registeredAt, TimeZone.PST) I can assume that it’s just doing some simple math and returning a result. It probably doesn’t make network calls or hold locks or mine bitcoin

No need to assume, check the type signature. Pure function. The compiler will check it for you if you forget:

    utcTimeTimestamp :: UTCTime -> Timestamp
    https://hackage.haskell.org/package/timestamp-0.2/docs/Timestamp.html
> Context is only dangerous because it is absent from a function's arguments and so is opaque to the caller. The caller, for example, has no way to know if a function could cause a deadlock because it cannot know if the implementation of that function relies on locking.

> Knowing when a function modifies state is half the battle, the other half is knowing when a function reads state. This is not an Effect, but rather a Coeffect — although sometimes it all gets lumped together under the banner of ‘Effects’ or ‘Effect Systems’.

Of course it gets lumped together. That kind of reading is an effect. It can cause deadlocks, it can block your UI, it requires thinking about context, it may not be trivially reproducible.

That's enough about pure functions. The author then pivots to proposed features to support context. These don't solve the above problems, but there's plenty of prior art. So this last bit is more about Haskell and less about pure functions in general.

We have a complicated git merge process at work, with around 70 repos. I wrote a Haskell CLI tool to help me out with the process. I believe I've done exactly what the author is suggesting (explicit, compiler-assisted context markers) by using the 'tagless final' approach. Here are a couple of example type signatures:

  ensureCheckedOut :: (Git m, Logger m, Monad m, StatusApi m) => Repo -> m ()

  ensureRepoDeleted :: (Logger m, Monad m, Shell m, StatusApi m) => Repo -> m ()
This keeps my context in check by only allowing me to call impure functions if they are provided by one of the constraints on m. e.g. I can call logging from both, but ensureCheckedOut cannot run shell commands, and ensureRepoDeleted cannot invoke my git api (for the pedantic, it could technically use the shell to invoke git via cli.) Pure functions are of course allowed from anywhere.

In short:

* Pure functions do solve the above problems and it's a mistake not to use them wherever possible.

* Context systems do not solve deadlocks, integration issues, reproducibility, etc. But if you think they're useful, Haskell's got you covered using the 'tagless final' approach.



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

Search: