Live data from Hacker News

Pain Points of Haskell

dixonary.co.uk

311–320 of 322 posts

Re: Pain Points of Haskell

#311
post #6

As someone who stopped following Haskell world few years ago, this was quite interesting read. I wonder what is the state of ecosystem (libraries) now: a few years ago one of my friends complained that for most basic tasks there is some library but usually half-working and abandoned. However: I think the point about monads is fundamentally misguided. Monad is an abstract concept and trying to explain it in non-techni…

I fully grok monads. I’ve done enough reading and usage of them to understand them. They are not a useful abstraction for programming. They’re mathematically correct, but that’s not the same as what an industrial engineer needs. The big warning sign is monad transformers. Alone, monads are totally fine, but the issue is that you rarely want one . So you end up with this unwieldy tower of transformers that would make…

This isn't true at all, promises in JavaScript are monadic, as are chains of conditional logic. Stuff like this is literally everywhere:

if a: b = f(a) if b: c = g(b) ... else: p()

Which as a monad would be something like

A >>= f >>= g >>= h >>= ...

I've built several classes with a covert "return" and "bind" operation to keep this kind of programming clean, I just don't tell anyone it's a monad.

Re: Pain Points of Haskell

#312
post #282

Earlier quoted context omitted.

I fully grok monads. I’ve done enough reading and usage of them to understand them. They are not a useful abstraction for programming. They’re mathematically correct, but that’s not the same as what an industrial engineer needs. The big warning sign is monad transformers. Alone, monads are totally fine, but the issue is that you rarely want one . So you end up with this unwieldy tower of transformers that would make…

As an industrial engineer, monads are the best solution I've ever found to the cross-cutting concern problem. I need to be able to put custom cross-cutting concerns on my functions - things like must-happen-in-database-transaction, record-in-audit-log, must-have-this-authorisation-level, record-these-statistics. If your language doesn't have monads, you end up using reflective proxies, metaclasses, decorators, byteco…

Absolutely. It's worth noting that every single effect system implementation that currently exists also leverages monads at the user level, even if they're doing exciting type-theoretic things underneath.

Re: Pain Points of Haskell

#313
post #295

Earlier quoted context omitted.

It is definitely not harder to reason about time. I don’t think you will find many haskellers agreeing with this.

Lazy evaluation makes it difficult to figure out when expressions are evaluated, which is relevant to both space and time complexity. To take an trivial example, whether or not your infinite list is (completely) evaluated could be the difference between your program running in constant time or not terminating at all! Here is a good Stackoverflow response that expands on the point: https://stackoverflow.com/a/12061524…

Yeah, I understand what you mean, sorry if I created confusion. My point is that the claim that it makes it difficult to reason about is misguided in most cases because there is an easy way to estimate the upper bound of any function if you know the upper bounds of the same algorithms in a strict language.

Generally speaking, the complexity of any function has an upper bound equal to the most complex algorithm used. Laziness can bring the upper bound for complexity down, though. The fact that upper bound may be lower is almost certainly not something that you care about in the overwhelming majority of programs. And I haven't found the first person upset to find out that their algorithm performed much better than estimated.

If you have an algorithm with O(n) complexity, it will be the same both for a strict and non-strict language if the whole result is consumed.

When the whole result is not entirely consumed, then the upper bound for a lazy language may be lower.

The classic example, as you linked to is this:

    minimum = head . sort
If we consume the entire result of the sort function, then the upper bound is O(Nlog(N)). Because of laziness, using head makes the minimum function O(N)

The implementor chose to compose those two functions because laziness makes this possible. If you are in the business of reviewing core code to evaluate their complexity, you are right. It is difficult to nail down get the more accurate upper bound, but that does not mean you cannot guess what a less accurate estimate would be. I would have guessed O(Nlog(N)) the first time for a minimum, and would have been pleasantly surprised that laziness makes that faster.

When has reasoning about time complexity been difficult for you in a lazy language?

Re: Pain Points of Haskell

#314

Earlier quoted context omitted.

> The only members of the ML family that I'm aware of are Ocaml and the flavours of Standard ML. Only Ocaml is fit for production. F# is also a member of the ML family and is totally fit for production.

F# is the language I want to like, and would like to use in prod, but it constantly feels like the ignored step-child. The REPL is great in theory, but trying to import packages is a nightmare every time I try. Dependencies exist, but they all seem to target wide and inconsistent range of the ecosystem. Want to target dotnetcore 3.1? Good luck have fun: everything you find useful is targeting some combination of stan…

