[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)