Earlier quoted context omitted.
I don't think that's true. Say you have an imperative algorithm that modifies an array in a loop. Then the Haskell encoding of that algorithm will use writeArray. But since it's monadic, it won't look like general recursion, it'll need to use a combinator like forM. You could say it's general recursion plus bind and return, but it gets worse. If the loop body needs to use both writeArray and randomness, you need more…
What's wrong with this? import qualified Data.Vector.Mutable as V import Control.Monad (when) import qualified System.Random as R main = do v
Rust as a gateway drug to Haskell
191–200 of 218 posts
Re: Rust as a gateway drug to Haskell
#192Earlier quoted context omitted.
What's wrong with this? import qualified Data.Vector.Mutable as V import Control.Monad (when) import qualified System.Random as R main = do v
That's indeed nice, and made me change my mind somewhat. I wonder if you can write a napkin example that mixes ST with randomness, instead of using IO for everything?
import qualified Data.Vector.Mutable as V
import Control.Monad (when)
import qualified System.Random as R
import Control.Monad.ST
st = runST $ do
v Re: Rust as a gateway drug to Haskell
#193Earlier quoted context omitted.
That's indeed nice, and made me change my mind somewhat. I wonder if you can write a napkin example that mixes ST with randomness, instead of using IO for everything?
It's not too bad to manually thread a seed around. import qualified Data.Vector.Mutable as V import Control.Monad (when) import qualified System.Random as R import Control.Monad.ST st = runST $ do v
Re: Rust as a gateway drug to Haskell
#194Earlier quoted context omitted.
> There's no general purpose idiom you can use to replace all loops, only tons of special purpose HOFs you must memorize. Well, yes, there is: general recursion. You can recreate any loop with plain old recursion. But stating that as a problem is a little like coming to Java and saying that "there's no general purpose idiom you can use to replace goto, only tons of special semantic branching constructs you must memor…
I don't think that's true. Say you have an imperative algorithm that modifies an array in a loop. Then the Haskell encoding of that algorithm will use writeArray. But since it's monadic, it won't look like general recursion, it'll need to use a combinator like forM. You could say it's general recursion plus bind and return, but it gets worse. If the loop body needs to use both writeArray and randomness, you need more…
----
I'm going to show you some Haskell code now. This is code that could probably use improvement, but the reason it may look sketchy in some places is that I – surprise! – reliably wrote it on a napkin.
Excepting some typos, obvious brainfarts and missed imports, this is actually the first draft of the code. Here's the quicksort algorithm as it is written on Wikipedia:
algorithm quicksort(A, lo, hi) is
if lo
It's Haskell implementation will be very similar: quicksort vec = runST $ do
mvec
I'll walk through it line by line, even though most of it is very similar to the Wikipedia imperative pseudocode. quicksort vec = runST $ do
mvec
We run this stuff as an ST expression, which is the Haskell way of saying "hey this block of code does actual mutation, be careful". The first thing we do is thaw the vector, which means making it mutable. let loop lo hi = when (lo
We define a loop that is going to depend on two variables for iteration: lo and hi. It runs for as long as lo is less than hi, and will break when hi is equal to or less than lo. p
First, we call the partition procedure which divides the vector into two halves and returns to us the index of the pivot element between the two.Then, since we are in a function called "loop" we can continue to the next iteration by calling "loop". The neat thing about this is how it looks like we almost defined our own keyword, which acts somewhat like a "continue" statement in imperative languages.
There is a difference, though. The "continue" statement in an imperative language would abort the current iteration, go back to the top of the loop and run another Iteration. The "loop" statement in our code also goes back to the top of the loop, but it doesn't abort the current iteration. Which means we can
loop (p+1) hi
also run a second iteration. This is in principle independent from the previous execution, so you can sort of view this as a "multi-continue" that starts two new iterations of the loop in parallel. In reality, though, the execution is sequential because the compiler doesn't have enough information to determine that they are indeed independent. loop 0 (Vector.length mvec - 1)
Note that until now, the loop was only defined – it was never executed. But now we execute it with the initial values for lo and hi. It may seem weird that definition and execution of a loop can be separate from each other, but I haven't been able to come up with any reason that could end up bad. freeze mvec
After we're done, we freeze the vector to make it immutable again. It might sound like an expensive operation, but it's not. Hopefully, the compiler will understand that nobody else has simultaneous access to the array so it will perform the modifications in-place and optimise away the thawing and freezing.(continued in second comment)
Re: Rust as a gateway drug to Haskell
#195Earlier quoted context omitted.
> There's no general purpose idiom you can use to replace all loops, only tons of special purpose HOFs you must memorize. Well, yes, there is: general recursion. You can recreate any loop with plain old recursion. But stating that as a problem is a little like coming to Java and saying that "there's no general purpose idiom you can use to replace goto, only tons of special semantic branching constructs you must memor…
I don't think that's true. Say you have an imperative algorithm that modifies an array in a loop. Then the Haskell encoding of that algorithm will use writeArray. But since it's monadic, it won't look like general recursion, it'll need to use a combinator like forM. You could say it's general recursion plus bind and return, but it gets worse. If the loop body needs to use both writeArray and randomness, you need more…
But what's really interesting is the partitioning procedure. That's where things get complicated. According to Wikipedia, it can be implemented in an imperative language like so:
algorithm partition(A, lo, hi) is
pivot := A[hi]
i := lo - 1
for j := lo to hi - 1 do
if A[j] ≤ pivot then
i := i + 1
if i ≠ j then
swap A[i] with A[j]
swap A[i+1] with A[hi]
return i + 1
This is the Haskell translation I came up with: partition a lo hi = do
pivot do
j
Again, I'll walk through it part by part. partition a lo hi = do
pivot
Since we're already running this as part of an ST expression, we don't need to specify "runST". First thing, we read the last element of the vector as our pivot, and we define two new references to mutable values i' and j'. (I like to indicate references with ticks like that to distinguish them from the value they contain. Ticks sort of remind me of the C pointer asterisk so it works out.) fix $ \loop -> do
j
Okay, so last time we created a named loop through the "let" keyword, which defines new variables and functions. We could do that here as well, but I think this other approach generates neater code in this case. The "fix" combinator might twist your mind the first few times you see it, but suffice it to say that it creates a recursive function from an anonymous function, by supplying the function with itself as its first argument. You'll see soon one of the reasons I preferred it in this case.Then we read the value of the j' reference and use it as our loop condition. The loop should run as long as j is less than hi.
aj
We read the value under j in the array, and if it is less than the pivot we increment the value in the i' reference. If the incremented value is different from j, we swap the two in the array. inc j'
loop
Regardless of how aj compared to the pivot, we increment the value under the j' reference and go back to the top again to start a new iteration. i
When the loop has finished, we increment i', swap the pivot element back in to its right place, and then return i.There are three things of note here:
1) One of the major differences with the imperative code is that in imperative Haskell code, we sometimes need to "dereference" mutable variables in a separate statement. We (generally) cannot do that as part of an expression.
There are some structured ways around this even in Haskell, but at that point it might no longer be worth using Haskell to write imperative code. Why do I say that? Because it's actually a good thing that we need to dereference mutable variables in a separate statement. Several "safe coding standards" over the decades have evolved toward "keep expressions free from side-effects and have one statement per side effect".
2) When we defined the loop with fix we didn't have to call it separately from its definition. That's one of the benefits I was talking about.
3) I forgot what number three was.
----
Randomness, you said? That's a common optimisation to the quicksort shown above. The pivot is picked at random in the inclusive range [lo,hi] instead of fixed at hi.
It is child's play to include it by simply passing a random generator as a parameter down the call chain to partition, so I'm not going to show that. What I'm going to show instead is how trivial it is to abstract that parameter away into a state transformer wrapper.
I intentionally ignored this aspect until now to get honest results about shimming randomness in there. Here are the changes:
1.
partition a lo hi = do
p
I converted the partition method to a state transformer, which makes it possible to compute a random number in the [lo,hi] range and implicitly update the generator state. Then I swap the highest element and the chosen pivot, and the rest of the algorithm is the same as before.The state transformer also means that the rest of the partition function is now lifted, but that's no biggie.
2.
quicksort gen vec = runST . flip evalStateT gen $ do
-- pivot
The quicksort function needs to wrap the ST expression in a state transformer, but is otherwise exactly the same as before.Since we need some sort of generator to start with, the quicksort function now also takes a generator as an argument and puts it in the state.
This is not super clean – the quicksort needs to receive a generator each time it is called? Well, yeah, sorta–kinda. This is probably one of those places where it's legitimate to do an "unsafePerformSomething" – the randomness does not cause any actual impurity in the code.
----
What I did not attempt, and what is a complicated subject anyway, was the issue of arbitrary pivot selection strategies. It's easy to integrate support for random pivots, but what if the user wants to specify a pivot selection strategy of their own?
That's fine if it's pure, or at least only relies on randomness, but what if it's unrestricted in its effects? What if they want to supply a strategy that calls your grandmother and asks her for a good pivot, or one that launches nuclear missies and counts the casualties to determine a pivot?
That's clearly some serious international side effects, and I think that we do want to prevent the user from inserting strategies with arbitrary effects. But where to draw the line? And how to distinguish these kinds of effects? Haskell only throws them all into the IO bin, which is a problem.
But the problem is not that effects are controlled, it's that even in Haskell there's a certain lack of control over effects.
Re: Rust as a gateway drug to Haskell
#196Earlier quoted context omitted.
I don't think that's true. Say you have an imperative algorithm that modifies an array in a loop. Then the Haskell encoding of that algorithm will use writeArray. But since it's monadic, it won't look like general recursion, it'll need to use a combinator like forM. You could say it's general recursion plus bind and return, but it gets worse. If the loop body needs to use both writeArray and randomness, you need more…
(continued from previous comment) But what's really interesting is the partitioning procedure. That's where things get complicated. According to Wikipedia, it can be implemented in an imperative language like so: algorithm partition(A, lo, hi) is pivot := A[hi] i := lo - 1 for j := lo to hi - 1 do if A[j] ≤ pivot then i := i + 1 if i ≠ j then swap A[i] with A[j] swap A[i+1] with A[hi] return i + 1 This is the Haskell…
Just a small note, randomness in quicksort can't be hidden in unsafePerformSomething, because it does lead to impurity. For example, sorting [(0,1),(0,2)] on fst will give different results depending on randomness. But that's not to detract from your main point.
Re: Rust as a gateway drug to Haskell
#197Earlier quoted context omitted.
Is a difference in a default option really enough for a language to take the place of another? And if it's not just a change in a default option, but the removal of laziness, we lose something, which will make the choice less obvious. I think it's more likely that, if dependent types prove really useful, Haskell will adopt these, and people will adapt their code, rather than port everything to a new language. As far…
>And if it's not just a change in a default option, but the removal of laziness, we lose something, which will make the choice less obvious. What would we exactly lose? >As far as I can see, Idris is too much like Haskell to take its place. Porting thousands of libraries to a new language is a huge effort, so a huge advantage is required, which I don't see Idris offering. That is true, though. Perhaps Rust is the saf…
We'd lose composability and clearer code. This[1] section of the Haskell Wiki contains a good example.
In short, with something like this:
any :: (a -> Bool) -> [a] -> Bool
any f lst = or boolLst
where boolLst = map f lst
the compiler can produce reasonably optimal code, because it doesn't have to convert the entire [a] to a [Bool], because of lazy evaluation -- when or encounters the first True, the map f lst expression stops being evaluated because of lazy evaluation.Re: Rust as a gateway drug to Haskell
#198Earlier quoted context omitted.
but F# doesn't have Ocaml's performance, which I believe was the point being made for ML over Haskell I honestly don't know how F# compares to Haskell over performance
F# is essentially just C#, which should be reasonably performant? It's possible the persistent data structures aren't optimized I guess? Pity something's busted in the benchmarks game: http://benchmarksgame.alioth.debian.org/u64q/fsharp.html
Is F# working with .NET Core 2.0 Preview 1 for you?
Re: Rust as a gateway drug to Haskell
#199Earlier quoted context omitted.
> .. you can write vastly more complicated programs that are, say, 80% as fast as hand-optimized C for only 10% of the effort of hand-optimized C. Computer Language Benchmarks of GHC versus C don't seem to be close to matching your 80% as fast claim [1]. Also, the 10% of the effort of hand-optimized C bit - seem to recall there was a caveat - "if you happen to Don Stewart" [2]. The following is just my vague, uninfor…
>Computer Language Benchmarks of GHC versus C don't seem to be close to matching your 80% as fast claim [1]. Also, the 10% of the effort of hand-optimized C bit - seem to recall there was a caveat - "if you happen to Don Stewart" [2]. That's a really bad benchmark. The power of Haskell is not that it's fast for generating the Mandelbrot set, it's that it allows you to things you couldn't do in any other language whil…
Re: Rust as a gateway drug to Haskell
#200Earlier quoted context omitted.
F# is essentially just C#, which should be reasonably performant? It's possible the persistent data structures aren't optimized I guess? Pity something's busted in the benchmarks game: http://benchmarksgame.alioth.debian.org/u64q/fsharp.html
It wasn't busted with .NET Core 1.0.1 005db40cd1. Is F# working with .NET Core 2.0 Preview 1 for you?