Most of those package issues also apply to C# particularly as it stabilizes towards .NET 5. However it isn't too bad once you get it; and for most standard use cases it seems to work with the dotnet cli out of the box. Many people seem to be productive with C# so I'm not sure how big of an issue it is other than the small initial learning curve but many package managers have that (IMO Maven is harder yet many people use that in Java space and not too hard once you learn it).

REPL's often have problems with package management in a number of languages as they often have a different build chain from the compiled style apps which have the chance to resolve packages. When doing .NET Core in the past I know Paket can generate these scripts for the REPL to import packages (seen csx/fsx scripts for each package) - but another comment alludes to a more supported way in the future which is good.

Doesn't seem like you have a problem with the language per se; rather the package management story for .NET Core.

Re: Pain Points of Haskell

#315
post #295

Earlier quoted context omitted.

Lazy evaluation makes it difficult to figure out when expressions are evaluated, which is relevant to both space and time complexity. To take an trivial example, whether or not your infinite list is (completely) evaluated could be the difference between your program running in constant time or not terminating at all! Here is a good Stackoverflow response that expands on the point: https://stackoverflow.com/a/12061524…

Yeah, I understand what you mean, sorry if I created confusion. My point is that the claim that it makes it difficult to reason about is misguided in most cases because there is an easy way to estimate the upper bound of any function if you know the upper bounds of the same algorithms in a strict language. Generally speaking, the complexity of any function has an upper bound equal to the most complex algorithm used.…

I misspoke in my last comment when I referred to space and time complexity. The usual problem in the field isn't figuring out the big O characteristics, it's unpredictable performance due to evaluation of an expression building up a huge chain of thunks. Because of the inherently non-local logic of lazy evaluation (or, whatever evaluation strategy compatible with non-strict semantics that GHC actually chooses to use), it is not always easy to find which piece of code is introducing unwanted laziness.

That said, it's the same fundamental non-locality that also makes it more difficult to figure out the big O complexity of an algorithm implemented in a non-strict language.

Re: Pain Points of Haskell

#316
post #196

Earlier quoted context omitted.

I'm sure that very smart people have risen to the challenge of supporting strings in Haskell, and it is therefore possible for a sufficiently motivated user to process text decently after all. But the problem is that strings must be a built-in feature in any programming language that wants to be taken seriously (with an exemption for specialized ones that don't need strings, like GLSL). What would you think of a hote…

Huh, the problem isn't that people have made their own. The string types are all in the standard "boot" packages. The problem is that there is one that is obsolete from another era (String) and four usable ones, each combination of strict/lazy and Unicode/Bytes. Haskell gets twice as many as Python (say) because of the desire to have strict and lazy versions.

Languages are judged by their standard library, and standard Haskell strings are part of the problem and not part of the solution.

Multiple moderately bad string types, with strict typing that exacerbates interoperability problems, are worse than one really bad string type (like char pointers in C) or nothing at all.

Re: Pain Points of Haskell

#317
post #196

Earlier quoted context omitted.

Huh, the problem isn't that people have made their own. The string types are all in the standard "boot" packages. The problem is that there is one that is obsolete from another era (String) and four usable ones, each combination of strict/lazy and Unicode/Bytes. Haskell gets twice as many as Python (say) because of the desire to have strict and lazy versions.

Languages are judged by their standard library, and standard Haskell strings are part of the problem and not part of the solution. Multiple moderately bad string types, with strict typing that exacerbates interoperability problems, are worse than one really bad string type (like char pointers in C) or nothing at all.

Can it be worse than Microsoft's hatful of incompatible strings? You change the default string type, you have to change code. Gah.

Re: Pain Points of Haskell

#318
I would add a debugger to this list.

