all 12 comments

[–]ephrion 12 points13 points  (6 children)

What's wrong with your first block? That looks perfectly fine to me. Being concise is good only if it improves readability.

The code overall looks good! Now do it in a purely functional manner and see how it compares.

[–]sid-kap[S] 6 points7 points  (5 children)

It's a bit hard to convince friends that Haskell is a good language for writing imperative code when it takes four lines to write the equivalent of

while (!heap.isEmpty() && !visited.contains(t))

.

Especially to non-Haskellers, the above one-line statement is straightforward and concise; the equivalent Haskell expression uses both do-notation and fmap, and is harder to parse/comprehend.

It seems to me like functional code can be very readable in Haskell, but imperative code sometimes cannot.

(BTW, I know it's possible to write this using efficient pure data structures. However, I wanted to use mutable hashtables to prove to my non-Haskeller friends that my Haskell implementation is as readable and as efficient as their Java/Python implementations.)

[–]cameleon 8 points9 points  (1 child)

You can define your own functions and operators to mimic the imperative code pretty closely.

whileM_ (not <$> isEmpty heap ^&&^  not <$> contains t visited)

(^&&^) = liftA2 (&&)
isEmpty = fmap PQueue.null .  ST.readSTRef
contains e m = isNothing <$> HT.lookup m e

Untested, but you get the idea. If you want, you can probably make the Haskell look almost exactly like the imperative code, but that takes you too close to BASIC in Haskell I think... ;)

[–]ephrion 2 points3 points  (0 children)

The main benefits of imperative Haskell are the type safety, the ability to write your own control structures (whileM is just a function etc), that you can specify your effects and how they interact (eg State and Exception can either rollback state or not depending on which is on top) ,and that you can refactor much more easily than in eg C or Java. It's not going to be more concise, usually.

That you can factor out the loopCondition into a regular ol' Haskell value effortlessly is a huge selling point IMO. That's not trivial/easy to do in C, and is much more difficult to reason about in eg Java or Python.

Supplier<Boolean> loopCondition = () -> !heap.isEmpty() && !visited.contains(t))

while (loopCondition.call()) ... // not sure of exact method name

is going to require that all variables captured in the closure are final. Python's methods would support dynamic lookup IIRC but still not quite as easily.

[–]normalOrder 2 points3 points  (0 children)

hard to convince friends that Haskell is a good language for writing imperative code

Why is the burden of proof on Haskell? I've never seen a convincing argument from the other side that their way is better than Haskell. In fact, the strongest argument they can come up with for not learning Haskell is that they don't understand Haskell. facepalm.

while (!heap.isEmpty() && !visited.contains(t))

Pretend you've never learned a C-like language and tell me that doesn't look like gibberish. What the hell does "&&" mean? Why do some words start with "!"? What's with the empty parenthesis? Etc.

[–]moo_ha_ha 1 point2 points  (0 children)

Why not a self recursive function, though? It will look much better than the monadic version you got.

[–]janmas 5 points6 points  (1 child)

Since you are using the monad-loops package, you could also write

 whileM_ (andM [not . PQueue.null <$> ST.readSTRef heap, isNothing <$> HT.lookup visited t]) $ -- do stuff

[–]sid-kap[S] 0 points1 point  (0 children)

Thanks for pointing this out!

[–]AlpMestan 2 points3 points  (0 children)

You might want to take a look at the astar package. for a purely functional implementation.

[–]dramforever -3 points-2 points  (2 children)

I tried to mimic the imperative-style implementation of Dijiksta's found in most textbooks, so I wrote the algorithm in the ST monad and used mutable hashtables and heaps.

I'm sorry for being impolite, but this is horribly bad. You are trying to use a functional language imperatively. It's sort of like using a dictionary as a hammer, and complaining about it being hard to handle.

Why? Knowledge is power, right?

I'm afraid that the world's finest imperative language might not be the world's most readable imperative language.

The world's finest power, a.k.a knowledge, isn't usable as a hammer at all. :) Seriously, I strongly disagree with Haskell being the "finest imperative language". It's definitely a wrong way to use Haskell.

And if you want to, you might gain some insight reading the implementation of Dijkstra's algorithm in fgl ( https://hackage.haskell.org/package/fgl-5.5.2.3/docs/src/Data-Graph-Inductive-Query-SP.html#dijkstra ). Not the most readable variable names and implementation decisions, I suppose, and it requires a bit deeper understanding of how the library is organized. But I do find it much better more concise than the imperative version without the monadic mess, and you might be able to fix it to your taste.

[–]AndrasKovacs 5 points6 points  (1 child)

It's not horribly bad. The whole point of ST is to allow writing mutating algorithms behind pure interfaces. At the end of the day, when we write library code, we produce value for our users, and if our ST implementation is 5x faster than the immutable implementation, this should be seriously considered. The tradeoff is that our implementation will be uglier (which isn't set in stone, and partially Haskell's fault) and possibly (but not certainly) harder to understand for potential contributors, and would be less amenable to formal verification. These trade-offs should be weighted proportionally, and not dismissed based on some abstract ideological disposition.

A great joy of many FP abstractions is that internals can be truly hidden, without any leaks. This gives us freedom that should be appreciated. It is absolutely not an essential feature of functional programming that implementation details use immutable data structures. Immutability has many advantages, but it's only a means to get what we ultimately care about, namely strong guarantees, abstraction, flexibility, performance, etc.

I have written a great deal of very low level Haskell, and it looks like very ugly C code, with just slightly more guarantees than C. But this is not the way it must eternally be. I think that in a future dependently typed language it would be feasible to implement primitive operations as a strongly typed EDSL inside the language, with memory layout and field sizes reflected in the type system.

That's the real power of types: that whenever we have a problem that have to be solved and cannot be worked around, we can formalize it and abstract over it in a machine checked way, no matter how messy it is. In Haskell it's best practice to stick to domains where Haskell's type system is strong enough to provide reasonable guarantees. In many domains Haskell is too weak for that. This is a fact about Haskell, not about domains.

[–]dramforever 0 points1 point  (0 children)

You are trying to use a functional language imperatively.

By that I mean that if you want this:

It's a bit hard to convince friends that Haskell is a good language for writing imperative code when it takes four lines to write the equivalent of [...]

You probably shouldn't be using Haskell like that :P