Earlier quoted context omitted.
the point is that it's not semantics of a language alone, it's the semantics of the language + the libraries + the data constructors + the flow order. Lazy evaluation doesn't introduce additional complexity on top of that for no good reason, it actually trades a few of these complexities for one additional abstraction that lets you think about your programs in terms of data flows and transformations (instead of alloc…
The additional complexity may or may not be justified; the point is that it exists.
Pain Points of Haskell
121–130 of 322 posts
Re: Pain Points of Haskell
#122Re: Pain Points of Haskell
#123Earlier quoted context omitted.
The additional complexity may or may not be justified; the point is that it exists.
for some reason you specifically ignore that it's not a complexity on top, but rather a trade-in.
Re: Pain Points of Haskell
#124As a guy who casually (definitely not in-depth) reviewed working with Haskell about 2 years ago I'd say the most relatable part of the article to me is: complexity in tooling and unnecessary tension when working with libraries. Also the String situation (several types of them). A lot of the other points could be addressed but the community's seeming unwillingness to tackle everyday productivity sends to me the messag…
It is in your interest to have multiple string-like datatypes in a lazy language, especially when some of them are not real strings, but rather streams of binary data. https://mmhaskell.com/blog/2017/5/15/untangling-haskells-str...
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:
+---+---+ +---+---+ +---+---+
| * | *---->| * | *---->| * | *---->[]
+-|-+---+ +-|-+---+ +-|-+---+
| | |
V V V
'a' 'b' 'c'
Those arrows are pointers, which can point to locations arbitrarily far away. From this we can see that just storing a (fully evaluated) String is expensive, since each character requires a pair of pointers (in addition to the unique characters themselves). Processing the contents of this String is also slow, since we need to dereference two pointers for each character (one to get the character, one to get the tail of the String). Since the data may not be contiguous in memory, this can make poor use of the CPU caches, which might otherwise speed up these dereferences.Compare this to a C-style string, which would look like this in memory:
+---+---+---+---+
| a | b | c | 0 |
+---+---+---+---+
This "packed" representation requires no long-distance dereferencing, the memory is contiguous so it will make good use of the cache, and we can process the characters directly without having to chase pointers.That article describes the ByteString type as a "list of Word8 objects", but that's a bit misleading. Firstly we often say "list" to mean a chain-of-pointers like the first diagram above, e.g. that's exactly what Haskell's built-in list type is, and Haskell's String is such a list. ByteStrings store their Word8 objects more like the second diagram, so it would be better to call them an "array of Word8 objects". Secondly, ByteStrings use a clever three-layered structure to speed up common operations:
- The raw Word8 characters are stored in arrays, like the C-style string above (but without the NUL byte at the end)
- These arrays are pointed to by values called "chunks". These are "fat pointers", i.e. they also store a length and offset value.
- A ByteString itself is a list of chunks (like the first String diagram, but instead of values like 'a', 'b' and 'c' the values are chunks).
This makes ByteString really fast, for three reasons:
- Some operations can be performed by just fiddling at the list-of-chunks level. For example, to append X and Y, the result is just a list of X's chunks followed by Y's chunks; no need to touch the underlying chunks or arrays.
- Some operations can be performed by just fiddling the length and/or offset of a chunk. For example, incrementing a chunk's offset will chop characters off the start (they're still stored in memory, but they will be ignored); decrementing a chunk's length will chop characters off the end.
- The same underlying Word8 arrays can be pointed to by many different chunks in many different ByteStrings; i.e. we rarely need to copy the actual characters around.
As an example, let's say we read the ByteString "[\"foo\", \"bar\"]" (without the backslashes), we parse it as JSON to get a list of ByteStrings ["foo", "bar"], then we concatenate that list to get the single ByteString "foobar", here's how it might look in memory:
BS--+---+ BS--+---+
"foobar": | * | *---->| * | *---->[]
+-|-+---+ +-|-+---+
| |
+----+ +----------------+
| |
| List+---+ List+---+ |
["foo", "bar"]: | | * | *------->| * | *---->[] |
| +-|-+---+ +-|-+---+ |
| | | |
| | | |
| | | |
| V V |
| BS--+---+ BS--+---+ |
"foo" and "bar": | | * | *---->[] | * | *---->[] |
| +-|-+---+ +-|-+---+ |
| | | |
| | +---+ |
| | | |
V V V V
Chunk---+---+ Chunk---+---+
(chunks) | 3 | 2 | * | | 3 | 9 | * |
+---+---+-|-+ +---+---+-|-+
| |
BS--+---+ | |
Input: | * | *---->[] | |
+-|-+---+ | |
| | |
V | |
Chunk+---+---+ | |
(chunk) | 14 | 0 | * | | |
+----+---+-|-+ | |
| | |
| | |
| | |
V V V
Array---+---+---+---+---+---+---+---+---+---+---+---+---+
(array) | [ | " | f | o | o | " | , | | " | b | a | r | " | ] |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
(Note that the exact position where arrows "land" doesn't matter; they're pointing to the entire datastructure they "hit")Here we can see that there's only one copy of the underlying text; that the substrings 'foo' and 'bar' are simply chunks with length 3, offset by appropriate amounts; and that the resulting 'foobar' ByteString is just a list of these two chunks.
This approach of "store things once, then do all processing with indices" can be very fast (even faster than C in some cases https://chrisdone.com/posts/fast-haskell-c-parsing-xml ). Whilst we can obviously write this sort of algorithm in any language, re-using arrays and chunks like this relies on them being immutable, which is more idiomatic in Haskell. In particular:
- Languages which tend to use mutation aren't well suited to this, since mutating one value can have unforseen consequences to those which are re-using the same components. Copying is safer and more predictable in the face of mutation.
- Languages which favour some built-in interface rather than high-level functions may need to copy values in order to comply with these interfaces. In particular, C's array syntax will work for arrays and chunks, but not for the overall ByteString structure (a list of chunks).
- If we want NUL-terminated arrays, we'll need to make copies when truncating strings, to avoid the NUL byte overwriting the truncated part.
- Re-using values can make it hard to keep track of which parts can be freed. Garbage collection (and other approaches, like linear types, borrow checking, etc.) can make this easier.
The difference between lazy and strict ByteStrings is just whether the overall list-of-chunks is lazy (chunks generated on-demand, useful for streaming) or strict (chunks are generated up-front, closer to using one big array, hence potentially faster and more predictable).
The Text type is just a ByteString paired with a particular encoding method, e.g. 'UTF-8'.
The article you link also talks about fusion, claiming that's why Text (and hence ByteString) is faster, or avoids intermediate allocations. Fusion is great, but care must be taken if we're going to rely on it; e.g. very minor changes to an expression (say, to make debugging easier) can stop things from fusing, greatly changing the compiled code's speed and memory usage.
On the subject of fusion, it's worth noting that Haskell's built-in list type is also subject to fusion, often resulting in zero allocation (e.g. generating a list of characters, then counting them, might compile down to a single loop with a single counter variable, and no Chars/Lists/Strings in sight!). Again, it can be tricky to ensure that this happens. One approach is to test for it with something like https://hackage.haskell.org/package/inspection-testing
Re: Pain Points of Haskell
#125Earlier quoted context omitted.
A bit off topic, but is Haskell viable for production? Or is it just really intended as an experiment? Is it worthwhile to jump into Haskell now, or perhaps one should look into Idris or Agda? Any companies using Haskell where it has proven a distinct advantage? Ocaml and SML (the former being sadly quite unpopular these days) are an order of magnitude simpler than Haskell. They are easy to master. I have been quite…
> A bit off topic, but is Haskell viable for production? Or is it just really intended as an experiment? It's viable for production and there are businesses which use it, notably fintechs. Also Facebook for some things (Facebook employs Simon Marlow [1]). And I know this is an irrelevant anecdote, but I have a friend who has been working in several startups which use Haskell for the last few years. Like Paul Graham h…
That's exactly my question. I'd be really interested in hearing about domains where Haskell is a secret weapon and how it compares to ML family languages and dependently-typed ones.
Re: Pain Points of Haskell
#126Earlier quoted context omitted.
> OCaml's is also excellent but not immediately obvious (their docs have improved a lot) Interesting! The last time I tried OPAM, it actually seemed more frustrating than Cabal! Maybe it's improved? Last time I tried OPAM it would install packages globally by default, and avoiding that was a confusing process, when that should be the default behavior. Cargo (while not perfect) has really nice defaults out of the box…
A bit off topic, but is Haskell viable for production? Or is it just really intended as an experiment? Is it worthwhile to jump into Haskell now, or perhaps one should look into Idris or Agda? Any companies using Haskell where it has proven a distinct advantage? Ocaml and SML (the former being sadly quite unpopular these days) are an order of magnitude simpler than Haskell. They are easy to master. I have been quite…
Re: Pain Points of Haskell
#127The compilation time is the dealbreaker for me right there. If your language is slow to compile, it cannot be that good.
> If your language is slow to compile, it cannot be that good. I know compilation times are a problem, but this seems untrue. Compile times are a problem of the compiler and not the language. GHC is slow because of how much it is doing behind the scenes. Other, more streamlined compilers may be faster.
This is definitely untrue. Language design has a huge effect on how easy (or even possible) it is to write a compiler that scales well.
You can quite easily design a language where compilation is NP-complete...
Re: Pain Points of Haskell
#128Earlier quoted context omitted.
It is in your interest to have multiple string-like datatypes in a lazy language, especially when some of them are not real strings, but rather streams of binary data. https://mmhaskell.com/blog/2017/5/15/untangling-haskells-str...
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…
Re: Pain Points of Haskell
#129Earlier quoted context omitted.
> A bit off topic, but is Haskell viable for production? Or is it just really intended as an experiment? It's viable for production and there are businesses which use it, notably fintechs. Also Facebook for some things (Facebook employs Simon Marlow [1]). And I know this is an irrelevant anecdote, but I have a friend who has been working in several startups which use Haskell for the last few years. Like Paul Graham h…
> Haskell really is a "secret weapon" and people who use it for production tend to love it That's exactly my question. I'd be really interested in hearing about domains where Haskell is a secret weapon and how it compares to ML family languages and dependently-typed ones.
Like I said, I do know Haskell is used at fintechs (source: a friend whose job is precisely that).
Re: Pain Points of Haskell
#130Earlier quoted context omitted.
for some reason you specifically ignore that it's not a complexity on top, but rather a trade-in.
That's irrelevant. OP simply said that lazy evaluation makes it harder to reason about time and space complexity. Everyone agrees with this, for goodness sake - even SPJ himself.
oh, then I definitely should stop thinking on my own about the things that I practice on a daily basis.