
Imperative Haskell - rutenspitz
http://vaibhavsagar.com/blog/2017/05/29/imperative-haskell/
======
runeks
As far as I'm concerned, purity is more fundamental than impurity, because we
can always emulate impurity using a pure language (as is shown in this
article), but it's not possible to emulate purity using an impure language.

This only leaves us with the challenge of performance. While we can always
describe e.g. x86 assembly in terms of a pure intermediate representation
language (e.g. GHC Core), transforming this description back into equally
performant x86 doesn't seem easy.

Also, anyone care to produce some benchmarks comparing the imperative
Python/Haskell quicksort implementations? They look so similar, it'd be quite
interesting to see how well GHC can optimize this sort of stuff.

~~~
cousin_it
Purity is tricky to define. For example (stolen from Reddit user gasche), if
you can measure how much time a computation takes, then Haskell is impure,
because a lazy value takes longer to compute the first time than the second.
You could try to patch it up by saying the impurity must be observable via
pure code, but that makes the definition circular. And that raises another
problem, where printing to standard output becomes "pure" if the standard
output can't be observed by pure code (which is true in Haskell).

Another problem with seeing purity as fundamental is that our physical reality
isn't a persistent data structure (past states aren't accessible), so the best
algorithms possible won't treat it as one. Among the tons of papers describing
fast algorithms, practically none are using purity, unless they required
purity to begin with.

And there's a third problem, specific to Haskell, that might interest you. The
article is using ST to implement an impure computation. You'd think that ST
itself can be implemented in pure Haskell, maybe with the usual logarithmic
slowdown. But unfortunately no one knows how to do it, and there's a strong
suspicion that the (pure) type signature of runST has no pure implementation
at all, even with quadratic or exponential slowdowns. The reason is tricky to
explain, but it's well covered on StackOverflow and Reddit.

(That's not even going into the issues of quicksort. Suffice to say that a
pure quicksort is hard to write, ST or no. The problem is that you need good
pivot selection to prevent the quadratic worst case, but randomized pivot
selection will lead to observable impurity in the output, because quicksort
isn't stable. So you must use something like median of medians, making the
algorithm much slower and more complicated.)

~~~
lmm
A pure language is one in which replacing any subexpression of any expression
with the evaluation of that subexpression yields an equivalent expression.

Of course this means "pure" is not an absolute term but rather relative to a
given definition of "equivalent". But this isn't circular and is practically
useful: if you're working in a context where precise execution time matters
(e.g. cryptography) you really do need to use a different concept of "pure"
language from what you would use in a more "normal" context where two programs
that produce the same output are equivalent even if they take a different
amount of time to execute.

~~~
skybrian
Performance matters for any task that has a deadline, not just crypto and
real-time stuff. That's why we make performance improvements, after all.

I think it might be better to think of purity as a way of ensuring that
performance and logical correctness are independent effects. Purity allows us
to safely improve performance by making local substitutions of faster but
logically equivalent code, without having to reason globally about
correctness. It's also what allows us to tolerate variation in performance
(within reason) without threatening correctness.

For crypto we can reason separately about logical effects (does the crypto
work) versus information leakage. For a user interface, we can treat dropped
frames as a performance problem rather than a correctness problem.

This is useful even though we still care about performance. Substituting a
much slower function for a faster one probably isn't okay, but purity lets us
understand the effect.

~~~
lmm
Performance matters a little bit - most of the time we don't make perfomance
improvements. Really in a context where performance was of serious importance
I would want to have an explicit model of it to be sure I could reason about
it compositionally.

I find any single effect is easy to reason about in isolation, it's the
interaction that gets tricky. Viewing purity as isolating performance is just
one perspective on this - you can equally view it as isolating state mutation,
or isolating async transitions.

~~~
skybrian
Yes, compositional reasoning is important, but it depends on being able to
substitute equivalent code. If you're too strict about what counts as
equivalent (including performance in the type system, say) it would be harder
to make local changes since fewer substitutions would be possible.