The one built into GHCi is the only working one I know of, and it has major failings (such as telling me that variables aren't in scope when they clearly are).

Re: Pain Points of Haskell

#319

Earlier quoted context omitted.

> I can say I don’t understand the last rant paragraph at all or why you think it’s related to my point about generators. This is not a rant, you made a claim about generators' allocations inefficiencies compared to physical list() and tuple() allocations, I'd like to see the case where it's true.

It is true in many many cases. For a very simple illustration consider def f(): x = list(range(10000)) yield from x vs list(range(10000)) The former includes the overhead of memory for both the materialized list and function execution frames. The same thing happens when people naively split up generators that hold onto a lot of data, for example (this comes from real life experience where someone wanted to essentiall…

> You don’t seem open to consider what I am saying, rather in a rush to demand some kind of “proof” with no willingness to think through it

it doesn't work this way, if you state something to be true, the burden of proof lies on your shoulders and I am open to consider the proof, and not just words alone.

It's funny that this subthread was started with a statement that my example of complexity estimation difficulties was deeply contrived, yet the examples for a proof of generators' inefficiences materialize all intermediate values into lists. Why would I want to materialize a list to yield something from it later, if I can just pass the range() stream to the caller instead?

Your second example is an exemplary code smell, as there's no need for a helper() closure. And there's no need to hold resources for check_expensive_in(), as most of DBs have lazy query cursor implementations in one way or the other. Afterall, it's not about doing everything in one expensive DB call and laziness doesn't negate buffered data, it just says that there's an explicit boundary on internal buffers within a single iteration, and every single iteration may perform a separate but less expensive DB call.

Re: Pain Points of Haskell

#320
post #305

Earlier quoted context omitted.

I agree, although the arguments in that article aren't the strongest. In particular: The main reason given for 'String' being slow is that it's immutable and hence leads to many duplicate Strings being created. In fact, the main reason Strings are slow is that their linked-list structure has a lot of overhead, and may be scattered around in memory. For example, the String "abc" will be represented something like this…

> In fact, the main reason Strings are slow is that their linked-list structure has a lot of overhead, and may be scattered around in memory. This is almost entirely a myth in the context of Garbage Collected languages. Most popular GC'd languages use a copying and compacting garbage collector. They should follow the pointers when copying and put the list together in one place. Furthermore, if the compiler were to ma…

You're right that there are higher-order effects at play, but I was careful to hedge my bets (e.g. the sentence you quoted says "may be").

Besides a GC re-arranging/compacting things, the elephant in the room for Haskell is laziness, which introduces another level of indirection. I tactfully avoided this by limiting what I said to "fully evaluated" strings.

Lazy values start out as "thunks" (effectively zero-argument closures); if their value is needed (e.g. if we're branching on it), the thunk is evaluated to "weak head normal form" (i.e. until we reach a constructor we can branch on); the thunk in memory is replaced by this evaluated version, so it won't need to be re-evaluated if it's accessed again.

There are a few ways this can complicate things: thunks can prevent values from being garbage collected; layers upon layers of thunks can cause "space leaks" (where the order we evaluate things is correct, but inefficient; not to be confused with "memory leaks", which is where incorrect memory usage prevents it ever being freed); evaluating in order of demand can jump around inside different values, causing their allocations to be interleaved, and hence fragmenting their memory usage (until a compacting GC is able to defragment them).

As an extra complication, Haskell compilers like GHC will perform "strictness analysis" to find values whose evaluation is inevitable, and hence can be evaluated immediately rather than making a thunk. Strictness annotations can also be added to the code.

As far as types and boxing goes, GHC erases types at compile time, so values aren't technically boxed. However, most values will have several levels of nested structure, due to the way their types are defined, which is amounts to the same thing. For example, in GHC the default 'Int' type is a wrapper around a machine integer, whose type is 'Int#', e.g.

    data Int = I# Int#
In other words an 'Int' value like '42' is not a boxed pair like '(IntType, 42)', like in Lisp or Python; instead it's just a bare, raw 'Int' value, but that value will be still involve a wrapper pointing at a machine word, like 'I# 42#'. For this reason, the #-sufficed types are known as "unboxed".

We can write our code in terms of unboxed types but, since they're always strict, it's less flexible than using the normal "boxed" types (e.g. many common library functions won't work for boxed types). Hence we usually rely on the compiler to unbox values for us.

In practice, the biggest effect on space and time usage will be whether or not fusion is triggered. After that, I'd guess it's the difference between ByteString's fiddling-with-indices/chunks versus String's single-character-at-a-time rearrangement of pointers (even if they're reasonably contiguous).

Regarding the pattern/implementation distinction, I also agree. I was originally going to say something about "view patterns", which we can use to pattern-match on ByteStrings as if they were linked-lists. AFAIK Clojure does this too, using arrays of a reasonable length to implement its cons-based lists.

One tricky aspect of that is how we treat polymorphism. One of the only redeeming features of Haskell's default String type is that we can use it with any generic list functions we like; if we want to change the underlying representation of String, we would need to change the way all of our linked lists work, which might be less ideal for large element types. There are ways to avoid this, but they introduce complications; e.g. writing against "any Traversable, Foldable Functor", rather than just lists. That's more generic, but can also be more confusing (especially for newcomers)!

Post reply on HN