8 ms·
There are a lot of really annoying quirks about Haskell. It is a beautiful language but after using it professionally for a while, some things come to mind that
by Morgawr 10y ago
There are a lot of really annoying quirks about Haskell. It is a beautiful language but after using it professionally for a while, some things come to mind that really make me pull my hair out of frustration (although I understand why they exist).
My biggest gripe is record handling. There are a few libraries that help dealing with the pain of records, but if you're using 'vanilla' Haskell, they really can be frustrating.
For instance, any field name in a record pollutes the global namespace for that module.
data MyRecord = MyRecord1 { name :: String } | MyRecord2 { name :: NameType } <-- This is an error because the "name" field can't be both a String and a NameType (a made up type, just for the example). You need to use something like rName or r2Name to differentiate the two constructors...
...Which leads me to the other problem of different record constructors of the same data type having different fields, which can lead to some very ugly runtime exceptions (so much for a strong type system!):
=> data MyData = MyData1 { name :: String } | MyData2 { value :: Int }
=> let x = MyData1 { name = "hello" }
=> x { value = 0 } -- <-- this causes an exception because 'value' field does not exist for the constructor MyData1, but does for MyData2 so it matches the type system and there is no compile-time warning to catch it.
Another big issue I've recently had with my code was dealing with multiple constructors into pattern matches/pattern guard conditionals. For example, I have a record type like this: data
MyData = A { ... } | B { ... } | C { ... }
and I want a case to match either A or B, otherwise do C. Let's say I have a function f1 that I want to run on A and B, and a function f2 that I want to run on C. I'm forced to do something like
case myData of
A{} -> f1
B{} -> f1
C{} -> f2
Whereas I'd love to be able to do
case myData of
A{} or B{} -> f1
C{} -> f2
This becomes a big problem when I have a lot of cases to take care of, and repetition makes it harder to spot bugs and easier to mess up.
- pka 10y agoYes, record accessors being partial functions is a big problem. One way to deal with this is to not use fields at all together with algebraic data types (i.e. when you have more than one constructor), just pattern match. When you have some common field across all constructors, define a regular function to pull it out: someData :: MyData -> a and use that instead. As to the second use case, if you need to access something common to A and B but not C, then: someOtherData :: MyData -> Maybe a and pattern match on that. Or first pattern match on C and then have a catch all _ for A and B. lens remedies a lot of Haskell's pain points when it comes to records, but I have to say that PureScripts record system is so much nicer.
- maweki 10y agoLens is really cool but sadly it depends on ghci being available which immediately breaks ARM compatibility. :(
- dllthomas 10y ago> sadly it depends on ghci being available Do you mean Template Haskell? If so, I'm pretty sure it's not required for use, just to have the lenses defined for you. You can still define them manually, at the cost of a little extra boilerplate. If you actually mean ghci, then I have no idea what you're talking about...
- tome 10y agoI guess so, because Template Haskell is not available on ARM. But yes, then you can write out the lenses manually. EDIT: And this link ties Template Haskell to GHCi: http://stackoverflow.com/a/10929803 http://stackoverflow.com/a/10929803
- dllthomas 10y agoAh, good catch!
- dhess 10y agoI don't think it's true that Template Haskell isn't available on ARM, at least not with recent versions of GHC. Maybe what you're thinking of is the problem with cross-compiling and Template Haskell. Generally speaking, that does not work because Template Haskell can do evil platform-specific things at compile time, so if the host and target architectures don't match, you can run into problems. Anyway, you can use Template Haskell on ARM, you just have to compile it on ARM.
- more_original 10y agoIncedentally, OCaml gets some of these right. Record update in constructors is statically checked: type myData = MyData1 of { name : string } | MyData2 of { value : int } let x = MyData1 { name = "hello" } (* * rejected: * let y = MyData2 { x with value = 0 } *) let y = match x with | MyData1 z -> failwith "todo" | MyData2 z -> MyData2 { z with value = 0 } And one can write: match x with | A | B -> f1 | C -> f2
- hellofunk 10y agoDidn't GHC 8 recently released address this problem?
- nilved 10y agoYes, but I'm going to guess that that runtime exception is still possible. Record accessors are disambiguated through type inference so if their type is ambiguous it will still fail.
- Morgawr 10y agoI'm intrigued, which one of these problems was fixed? Unfortunately I still have to use 7.8 so it won't help me currently but in the long run it's good to know.
- hellofunk 10y agoUsing the same record field name in different records: https://downloads.haskell.org/~ghc/master/users-guide/8.0.1-notes.html https://downloads.haskell.org/~ghc/master/users-guide/8.0.1-...
- dllthomas 10y agoTechnically the complaint here was of the same record field name in different constructors for the same record, which is perfectly allowed when the fields in question have the same type but not when the types differ. I don't think DuplicateRecordFields addresses this.
- taylorfausak 10y agoThat's correct. {-# LANGUAGE DuplicateRecordFields #-} data A = B { x :: Bool } | C { x :: Int } Example.hs:2:1: error: • Constructors B and C give different types for field ‘x’ • In the data type declaration for ‘A’
- nilkn 10y agoThis remains a major pain point for me in Haskell. I love the language and find that it's often one of the most powerful languages I've ever used, especially for highly concurrent programs safely utilizing multiple cores, but I'm sort of in awe of how shoddy the record syntax is. For what it's worth, lenses can avoid some of these issues. For instance, they can allow you to have the same field name in multiple records: data RecordA { _a_name :: String } data RecordB { _b_name :: String } makeLensesWith underscoreFields ''RecordA makeLensesWith underscoreFields ''RecordB You can now use something like `view name x` or `x^.name` for values of type `RecordA` or `RecordB`. Within the record definitions themselves, the field names still have to be disambiguated. The naming convention I chose here is pretty arbitrary. The first argument to `makeLensesWith` tells it how to parse the field names to get the "true" name.
- lil1729 10y agoI think using same field names in multiple records is possible with a GHC extension in GHC 8.0.1. https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedRecordFields/DuplicateRecordFields https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedReco... https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedRecordFields https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedReco...
- dllthomas 10y agoRegarding... A{} or B{} -> f1 a big part of the problem is that A and B are going to be putting different things into scope in the context of f1. If you're not binding anything, you could do it with guards: ... | isA myData || isB myData -> f1 ...
- Morgawr 10y agoThis assumes that you have an isA and an isB function. This forces me to write such a function which I might not have and in case of very very very large data types (like ~20-30, if not more, constructors) it becomes unwieldy and very messy.
- dllthomas 10y agoIt's probably less work than duplicating the cases, and ideally you can give a more meaningful name to the whole "isA or isB" construct - especially when you have a lot of constructors!
- pklausler 10y agoIf he's not binding anything, he could just match C{} in the first rule and a wildcard (_) in the second.
- dllthomas 10y agoYes, but there could be a D{}, now or in the future. I rarely use wildcards that aren't embedded in the middle of some larger structure. They stop the compiler from telling me points I need to consider, as I change my code.
- dllthomas 10y agoRegarding heterogeneous fields across constructors... I agree that this is poor design. Field accessors for a field present in one constructor and absent in others should give a Maybe. I'm not certain what should be done for the case of different types. Packaging it up in an Either seems possible, but the obvious approach would make the shape of the return type depend on the order in which I've listed my constructors, which is ugly... Refusing to generate the accessor function seems okay. Disallowing the collision - effectively the current state - isn't so bad. In practice, the rule others have mentioned works fine: pretend Haskell doesn't support named fields in types with more than one constructor. The RecordWildCards extension also makes traditional Haskell records more pleasant to work with.