You can look at const correctness in C++, Java checked exceptions, async
versus non-async functions [1], and Rust's ownership model as other examples
where making fine distinctions for good reason has a side effect of making
substitution harder - or perhaps safer, depending on your point of view.

If you need it, you need it, but when you don't, it's a luxury to be able to
substitute whatever you like and see what happens.

[1] [http://journal.stuffwithstuff.com/2015/02/01/what-color-
is-y...](http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-
function/)

~~~
lmm
> If you're too strict about what counts as equivalent (including performance
> in the type system, say) it would be harder to make local changes since
> fewer substitutions would be possible.

I think this is avoidable if your types/effects can decompose properly into
orthogonal factors. E.g. with a Future type and a generic monad abstraction
it's fairly easy to substitute a sync call for an async call - you have to be
explicit about the change you're making, but only very slightly, and you'd
never get confused between changing sync/async and changing the "actual" type
that's returned.

------
fizixer
The first code block reads like a definition, or the 'what' of quicksort,
which has to be fleshed out using the 'how' of quicksort. And it just so
happens that 'what' is declarative, and 'how' is imperative (i.e., do this, do
that, that's how).

Speaking of which, there is a definition of, not quicksort, but sort itself,
and it goes something like this:

'sort is a map that takes a sequence S of orderable items, to one of its
permutations P such that for every two adjacent elements e1, e2 in P, e1 < e2'

I'm not familiar with Haskell syntax but I'm sure this definition can be
encoded in Haskell. Actually imperatively speaking, this is a sort algorithm
itself, the very less discussed (probably because of very high complexity)
'permutation sort', i.e. you keep permuting the input sequence and keep
checking your e1/e2 condition until it is satisfied.

So we can define sort, but then we can specify a language to not compute based
on that definition but instead pick from one of the preferable computation
schemes that are also defined in that langauge (quicksort, mergesort, etc,
etc). But then we specify not to use the definition of that scheme but instead
use an algorithm for it (e.g., the imperative algorithm for quicksort).

I guess this is a form of semantic layering, something that I'm interested in
as a topic of study.

~~~
infinisil
That's where formal proofs come into play, because you need to somehow show
that the implementation actually does what the definition states. Since
Haskell isn't a proof checker you can't use it for this. The newish and very
promising looking language Idris [1], which is very similar to Haskell, can
actually do this because of having dependent types. Upon a quick search I
found this great example [2] of a proof that insertion sort does indeed return
the list sorted, all in the type system. I can really recommend Type Driven
Development [3], a recently released book on development in Idris.

[1]: [https://www.idris-lang.org/](https://www.idris-lang.org/)

[2]: [https://github.com/davidfstr/idris-insertion-
sort](https://github.com/davidfstr/idris-insertion-sort)

[3]: [https://www.manning.com/books/type-driven-development-
with-i...](https://www.manning.com/books/type-driven-development-with-idris)

~~~
marcosdumay
Well, if you decides like the GP does, that the definition is the Haskell
code, it's not a proof checker, but it's an automatic proof writer that
(except from bugs on GHC) is expected to write correct proofs.

------
aetherspawn
Is the ST performance better or worse than the purely functional approach? It
actually looks pretty good, not really any more verbose than Java to be
honest.

~~~
Tarean
Some algorithms require mutation to have a decent time complexity and ST
allows you to implement those.

There is a tiny cost for each mutable memory location since the garbage
collector has to work around then. A single array won't even be measurable,
though.

Also, there are some referentially transparent algorithms that can't be
implemented via ST - like laziness.

------
marcosdumay
The article is assuming a little too much when it's claimed that the first
implementation doesn't sort in place.

It's not clear how GHC would optimize this (even more if you don't use the
unsorted list for anything else), and it would be interesting to see the
benchmarks for that version too.

------
_pmf_
That's Haskell I can understand.

