Rust as a gateway drug to Haskell
xion.io
xion.io
Ran across Option in it the other day, though it's not used heavily. I really wish there was something like Haskell but with a runtime like Nim or Go. Perhaps that is OCaml?
I wish I could invest some time in working with Nim. It doesn't get as much attention as it deserves in my opinion, and it would really benefit from growing the community a bit.
Basically 4.0 will introduce the notion of exclusive access, with more affine type ideas to follow up on 4.x.
EDIT: This feature is described by SE-0176.
https://github.com/apple/swift-evolution/blob/master/proposa...
Or this one https://developer.ibm.com/swift/
+1
One approach that occurred to me was to use Haskell and Hackage for prototyping a design and then a Rust implementation for production.
It also reminds me of this from several years ago: GHC honcho Simon-Peyton Jones saying the next Haskell will be strict, but still pure with effects only via monads. [1], [2].
Following on and presumably related to that PureScript is strict. [3]
[1] https://news.ycombinator.com/item?id=1924094
[2] http://www.cs.nott.ac.uk/%7Egmh/appsem-slides/peytonjones.pp... (PowerPoint) Slide 40
[3] https://github.com/purescript/purescript/wiki/Differences-fr...
Idris is strict too, but optional laziness is built into the compiler with the use of a special-cased Lazy wrapper of the form
data Lazy a = Delay a
This is kind of the opposite of strictness annotations (bang-patterns) from Haskell, except one doesn't need to pattern-match on Delay or use it to wrap arguments to functions expecting Lazy values. The Idris compiler can do something like "laziness analysis" (admittedly trivial in comparison to GHC's strictness analyser) to introduce and eliminate Delay constructors automatically. The laziness information is carried around in the types: force : Lazy a -> a
force a = a
https://github.com/idris-lang/Idris-dev/wiki/Unofficial-FAQ#...(purescript-lazy is also a thing, but I think it's not as seamless because of the "no compiler magic" position.)
In any case, what about -XStrict? I've never used it (or -XStrictData either), but isn't it a solution to the laziness problem, albeit a nuke-ish one at that?
And while we're thinking ahead, let's talk about the project to get linear types (something similar to Rust lifetimes, but weaker) into GHC :)
Why weaker? My understanding is that linear types provide a stronger guarantee than Rust's affine types: linear types guarantee that a value is used once, while affine types only guarantee that a value is used no more than once.
SPJ made a great lecture about this. It is very easy to follow and a fun insight into the academic history of modern functional languages https://www.youtube.com/watch?v=re96UgMk6GQ
I think all he meant that strictness was an interesting paradigm that would differentiate a language enough to justify its existence alongside Haskell.
>Following on and presumably related to that PureScript is strict
Partly also to distinguish from GHCJS. Laziness in a strict language is really hard and PureScript generates semi-readable JavaScript.
Trying to compete with Rust or C++ performance in Haskell is certainly frustrating to reason about :)
Obviousness is hard to pin down. What may be completely obscure to a Haskell beginner is obvious to a veteran. Is it really fair to count people's strict-language preconceptions against Haskell? I don't know.
GHC provides a wealth of profiling tools that an expert Haskell user can employ to find space leaks. Heck, experts generally know where to look (excessive use of lists, spine-nonstrict data structures) so that the profiling tools become a last resort.
Since performance is exactly a measure of the number of imperative steps that need to happen in a program, low level imperative langauages are going to win in performance-obviousness every time. (Rust probably beating C in terms of avoiding memory problems, C probably beating rust in terms of not supporting language constructs that can make a single line worse than O(1).)
"Accidentally" asking the language to do something time-expensive is as impossible in C as forgetting a free() in Haskell.
Unless you use GCC's cleanup extension (which runs an arbitrary function when leaving a scope, similar to Rust's Drop trait). Or evil macros which turn innocent-looking code into something else (a well-known project, in some compilation modes, redefines the "if" keyword to track how many times each branch is taken). Or both at the same time (like hiding GCC's cleanup extension with a macro).
This is an oversimplification. Writing fast code for modern processors is all about how much time you spend waiting for memory.
e.g. bubblesort will beat quicksort for tiny lists or large lists which are already sorted.
Or, put another way, 1000 operations against 1ns latency memory (L1) has the same performance as 10 operations against 100ns latency memory (RAM).
TL;DR: "fast" isn't well-defined, and for a lot (or even most) applications "fast" is about latency, not throughput, and Erlang is "faster" in that sense.
[1] https://making.pusher.com/latency-working-set-ghc-gc-pick-tw...
[2] they eventually abandoned Haskell for Go https://making.pusher.com/golangs-real-time-gc-in-theory-and...
The other big part of this is that it can make stream fusion predictable. Currently haskell programs can become 100x slower or faster after some innocuous change, so linear types will make it a lot easier to write fast haskell in general.
Well, yeah, Rust has manual memory management. Of course you don't get space leaks.
Anyways, Rust's laziness is not on par with Haskell. Iterators are great but it's not at all the same as e.g. GHC's lazy IO.
I wouldn't call Rust's memory management "manual". It's the only language I know of that I'd describe as having automatic memory management without garbage collection.
> Anyways, Rust's laziness is not on par with Haskell. Iterators are great but it's not at all the same as e.g. GHC's lazy IO.
Iterators, futures, and macros (for creating new language constructs) tend to provide the laziness I want. Beyond that, I've tended to find laziness as much a source of bugs as features. (Speaking from a perspective of having written and maintained large programs in Haskell, and in Rust.)
I honestly don't know how F# compares to Haskell over performance
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?
Maybe the only thing I would complain would be the separation between integer and floating point math.
Something about people writing fast code with Flow/TypeScript than with plain JavaScript because the typing leads to less polymorphic code which is easier to optimize for the compiler.
This has nothing to do with performance per se. It doesn't make OCaml code faster than languages that allow you to write "+" for both int+int and float+float. The compiler knows anyway.
Anyway, Reason seems to have a bit nicer syntax than OCaml, I think.
Not saying these are insurmountable, unforgivable problems, just really annoying.
If it's really a struggle for some reason, you can always take the nuclear option and enable strict mode on your code.
Haskell is very good for writing extremely fast code that's also extremely composable, which most languages (even Rust, to a substantial degree) really struggle with. Haskell's pure non-strict semantics and support for fusion/rewrite systems means that you can write complex operations on vectors, byte arrays, text, etc. that span multiple modules but get compiled down to extremely tight in-place assembly code. Another way of looking at it is that the economies of scale for Haskell are very different. There's a higher up-front cost because you have to learn about how strictness works, but as a consequence 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, uninformed impression, but Haskell and Go seem to be opposite ends of a spectrum, with Rust somewhere in between:
Haskell: good for expert individuals and small teams. The language is highly sophisticated and the compiler is rather slow. At heart, it's a research language, so the direction might not always suit production use. If you contract out to a Haskell development shop, who do you turn to with your Haskell code-base if things don't work out?
Go: designed by Google for their use-case - large teams of commodity programmers. It's very quick to onboard programmers - they can be productive and non-dangerous quickly. The language is very simple and the compiler is very quick - which is again good for scaling large teams.
[1] https://benchmarksgame.alioth.debian.org/u64q/compare.php?la...
[2] https://donsbot.wordpress.com/2008/05/06/write-haskell-as-fa...
Your comparison between Haskell and Go is about market size, not language design. I suspect you would have a similarly hard time finding Rust devs as Haskell devs (i.e. mildly less convenient than a popular language like Python or Go).
I don't mean to be personal, but your quantative claim smells a little like unsubstantiated BS. Do you have any data to back it up?
> That said, Haskell performs pretty well in the toy benchmarks anyway.
Currently, "pretty well" looks like GHC performance is 50% to 10% of hand-crafted C using perhaps 5 times as much memory. (If the GHC program builds at all that is.)
fannkuch-redux
Haskell GHC: 16.70 sec 7,628 mem
C gcc: 8.97 sec 1,660 mem
spectral-norm
Haskell GHC: 4.06 sec, 9,880 mem
C gcc: 1.99 sec, 1,824 mem
n-body
Haskell GHC: 24.48 sec, 6,468 mem
C gcc: 9.96 sec, 1,016 mem
reverse-complement
Haskell GHC: 1.39 sec, 132,176 mem
C gcc: 0.42 sec, 143,948 mem
mandelbrot
Haskell GHC: 11.64 sec, 41,180 mem
C gcc: 1.65 sec, 32,684 mem
fasta
Haskell GHC: 14.68 sec, 455,088 mem
C gcc: 1.33 sec, 2,856 mem
binary-trees
Haskell GHC: 26.28 sec 511,444 mem
C gcc: 2.38 sec 131,728 mem
k-nucleotide
Haskell GHC: Make Error
C gcc 0.06 sec ? mem
pidigits
Haskell GHC: Make Error
C gcc: 0.06 sec ? mem
regex-redux
Haskell GHC: Make Error
C gcc: 0.02 sec ? mem
Haskell GHC: The Glorious Glasgow Haskell Compilation System, version 8.0.2
C gcc: gcc (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406
In parts haskells slowness seems to be because no one cares enough to work on it. Can't say for sure, though, so maybe I just should try a task and implement it to see how fast it'd be without amazing optimization foo.
Although of course these benchmarks aren't really representative of the real world because they'd just end up as a c ffi call in most languages if they actually were that performance critical.
But then remember that @wyager is claiming
>80% as fast as hand-optimized C for only 10% of the effort of hand-optimized C
If it's really only 10% of the effort then one shouldn't really have "to work on it" to get semi-decent performance.
A more modest claim of say:
50% as fast as hand-optimized C for only 20% of the effort of hand-optimized C
would have been more credible and still a big win for many use cases.
let outputs = map (exec content) tasks `using` parList rseq
That got around 24 seconds which seems acceptable for a fairly straightforward solution that is around half the size of the rust one I copied. Might try to optimize later.[1]: http://benchmarksgame.alioth.debian.org/u64q/knucleotide.htm...
It cannot.
The line "main = putStr.pidgits.read.head =<< getArgs" has been commented out because the program author has optimised-away some of the work that must be done.
My fault (except for pi-digits and regex-redux).
I neglected to work-through the force-reinstalls that inevitably follow my update of the GHC compiler.
Done.
I referenced Rust because that was the article was about.
Swift and F# are languages that are not as basic as Go, but easier to find devs than Haskell.
That said, Rust has a lot going for it, even if there are a lot less devs than Swift.
Heh
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 while still being fast enough to write e.g. compilers (Idris) or web services (Yesod).
Other real world programs have a strict "always push things into the world, react to inputs" cycle that Haskell has trouble supporting.
It's a common concern but it's a terrible critique. Laziness has many subtle benefits, but everyone whines about it because its pitfalls are decidedly not subtle.
"It only does what is needed!"
I didn't use Haskell but Nix and at least there it seemed reasonable to "not do" everything.
In Haskell when the compiler misses an optimization the performance hit is potentially unbounded, as it could think anything is a dependency. (You want the first N digits of pi? Just wait while I calculate all of them for you...)
I don't think that example cold actually happen, or at least I can't see how it would happen in Haskell.
I enjoy writing Haskell for small hobby projects, but I doubt I would choose it for real work now that F# and Rust exist.
* The ability to define control flow (structures) as abstractions instead of primitives.
* The ability to define potentially infinite data structures. This allows for more straightforward implementation of some algorithms.
* Performance increases by avoiding needless calculations, and error conditions in evaluating compound expressions."
In my several-year experience as a professional Haskell developer the first and second points are at least an order of magnitude more important than the third.
No offense, but 'performance is not as important as...' is what basically all pro-FP devs say for years, still it seems that the rest of the world thinks exactly the opposite.
It's true that there are also performance costs associated with laziness, but those weren't under discussion.
I make no claim about the importance of performance.
My claim is that the performance gains brought by lazy evaluation are far less important than the other gains it brings (because the performance gains it brings are rather small).
I'd agree that a lot of developers still over-index on performance though, probably because it's sexy and easy to measure.
To keep producing responsive compute programs as the complexity of those programs continues to rise while the single-core CPU performance remains steady, program performance must become more and more important.
Haskell also has inline-C FFI support for "type safe" performance escape hatches (in the sense that you can manually bring C constructs into Haskell's type system): https://github.com/fpco/inline-c
The only way that Haskell inescapably suffers from performance hits (that I'm aware of) is not in its laziness or lack of imperative programming support, but in its periodic GC pauses. In such cases where GC pauses are an issue, I still think you can -- in principle -- write more and more performance critical pieces of your application in a non-GC language (in fact, you could do this in Rust just as well as C/C++). You can also manually trigger a GC pass, although you cannot -- to my knowledge -- manually stop and start GC passes in your code.
This is the approach I'm currently taking in a performance critical VR application where GC pauses could absolutely be an issue (using as much Haskell as possible, with C and GC triggers as an escape hatch). And if someone could point out a flaw in my reasoning (in particular, how GC pauses might be inevitably avoided even with C FFI), I would gladly pay them money, because it's so important I not get this one wrong :).
Read about that technique from Simon Marlow of Facebook's Haskell team. Of course it would be awesome to not have to worry about that.
Strictness annotations (and associated pragmas) only change the strictness of constructors and functions defined in my modules. I still have to reason about laziness as I use almost anything from the Haskell ecosystem.
A Haskell port of a logging library I'd originally written in Python ended up being almost 2 orders of magnitude slower than CPython 3, and almost 3 orders of magnitude slower than PyPy3.
PyPy3 resulted in performance similar to a barely-optimized C port.
https://codereview.stackexchange.com/questions/157118/a-port...
As was pointed out, GHC is calling getCurrentTime too often. If you take advantage of GHC's laziness, the value will stay valid longer. I assume that is what fast-logger does.
However we live in the real world and in the real world you cannot eliminate the concept of time, even in an abstraction such as functional programming. Therefore Functional programming is an utter lie.
Reasoning about the performance of a functional program is to peer through the lie and "reintroduce" time into this abstraction. This is why it it notoriously hard for functional programs to be reasoned about. Procedural programs are much more better for this in that regard.
Everything you wrote is somewhere between 90° and 180° wrong.
I've heard this a lot but still can't find a good code example to illustrate this. Any help?
The benefits of laziness is difficult to appreciate if you are not used to it. For a great example of the benefits it brings, please read this:
http://augustss.blogspot.co.uk/2011/05/more-points-for-lazy-...
Conversely, I find it hard to achieve the same performance with strict code. I think it's likely a matter of familiarity.
>Haskell is notorious in my mind for being quite easy to write obscenely slow code in.
The standard prelude is flawed in many ways. Haskell 2020 will hopefully fix this. In the meantime: the recursion-schemes library might be of great use.
for (int i = 0; i < N; i++) {
a[i] = 2*b[i];
}
for (int i = 0; i < N; i++) {
c[i] = 2*d[i];
}
into for (int i = 0; i < N; i+=2) {
a[i] = 2*b[i];
a[i+1] = 2*b[i+1];
c[i] = 2*d[i];
c[i+1] = 2*d[i+1];
}
It seems like GHC ought to similarly be able to turn some foo :: [Int] -> [Int]
into foo :: [(Int, Int)] -> [(Int, Int)]
under the covers for a similar speedup as long as it can, as with the C compiler, handle the case of an odd length with a bit of extra code.There are limits to how much you can restructure a list that's exported outside the compilation unit (my Haskell knowledge is fuzzy here so that's probably not the right terminology). And if the list is being re-stitched in odd ways it might be a bad idea. But as far as I can tell it ought to be doable and useful in most common cases. Of course maybe it's already doing this in those cases where it can prove that it works and that's why Haskell performance generally is good but fragile.
for (int i = 0; i < N; i++)
a[i] = 2 * b[i];
for (int i = 0; i < N; i++)
c[i] = 2 * a[i] + b[i];
t = 0;
for (int i = 0; i < N; i++)
if (b[i] % 2 == 0) t += c[i];
Would be rewritten to the equivalent of: t = 0;
for (int i = 0; i < N; i++)
if (b[i] % 2 == 0) t += 5 * b[i];
Lots more info on the GHC optimizations page: https://wiki.haskell.org/GHC_optimisations#FusionIn regards to assembling the stream: when the list is actually materialized, I assume it's done one element at a time, but the LLVM backend that GHC uses might be doing some loop unrolling, but I'm not sure.
Stream fusion doesn't require that all of the streams be the same type, though! Elements at different points in the stream can have wildly different sizes. For example, this would be fused:
sum . map length . map double $ ["hello", "Symmetry"]
where double x = concat [x x]
That will never materialize a list of Strings, even though at two points in the stream you had a stream of strings. foldr z f (build g) = g f z
Foldr is the usual catamorphism for the inductive list type, and build is an "abstracted constructor", of sorts: build :: (forall b. (a -> b -> b) -> b -> b) -> [a]
build f = f (:) []
Note that build's argument is rank-2 polymorphic, and also the same as foldr's type.If you do not need to write notoriously slow code in Haskell, just don't do it. Use libraries (containers and mtl especially great), keep state strict (a handful of bang patterns).
I use Haskell's laziness with great success (look for paper "generating power of lazy semantics" it's fascinating; look "the essence of functional programming" for even more fascination). I use Haskell's flexibility with great success (how about beating C speed ten times? how about parallelization to all cores with couple lines of code?). It is really great language.
BTW, Haskell's RTS reuse chunk type field which is needed for implementation of laziness for synchronization and it is great! This means that it can provide everything that Java and/or .Net provides and get away with twice as less overhead for it.
Haven't sunk a lot of time into it yet to be honest, I think the biggest beneficiaries will be streaming libraries and non-gced data structures like long lived queues?
Pre 1.0: every day
Post 1.0: not really, though I guess it depends on your definition of "large scale"; we've made some soundness fixes, but made sure that they were warnings for a long time first, so most people experienced no breakage.
This is because, believe it or not, Haskell is actually used in the real world and people care about their code not becoming broken.
Am upvote hoping you don't get buried.
It may get some of the facts right, but the tone is completely wrong. Cynicism is just a step or two away from nihilism. It's far, far easier to destroy than it is to create. The world needs more creators, not destroyers.
Totally off topic:
Such a simple thing to say, but I thank you for it. I often want to do good, and spent time thinking about the problems in the world and how my path might help them, but I know that I am a massive cynic in many areas. Politics being a big one.
I have always agreed with your statement about creators vs destroyers, but nevertheless I've been a loud cynic. I need to reign that in. Thank you.
Cynicism is a self-defense mechanism.
Case in point, TypeScript and mypy. Both are adding more abstractions, more powerful types, more aspects, because people find/have fitting use cases for those features.
That doesn't mean let blah = document.getElementById("blah") ; blah.innerHTML = "..." or import requests ; requests.post(...) will become deprecated, but it means it'll be easier to express more things (like conditions/assumptions on shape or state of data, control flow, component dependencies) without boilerplate (and copy-paste), and maybe helping other developers make sure they cross all the t-s and dot all the i-s.
Does this have a cognitive cost? Yes, especially in Rust/Haskell.
Are there escape hatches? Yes, of course, like unsafe and whatever Haskell has.
Should a developer who isn't familiar with those assumptions/conditions work on the code base anyway? No, not really.
(Is Haskell ugly? Yes. :( )
https://www.rust-lang.org/en-US/friends.html has been growing at a pretty hefty pace lately!
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 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.
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 safer bet, even if it lacks many nice things. The sum is probably quite a lot better still.
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.any : (a -> Bool) -> Lazy List a -> Bool
... which would make it as performant and clearer than Haskell's any, since the laziness is explicit.
Someone with actual Idris experience might tell if it actually would work like this :) I guess we'd have to make sure that the used or function is also lazy.
Out of curiosity, would you know whether there exists sufficient strictness pragmas in Haskell to translate your example Idris (pseudo) code into Haskell? I often hear about performance/memory usage pitfalls with Haskell laziness, so it'd be really nice if Haskell were able to emulate Idris in this regards, by enabling enough LANGUAGE pragmas to require explicitly annotating types if laziness is desired.
Indeed. Some people actually use Haskell, though. They even claim it's fun and productive, but having done so myself, I'm forced to be undecided on whether they're just suffering from Stockholm Syndrome.
I was somewhat disappointed not to have a followup to what work-related reasons there might be. Note also that the About page says "work at Facebook".
The inverse was not quite as true. But I still love Rust. Any time I need something performant, or I just want to play around, it's my first choice.
Interesting reading but the author seems intent on pushing some buttons.
Instead of returning self, you could just pass a reference - you need to mutate self then just pass a mutable reference. Did I miss something here?
Returning a trait object instead of plain Self would work though.
Well, Java allows it, even if it looks a bit unglier than the Rust example.
<T extends Comparable<T>> T min (T a, T b) {
if (a.compareTo(b) > 0) { return b; } else { return a; }
}
And even without general availability of concepts (already in gcc 6.x), one can achieve it in C++ via if constrexpr.In any case, regardless of the fine grain details, all of them allow for to "specify the generic constraints explicitly.".
public interface Eq<A>
{
bool Equals(A x, A y);
}
public interface Ord<A> : Eq<A>
{
bool GreaterThan(A x, A y);
bool GreaterThanOrEq(A x, A y);
bool LessThan(A x, A y);
bool LessThanOrEq(A x, A y);
}
public struct OrdInt : Ord<int>
{
public bool Equals(int x, int y) => x == y;
public bool GreaterThan(int x, int y) => x > y;
public bool GreaterThanOrEq(int x, int y) => x >= y;
public bool LessThan(int x, int y) => x < y;
public bool LessThanOrEq(int x, int y) => x <= y;
}
public struct OrdString : Ord<string>
{
public bool Equals(string x, string y) => x == y;
public bool GreaterThan(string x, string y) => x.CompareTo(y) > 0;
public bool GreaterThanOrEq(string x, string y) => x.CompareTo(y) >= 0;
public bool LessThan(string x, string y) => x.CompareTo(y) < 0;
public bool LessThanOrEq(string x, string y) => x.CompareTo(y) <= 0;
}
public static class Testing
{
public static void Test()
{
var x = new[] { 3, 8, 1, 2, 10 };
var y = new[] { "mary", "had", "a", "little", "lamb" };
BubbleSort<OrdInt, int>(x);
BubbleSort<OrdString, string>(y);
}
public static void BubbleSort<OrdA, A>(A[] values) where OrdA : struct, Ord<A>
{
bool swap;
do
{
swap = false;
for (var i = 0; i < values.Length - 1; i++)
{
var x = values[i];
var y = values[i + 1];
if (default(OrdA).GreaterThan(x, y)) // Ad-hoc polymorphic call to GreaterThan
{
swap = true;
values[i] = y;
values[i + 1] = x;
}
}
}
while (swap);
}
}This is exactly how traits, implicits, type-classes, class-instances, and concepts work in other languages like Scala, Haskell, etc.
I suspect that it is closer to the truth to say that most large codebases are terrible, regardless of language, and that HKT won't save you. (Using HKT may help, some. Using HTK well may help more. But the problem with large codebases is that enough programmers work on it that the talent level tends toward the average of the universe of programmers. That's... not good. It means that whatever tool or technique you choose won't be used well.)
Rust is closer to Alan Turing than it is to Alonzo Church.
The code in this other post about implementing string distance metrics is also quite imperative: https://markkarpov.com/post/migrating-text-metrics.html
Mutable arrays: https://hackage.haskell.org/package/vector-0.12.0.1/docs/Dat...
Strict by default: Put {#- Language Strict #-} at the top of files where necessary but usually you'd just use bangpatterns and unbox-strict-fields.
Which is intentional, of course, but quite opposite to the imperative mindset.
About {#- Language Strict #-}, thank you - I was not aware of it.
I think the first example in this post https://markkarpov.com/post/migrating-text-metrics.html makes it clear that, while one can program in imperative style, it is not at all natural, and it is going to introduce as much boilerplate as, say, trying to program in functional style in Java 6
I think the first example in that post is a bit unfair. The Haskell code is unusually complicated because it avoids a complicated problem that exists in the C code. (Which is that 16-bit encodings can't cover all of UCS-4 with fixed character widths.)
Besides, I don't understand why they are making such a complicated solution. In this comparison [1], the first example in that post is the "target". The best performing solution is... you guessed it:
foldZip a b =
List.foldl' (\r (cha, chb) -> if cha /= chb then r+1 else r) 0 (Text.zip a b)
And a pet peeve: the way you say "only in the ST monad" is a funny rhetoric device. You make it sound like everyone agrees it's a bad thing to isolate mutation to local regions. Here's the thing: it's not. And the way you can integrate local mutation in your application with the ST monad is fantastic.By just typing "ST" a few extra times you get local mutation exactly where you want to, and nowhere you don't want it!
foldZip a b
| T.length a /= T.length b = Nothing
| otherwise = foldl' step 0 (T.zip a b)
where step acc (l, r) = if l == r then acc+1 else acc
This is slightly awkward because T.length is an O(n) operation, though. I think the optimal implementation would be a hyper optimized variant that depends on correct byte alignment and this as slow fallback method if that fails.What is fast – without reaching into the Text object internal representation – is good old tail-call recursion. And you can even integrate the length check into the traversal! Making it an O(min(n,m)) operation in total.
Ah, but there is! Recursion schemes.
>Usually an imperative algorithm will be much clearer in an imperative language than in Haskell, and easier to get right on the first try.
Not sure what an "imperative algorithm" is, but I agree there are tasks better suited to imperative languages.
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 memorize." Sure, a valid complaint if you've been forced to only use goto in your life, but not a reflection on good programming practise.
import qualified Data.Vector.Mutable as V
import Control.Monad (when)
import qualified System.Random as R
main = do
v <- V.replicate 10 0
putStrLn "Initialised to zero"
printThemAll v
setIncreasing v
putStrLn "Set with values increasing by 2"
printThemAll v
addRandomness v
putStrLn "Added a random number to each"
printThemAll v
printThemAll v = loop 0 (V.length v)
where loop i n = do
when (i < n) $ do
e <- V.read v i
print e
loop (i + 1) n
setIncreasing v = loop 0 (V.length v)
where loop i n = do
when (i < n) $ do
V.write v i (i * 2)
loop (i + 1) n
addRandomness v = loop 0 (V.length v)
where loop i n = do
when (i < n) $ do
r <- R.randomIO
e <- V.read v i
V.write v i (e + r)
loop (i + 1) n 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 <- V.replicate 10 0
setIncreasing v
addRandomness v
printThemAll v = loop 0 (V.length v)
where loop i n = do
when (i < n) $ do
e <- V.read v i
print e
loop (i + 1) n
setIncreasing v = loop 0 (V.length v)
where loop i n = do
when (i < n) $ do
V.write v i (i * 2)
loop (i + 1) n
addRandomness v = loop initial_g 0 (V.length v)
where seed = 1234
initial_g = R.mkStdGen 1234 -- Seed
loop g i n = do
when (i < n) $ do
let (r, next_g) = R.random g
e <- V.read v i
V.write v i (e + r)
loop next_g (i + 1) n----
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 < hi then
p := partition(A, lo, hi)
quicksort(A, lo, p – 1)
quicksort(A, p + 1, hi)
It's Haskell implementation will be very similar: quicksort vec = runST $ do
mvec <- thaw vec
let loop lo hi = when (lo < hi) $ do
p <- partition mvec lo hi
loop lo (p-1)
loop (p+1) hi
loop 0 (Vector.length mvec - 1)
freeze 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 <- thaw vec
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 < hi) $ do
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 <- partition mvec lo hi
loop lo (p-1)
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)
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 <- Vector.read a hi
i' <- newSTRef (lo - 1)
j' <- newSTRef lo
fix $ \loop -> do
j <- readSTRef j'
when (j < hi) $ do
aj <- Vector.read a j
when (aj <= pivot) $ do
i <- inc i'
when (i /= j) $
Vector.swap a i j
inc j'
loop
i <- inc i'
Vector.swap a i hi
pure i
Again, I'll walk through it part by part. partition a lo hi = do
pivot <- Vector.read a hi
i' <- newSTRef (lo - 1)
j' <- newSTRef lo
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 <- readSTRef j'
when (j < hi) $ do
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 <- Vector.read a j
when (aj <= pivot) $ do
i <- inc i'
when (i /= j) $
Vector.swap a i j
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 <- inc i'
Vector.swap a i hi
pure 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 <- state (randomR (lo, hi))
lift $ do
Vector.swap a hi p
-- pivot <- Vector.read a hi
-- ...
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 <- Vector.read a hi
-- ...
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.
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.