[HN Gopher] Failing in Haskell
       ___________________________________________________________________
        
       Failing in Haskell
        
       Author : zeepthee
       Score  : 107 points
       Date   : 2022-02-26 14:03 UTC (8 hours ago)
        
 (HTM) web link (jappie.me)
 (TXT) w3m dump (jappie.me)
        
       | schwurb wrote:
       | Adressing the whitespread conception "It is hard to programm in
       | Haskell because it is pure":
       | 
       | If you can write python, you can write Haskell. Don't believe me?
       | 
       | 1. Write your program completely in the IO Monad, in a huge do-
       | block
       | 
       | 2. Factor out as much pure functionality as possible (= Have as
       | little code in your big IO-programm as possible.)
       | 
       | Start at 1. and iterate 2. as many times as you please. It will
       | already be a program that prevents many traps that would bite you
       | in other langauges. Haskell knows exactly whether you are looping
       | over an array of strings or an array of chars.
       | 
       | (Why all the buzz about pureness, effects and so on? Well, with
       | Haskell you can design with a high granularity and reliability
       | what sideeffect is caused where. But you are not forced to use
       | that feature.)
       | 
       | Other tipps:
       | 
       | - Build small projects.
       | 
       | - Read as few tutorials on monads as possible. You might even get
       | by with 0.
       | 
       | - The trifecta of Haskell typeclasses are the functor,
       | applicative, monad. I would advise you to not try to understand
       | their mathematical origins, but just look up how the are used.
       | They will crop up naturally when you build even small projects
       | and then they will make sense.
        
         | zeepthee wrote:
         | > The trifecta .. mathematical origins
         | 
         | Ends up reading Leibniz and converting to Catholicism.
        
         | Kototama wrote:
         | It's hard because there are so many concepts to understand.
         | After reading one Python book you can write solid programs in
         | Python. Not so in Haskell, you would need to understand also
         | the extensions of the language which are popular and understand
         | the best practices (what to use to compose I/O and in which
         | context for example), on top of all the basics. That and
         | understand how to work with complex types in libraries: that
         | require time. That would be too much for one book.
        
         | andi999 wrote:
         | I like the idea of iterating from imperative to functional.
         | Here the devils advocate for your if you can do it in python
         | you can do it in haskell: I use quite a bit of numpy, scipy and
         | matplotlib, are there equivalent libraries for Haskell?
        
           | AnimalMuppet wrote:
           | Well... wasn't numpy, at least initially, a Python wrapper
           | around Fortran libraries? Sure, that made them accessible to
           | a bunch more people, but it wasn't some Python-only wonder.
           | Someone could probably write the same bindings for Haskell,
           | if they haven't already.
        
             | andi999 wrote:
             | Maybe some of the experts could name the haskell equivalent
             | libraries/wrappers.
        
               | matt_kantor wrote:
               | I'm certainly not an expert (have only dabbled in both
               | Haskell and Python, and never used numpy), but a web
               | search found https://pechersky.github.io/haskell-numpy-
               | docs which compares numpy to
               | https://hackage.haskell.org/package/hmatrix. I also came
               | across https://hackage.haskell.org/package/vector.
        
         | cleancoder0 wrote:
         | What Haskell did with Monads is nice, but eventually Monads are
         | just tags on what functionality the function uses.
         | 
         | That being said, I like that Nim and Koka did exactly that. You
         | just tag the functions (IO, Async, Whatever) and it works.
         | 
         | In Haskell, you need monad transformers (which have a runtime
         | costs) or whatever else was made to allow you to work with
         | multiple different effects.
        
           | siknad wrote:
           | > which have a runtime costs
           | 
           | As monad is just an interface, it doesn't necessary cause
           | runtime costs. Identity is a monad too. Effects may not
           | always require sacrificing performance, but as they can be
           | used to implement exceptions they are not just free compile
           | time annotations. Also the differences discussed there: https
           | ://www.reddit.com/r/haskell/comments/3nkv2a/why_dont_we...
        
             | DarylZero wrote:
             | Monad transformers are different from monads. Monad
             | transformers do have runtime costs, they are adding
             | indirection at runtime.
        
               | whateveracct wrote:
               | Sometimes - it's pretty cool what GHC can do
        
         | [deleted]
        
       | danidiaz wrote:
       | > If we need to compose these errors in a larger program we can
       | simply wrap previous errors in a bigger sumtype
       | 
       | This approach is being adopted in GHC itself to compose errors
       | happening at different stages of the compilation pipeline: each
       | stage has its own error type which later becomes a branch of the
       | global error type.
       | 
       | Another interesting post about errors-as-values in Haskell is
       | "The Trouble with Typed Errors":
       | https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_er...
        
         | ParetoOptimal wrote:
         | > Another interesting post about errors-as-values in Haskell is
         | "The Trouble with Typed Errors":
         | https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_er...
         | 
         | At the point of `AllErrorsEver` I usually find throwing an
         | exception make sense. That doesn't negate the use of defaulting
         | to `Either` rather than exceptions for the "leaves" of your
         | tree of code where each defines a sum type of errors at the
         | function or maybe the module level.
         | 
         | Edit: My last recommendation is basically consistent with the
         | article.
        
         | default-kramer wrote:
         | >
         | https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_er...
         | 
         | Nice. I've asked for a way to do that in the past and never
         | found a good answer, in any language! It's not exactly
         | conventional Haskell though, is it? What I really want is
         | first-class support in the language - something like checked
         | and unchecked exceptions in Java, except that if a method
         | declaration lacks a `throws` keyword then all the checked
         | exceptions are inferred by the compiler. For example, the
         | compiler might add `throws A, B, C` to a method that lacks a
         | `throws` keyword. Now if you want to assert that a certain
         | method throws a certain exception, you could write `throws A,
         | *` which means "If this method does not throw an exception of
         | type A, I want a compiler error. If this method throws
         | additional exception types, infer them as usual." Omitting the
         | asterisk (eg `throws A`) would disable the inference and thus
         | would work like a normal `throws` in real Java. You should also
         | be able to assert that a certain exception type is not thrown,
         | for example `throws * except F, G` or something like that.
        
         | codeflo wrote:
         | The GHC approach you describe is also what people do with
         | Results in Rust, and (analogously) with Java's typed
         | exceptions. The idea is that in a multi-layered program, every
         | layer exposes errors that are semantically appropriate for that
         | layer. So (to pick a silly little example) a database call
         | would expose a DatabaseError, not a raw network error if the
         | connection is interrupted. And so on until you get to the level
         | of application-level errors. I think that can work very well.
         | 
         | In the same spirit, I find the article you linked to a bit
         | silly, at least the examples they picked. Following the logic
         | above, there shouldn't even be a "HeadError" exposed anywhere
         | up the call chain. Inventing a complicated mechanism to
         | propagate the error upwards is the opposite of what you want to
         | do; you want elegant ways to handle the problem locally. Having
         | a special singleton HeadError isn't wrong, but I think "Maybe
         | a" would also be a perfectly fine return value for head (as I
         | mentioned in a sibling post, that's what Rust does): head can
         | only "fail" if the list is empty, so there is no actual
         | information in the "error" value.
        
           | fn-mote wrote:
           | > so there is no actual information in the "error" value.
           | 
           | At least as a beginner, the information about which line the
           | error occurred on would be helpful.
        
           | agentultra wrote:
           | The 'head' function is an unfortunate historical artifact and
           | not the norm these days. In practice there are libraries that
           | expose a head function that returns a value... but better
           | still, well typed programs can avoid the need for it
           | altogether: there are non-empty lists to consider in which
           | head is trivially safe to use, provided one can construct
           | such a value.
           | 
           | One error handling strategy not often employed is to prefer
           | code that is correct by construction. It can't always be done
           | but it's nice when you can do it.
           | 
           |  _update_ spelling
        
       | the_duke wrote:
       | I only used Haskell for small projects. I admire the language,
       | but I found error handling to be one of the weakest and most
       | inconsistent elements. To the point of being annoying and time
       | consuming.
       | 
       | Several popular libraries I used threw exceptions for expected
       | failures (like a non 2xx HTTP response) and required wrapping.
       | 
       | Even the standard prelude is full of partial functions.
       | (head...).
       | 
       | I saw a wild mix of Either, exceptions and custom monads all over
       | the ecosystem. So if you want to have a coherent strategy you end
       | up doing a lot of error juggling.
       | 
       | Manual errors with Either can make it very hard to figure out
       | where an error came from because they don't capture backtraces.
       | So if you don't have a very specific error for each failure point
       | you are left guessing and debugging.
        
         | maweki wrote:
         | > they generally don't capture backtraces
         | 
         | Backtraces with higher-order functions, lazyness, partial
         | applications, and all the transformations going on (SKI, CPS,
         | or whatever the GHC does), I don't think any kind of backtrace
         | would be legible.
        
           | xyzzyz wrote:
           | The transformations usually can and should be implemented in
           | a way that preserves the original call stack information.
           | However, you are right that laziness makes backtraces less
           | useful: they still are correct, but they pop up in completely
           | unexpected moment.
           | 
           | For example, you do something like "let x = f y in return (g
           | x)", where x is a (lazy) list, and g :: IO [U] -> V for some
           | types U and V. Then somewhere deep into g's callstack, 123th
           | element is accessed, which forces its computation, which
           | results in exception. You then get an error, and backtrace
           | should naturally come from function f, but in fact it
           | actually happened while executing g, and if g is missing from
           | the trace, a natural intuition from strict languages would
           | suggest that error happened before execution entered g,
           | because f is called before g, which gets its return value.
        
         | b123400 wrote:
         | I have to echo your point on inconsistency.
         | 
         | Our company uses Haskell and the Haskell team love to define
         | their own solutions which make things even more inconstant. For
         | error handling they end up using an extensible type-level-list
         | containing possible error types, embedded in an extensible
         | effect monad. We also have list, array, vector, and our own
         | collection types in the same place.
         | 
         | It feels like everyone want to make things better by
         | using/making something new, instead of making them consistent.
        
         | ParetoOptimal wrote:
         | > Manual errors with Either can make it very hard to figure out
         | where an error came from because they don't capture backtraces.
         | So if you don't have a very specific error for each failure
         | point you are left guessing and debugging.
         | 
         | Why wouldn't you have a very specific error for each failure
         | point?
         | 
         | Funnily enough, I _theoretically_ agree with your point but can
         | 't remember being bitten by it in practice for some reason.
         | 
         | Maybe you can help by giving an idea of a real world example of
         | this?
        
       | codeflo wrote:
       | I've written small stuff in Haskell a decade ago. I have a soft
       | spot for the language -- it has clearly influenced many notable
       | languages that came after it. But I also admire the patience of
       | anyone who actually manages to use it in practice, there are so
       | many little papercuts that don't get resolved, basically for a
       | decade or more. If I'm cynical, I'd say that's because little
       | practical stuff is often not worth publishing papers about.
       | 
       | Error handling was, for me, a big one. For a functional language,
       | Haskell seems very obsessed with exceptions. Even supposedly pure
       | stuff, like "head" (first element of a list) throws an exception
       | if the list is empty. You'd think Haskell would be the first
       | language to have it return a Maybe value, but no. (Rust, BTW,
       | gets functions like this right; they all return an Option.)
       | 
       | This reliance on exceptions clashes hard with the functional
       | paradigm. Exceptions are "magic": They are special additional
       | values that any type can have (so an Int can either be an actual
       | integer or an exception value), but you can't test for them or
       | handle them in any way pure code, you need IO for that. Which the
       | language makes intentionally hard to use, that's Haskell's entire
       | thing.
        
         | youerbt wrote:
         | > But I also admire the patience of anyone who actually manages
         | to use it in practice
         | 
         | Nothing you point out gets even close, in my mind, to stuff
         | like null pointers or untyped code. So I wonder what languages
         | you have in mind that require less patience.
         | 
         | > you need IO for that. Which the language makes intentionally
         | hard to use
         | 
         | Well, that is simply not true.
        
           | caente wrote:
           | >Nothing you point out gets even close, in my mind, to stuff
           | like null pointers or untyped code. So I wonder what
           | languages you have in mind that require less patience.
           | 
           | You two are talking about two different things: - The parent
           | is talking about the ecosystem, how menial tasks have tooling
           | in "less interesting" languages - You are talking about the
           | language itself
           | 
           | I would venture to guess that the parent would agree with
           | you, if talking about the language in a vacuum.
           | 
           | An interesting competition would be to develop a complex
           | product, without external dependencies.
           | 
           | My sad guess is that languages that are filled with escape
           | hatches, like Java, Javascript, or python, would defeat more
           | strict languages.
           | 
           | It's a sad guess, because I actually do prefer the Haskell
           | way.
        
           | [deleted]
        
         | PragmaticPulp wrote:
         | > But I also admire the patience of anyone who actually manages
         | to use it in practice, there are so many little papercuts that
         | don't get resolved, basically for a decade or more. I
         | 
         | Great summary of what it's like to use any niche language. You
         | don't realize the value of a mature and highly used ecosystem
         | until you have to chase issues in an ecosystem where maybe 5
         | other people total are doing the same thing you're doing and
         | nobody has updated some library you need for 3 years.
         | 
         | Fun for hobbies, terrible for real work.
        
         | exdsq wrote:
         | In Haskell you'd use pattern matching guards for the empty list
         | which works better for recursion & doesn't require you to
         | handle the Maybe monad in primitive data structures.
         | 
         | Even though Monads were introduced to programming after Haskell
         | had been written (to deal with IO, SPJ and Wadler have a good
         | paper on this) I don't know if this would have been worth
         | changing. After all, you can always wrap a custom Maybe<List>
         | if you need it!
        
           | dundarious wrote:
           | OP is saying the existence of head means someone will use it
           | and get the paper cut. It's true you just shouldn't use it
           | (even when you know it's non-empty, write the throw
           | yourself). But that's why it's an annoyance. Arguably it's
           | even more of a problem, it's a "foot gun". It would be nice
           | if the Prelude was just replaced, but that obviously presents
           | a host of annoying challenges. Several alternative Preludes
           | exist, but none appear to be becoming the new center of mass.
        
           | codeflo wrote:
           | True, but I think enabling more cases where you can use
           | function composition instead of pattern matching and explicit
           | recursion would be a win.
        
         | zeepthee wrote:
        
         | toomanydoubts wrote:
         | There are some alternative Preludes that attempt to fix this,
         | bringing a safer std lib to the table.
        
         | Tainnor wrote:
         | Partial functions and exceptions are a compromise solution for
         | the fact that you sometimes do know more than the compiler
         | does. I think it's fine to throw an exception in the case of
         | "programmer error". It's the equivalent of assertions in other
         | languages. Yes, it can blow up, but at least the error is a bit
         | more localised.
         | 
         | Having head return a Maybe means that you'll have to awkwardly
         | handly a Nothing case even in situations where there is no sane
         | behaviour to be added because it just simply would make no
         | sense for a particular list to be empty unless you've
         | introduced a bug somewhere else. It's hard to "recover" from
         | such an error.
         | 
         | The same goes for e.g. division, which is partial too (can't
         | divide by 0), but having it return Maybe would make arithmetic
         | incredibly awkward. You could instead define e.g. x/0=0 or any
         | other value--some languages like Coq or Pony do that, but I
         | think that has the drawback that this makes it rather easy to
         | mask some ugly errors.
         | 
         | In many such cases, the Haskell type system (without advanced
         | extensions) is not expressive enough to encode everything you
         | know about your values. In a language with dependent types,
         | such as Idris, you can specify the length of the list in your
         | type; then you can have a type-safe, total head function that
         | doesn't return Maybe. You can also write a division function
         | that requires a proof (possibly implicit) that the denominator
         | is not zero. But dependently typed languages are much more
         | niche than Haskell.
        
           | [deleted]
        
           | bspammer wrote:
           | Haskell has had non-empty lists as a type for a long time: ht
           | tps://hackage.haskell.org/package/base-4.16.0.0/docs/Data-...
           | 
           | Having partial functions in the Prelude is, as far as I know,
           | widely regarded as a mistake and they are only kept around
           | for backwards compatibility. Anyone writing code nowadays
           | should be using safeHead or non-empty lists.
        
             | oats wrote:
             | Or pattern matching that takes account of the empty list
             | case. Orrrrr using a fold! I usually find when I start
             | matching on list values that the function could be better
             | expressed with a fold instead.
        
         | madsbuch wrote:
         | What you are running into is the pureness of Haskell. The
         | `head` function in Haskell is only partially defined. What you
         | see as an exception is a case where a function is not defined.
         | This is all by intent.
         | 
         | defining a `head :: [a] -> Maybe a` is a very simple matter and
         | definitely something a developer should be encouraged instead
         | of using the prelude.
         | 
         | exceptions in Haskell are not meant to be used as a first class
         | thing, but is the way to ensure a full Turing complete language
         | where it is possible to define non-terminating behavior. Hence
         | it really is by design.
        
           | dundarious wrote:
           | The issue as I see it, is that one of the main selling points
           | of a pure language like Haskell, is that you have to
           | explicitly state where a certain class of surprises/failures
           | (from IO) lie, and therefore, you can account for them
           | better, handle them cleanly, prevent them from arising
           | accidentally or in some ways maliciously, etc. Partial
           | functions are another kind of surprise/failure, but they are
           | not at all explicit.
           | 
           | This is a bit strange. It's like caring deeply about whether
           | printf fails, but not so much whether array indexing is out
           | of bounds. Haskell has a great story for both kinds of issue,
           | and even its exceptions are better than panics IMO, even if
           | they are about as tricky to use as POSIX signals, but it is
           | relatively obscure and stigmatized to do a gross thing like
           | use unsafePerformIO, but actually quite common/natural and
           | accepted to use head. Lots and lots of people know to do the
           | right thing for the latter, and there is something of a
           | community push to avoid them, but it's just interesting to
           | note how easy it is to make one mistake versus the other,
           | when both matter a lot. One is treated as fundamental, and
           | the other is not, but day to day, both kinds of issue lead to
           | a similar magnitude of headaches, so the disparity is
           | noteworthy.
           | 
           | I'd love it if even just the type signature recorded that
           | exceptions are possible, even if there is no practical effect
           | on how or where it is used.
        
             | Rusky wrote:
             | IO is not (primarily) about where failures lie, but about
             | where side effects lie- side effects are where you start
             | caring about the order of execution.
             | 
             | Array indexing failures, on the other hand, are not
             | something you typically care about at quite that
             | granularity- they're usually just bugs, not something to
             | recover from except perhaps at a much higher level.
             | 
             | The parent comment lumps these kinds of failures in with
             | non-termination, which in pure functions is also typically
             | just a bug rather than a recoverable failure. And this one
             | isn't something you can generally check for, either- with
             | lazy evaluation, every type in a Haskell program by default
             | includes a "bottom" value.
             | 
             | I think both choices were made for a similar reason-
             | actually handling array bounds check failures everywhere is
             | pointless tedium (and often better folded into the
             | iteration itself), and actually handling possible non-
             | termination by using a total language can also get pretty
             | tedious. There are languages that do both, and they have
             | their uses, but Haskell went a different direction.
        
               | dundarious wrote:
               | You make a good point about IO, I forgot how it's also
               | not great about errors (but isn't there are an IO monad
               | with better error treatment? -- it has been several
               | years...). I also agree about granularity and tedium, but
               | that's orthogonal to whether exceptions are the best way
               | to approach such errors, and I don't think they are. Even
               | Go's approach of explicit if-return is not tedium to me,
               | but there are even less tedious approaches, that still
               | let you handle the handle-able errors and do some last-
               | ditch cleanup or just panic on the unhandle-able ones
               | like indexing errors.
               | 
               | The interesting thing about Haskell exceptions are the
               | async ones and the ability to `throwTo`, but I never
               | really had a use for that, so on the whole, that was a
               | bit of an encumbrance too. It's like trying to write
               | exception safe C++ -- tedious _and_ easy to get wrong. I
               | remember a fair few sections of Parallel and Concurrent
               | Programming in Haskell that temporarily didn 't handle
               | exceptions correctly, and it often wasn't for pure
               | pedagogical reasons. Great book though.
        
             | DarylZero wrote:
             | > quite common/natural and accepted to use head
             | 
             | No it isn't, not at all.
             | 
             | What absurd slander.
        
             | Tainnor wrote:
             | Idris does that. If you add "%default total" to a file (or
             | the equivalent compiler flag), it will make sure every
             | function terminates unless it's annotated with "partial".
             | In the best case, only your main function and a couple
             | others need to be partial.
        
         | ParetoOptimal wrote:
         | > But I also admire the patience of anyone who actually manages
         | to use it in practice, If I'm cynical, I'd say that's because
         | little practical stuff is often not worth publishing papers
         | about.
         | 
         | I feel my impatience pushes me towards Haskell if anything...
         | local-reasoning for instance rather than "understand this
         | entire call chain" requires less patience and is easier to get
         | right.
         | 
         | > there are so many little papercuts that don't get resolved,
         | basically for a decade or more.
         | 
         | I've been using Haskell a decade in practice, can you tell me
         | what papercuts you had in mind? I'm assuming I and other real
         | world Haskellers might just see them as much less of a priority
         | all things considered, but I'd like to be sure I'm not missing
         | something.
        
         | odyssey7 wrote:
         | This is why I see PureScript as a better starting point. It was
         | modeled after Haskell, but since it was created in 2013, many
         | of the design choices were to avoid these sorts of things.
        
       | HL33tibCe7 wrote:
       | > Some of my intelligent colleagues mucked up error handling. Not
       | only were they failing, they were failing WRONG 1. This
       | frustrates me because doing failing correctly in Haskell is quite
       | easy
       | 
       | Leaving aside that publicly shitting on your colleagues is an
       | extremely bad look and makes you come across incredibly arrogant,
       | isn't the fact that the intelligent colleagues didn't get it
       | pretty strong evidence that error handling in Haskell in fact
       | isn't easy?
        
         | zeepthee wrote:
         | This isn't isolated to Haskell. Bad errors are everywhere. And
         | by some of my colleagues, I mean YOU TOO!
        
         | zeepthee wrote:
         | I changed it, lov me HL33tibCe7 senpai
        
       | sharmin123 wrote:
        
       ___________________________________________________________________
       (page generated 2022-02-26 23:00 UTC)