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