Pitfalls in Haskell
users.jyu.fi
users.jyu.fi
The "monomorphism" in "monomorphism restriction" isn't the term from category theory: it's just the opposite of "polymorphism". You can read more about the details of the restriction and why it exists here: https://www.haskell.org/onlinereport/haskell2010/haskellch4....
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.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.
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...
EDIT: And this link ties Template Haskell to GHCi: http://stackoverflow.com/a/10929803
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.
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 -> f2https://downloads.haskell.org/~ghc/master/users-guide/8.0.1-...
{-# 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’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.https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedReco... https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedReco...
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
...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.
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.
3.5 Naming Conventions
Some names are inconsistent. For example Functor is not called Mappable while Traversable is not named after abstract nonsense. It is best to focus on what things are instead of what they are called.
A much better example would be "Show"(Showable) vs "Traversable".
If you really want to mess things up, use names that mean something different every day.
But I would know what a “Mappable” should be, regardless of PL.
Lots of terms like Profunctor, Contravariant Functor, Bifunctor etc. (which are also the names of typecasses in Haskell) rely on the precise, category-theoretical definition of Functor.
No, it has the same meaning. It does not behave identically because the languages do not have identical designs.
>But I would know what a “Mappable” should be, regardless of PL.
Lots of people wouldn't, and would be wondering what cartography has to do with anything. And even for people familiar with the functional "map", now they would just have an incorrect idea of what Functor is. Lots of Functors are things most people would not think of as being "Mappable".
> No, it has the same meaning. It does not behave identically because the languages do not have identical designs.
Functors in Haskell and OCaml are different things. Haskell Functors correspond to the category-theoretic notion of a functor (a thing mapping objects to objects and morphisms to morphisms in a reasonable manner). In OCaml and ML, Functors represent parameterized modules. These are not known to correspond to category-theoretic functors in any reasonable way (and there have been proposals for category-theoretic models of the ML module system). I guess the term Functor is a bit of a misnomer in the ML-like languages.
No, functors represent functions from a module to a module. Oh hey, that sounds just like the standard definition of a functor. As I said, the behavior is different because the languages are different, but both are named after the category theory functor, because they both emulate it.
http://cs.stackexchange.com/questions/9769/what-is-the-relat...
Functors play very different roles in Haskell and OCaml/ML.
In Haskell, the Functor class is there to implement the functoriality of a type. If F is a functor, then one implements maps of type F A -> F B from given A -> B in a functorial way.
In ML, the term functor was chosen because they are not first-class functions, but "a level up" (much like a functor is a level up from the maps in the category that it operates on). However, if F is an ML functor, then there are no "maps" between the modules F(A) and F(B). The morphism part, which is the essence of a Functor in Haskell, is missing in ML. This is why I think that the functor terminology isn't very good in ML.
Not everything that maps one thing to another is a functor.
Yes there is, the functor. Just repeating a false statement won't make it come true.
What we have is a functor F, which would be defined as in:
module F =
functor (X: S) -> ....
If you have two modules A and B of signature S, then you can form F(A) and F(B) and these are both modules. Now where are the maps from F(A) to F(B)? Just saying "the functor" does not explain this.> Just repeating a false statement won't make it come true.
Exactly!
If you want to claim a relation between OCaml functors and category-theoretic functors, then you must be able to justify your claims. Saying "functors represent functions from a module to a module. Oh hey, that sounds just like the standard definition of a functor." is just not a convincing argument.
That is literally the functor. Seriously, read the chapter on modules.
That would be an answer for Haskell (though a proper one would refer to fmap), but it does not work like that in OCaml. You do know some OCaml, right?
But if it's so obvious, then you can say how it works for the following concrete example:
module type S = sig
type t
val f: t -> int
end
module F = functor (X: S) -> struct
type s = X.t * X.t
let g x = X.f x + X.f x
end
module A : S = struct
type t = float
let f x = int_of_float x
end
module B : S = struct
type t = int
let f x = 2*x
end
module X = F(A)
module Y = F(B)
How do I get from X to Y by "literally the functor"?The point is: In the ML module system there is no equivalent to fmap: (A -> B) -> (F A -> F B), i.e. the functor action on morphisms. So, functors in Haskell and OCaml are different things. If you want to claim otherwise, you'll have to make an actual argument for it.
NB You only need to get from X to Y in ways that arise from getting from A to B. Since there doesn't seem to be any relevant concept of morphism it's hard to see how ML functors are anything other than trivially category theoretical.
Actually, I think there are three hard problems in CS. The third being pedants.
this would get downvoted quickly if it was a comment
I wanted to rename "Monoid" to "Joinable" in my code ("Concatenatable" is too long). But it turns out you can't alias a type class while maintaining compatibility with libraries that use it.
Do tell me more about how "concatenatable" is a "completely wrong" impression of what Monoids are for. I know you can use them to, like, multiply using abstract nonsense, but that's not why you care what a Monoid is. Monoids let you generalize `mappend` over many kinds of sequences.
People can cope with all kinds of bad things, regardless of whether or not they know haskell. That is not an argument in favor of bad things.
>I know you can use them to, like, multiply using abstract nonsense
What? There's nothing "abstract nonsense" about it. Integers are monoids. Things that can be appended is a subset of monoids, not a description of them.
It's such a Haskelly response to say that I am not allowed to use a concept for what it was designed for, unless I use it in its full generality.
No, it is partially wrong, as I said. Appending describes a subset of monoids, not monoids.
>It's such a Haskelly response to say that I am not allowed to use a concept for what it was designed for
What on earth are you talking about? I am not a haskell person and I said nothing even remotely resembling that.
I think that one bad habit that Haskell promotes is statements of this form, which are true in Haskell because of the way Haskell forces you to define typeclasses, but aren't really "true" of the concepts modeled by typeclasses. (Integer, +, 0) defines a monoid, as does (Integer, ×, 1).
> Things that can be appended is a subset of monoids, not a description of them.
The key operation that defines a monoid is the append operation ("mappend" in the definition of the Haskell typeclass Monoid, to avoid collision with the list-specific "append" operation), which for the monoid of integers over addition is (as the name suggests) addition.
>The key operation that defines a monoid is the append operation
No, it is the sole monoid operation. It is named mappend in haskell, but that does not make everything it encompasses into appending. Ask someone what operations can be done on "appendables" and you will get lots of answers like looping, sizeof, etc. Append always refers to collections in other languages, so the argument that it would help people to call something different "appendable" for those people is silly.
No, the specific statement is true neither in reality nor in Haskell, though statements of that form (which is what I said) are often true in Haskell (e.g., List is a Monad) though not reality.
Integers are not monoids. As I stated in GP, the combination (Integer, +, 0) defines a monoid -- as does (Integer, ×, 1); there's other monoids one could define on Integers, though those are the two most obvious and useful ones. Haskell, because its designed to make statements of the form type is a typeclass has to do one of two things to support the case where multiple useful instances of a typeclass can be defined involving a particular type -- one is the newtype wrapper approach used for Sum and Product with numeric types as monoids, with the base type participating in none of the instances; the other is to choose a primary instance that the base type participates in, and relegate the others to the newtype treatment (this is what is done with lists as monads in Haskell).
Both of these approaches are workarounds for the problem that typeclass instances aren't naturally defined by a participating type alone, but by the combination of a participating type, and one or more other values (some of which are usually functions.)
This is simply being pedantic. We're all well aware of the meaning of what I said, as it is how it is almost always said.
>Both of these approaches are workarounds for the problem that typeclass instances
That's not a problem, it is the point. That is what typeclasses were intended to be.
{-# LANGUAGE ConstraintKinds #-}
import Data.Monoid
type Joinable = Monoid
something :: Joinable m => m -> m -> m
something = (<>)
But like MustardTiger says, don't. newtype Sum a = Sum { getSum :: a }
instance (Num a) => Monoid (Sum a) where
mempty = Sum 0
mappend (Sum a) (Sum b) = Sum (a + b)
Finally, and perhaps most importantly, I seriously doubt anybody reading your codebase is going to do so in complete isolation of the rest of the Haskell ecosystem. They are going to encounter the word "monoid", so you're not really doing them any favors by hiding it from them. You're just forcing them to jump through the mental hurdles of reconciling your wording with what's used everywhere else. type Natural = [()]
zero, one, two, three, four :: Natural
zero = []
one = [()]
two = [(), ()]
three = [(), (), ()]
four = [(), (), (), ()]
...
concatenation `(++) :: Natural -> Natural -> Natural`: >>> four
[(), (), (), ()]
>>> three ++ one
[(), (), (), ()]
Your point still holds.Our (non-jargon) vocabulary simply isn't equipped to deal with this level of abstraction so we need jargon if we intend to be fully descriptive — existing words don't cut it and we are just at the bottom rung of the abstraction ladder.
It's up to us as a community how much we care about faithfully describing things, Simon Peyton Jones himself claims tongue-in-cheek that their biggest mistake was not calling monads “warm fuzzy things”[0] :)
[0] http://research.microsoft.com/en-us/um/people/simonpj/papers...
E.g., "classOf fmap" as a synonym for Functor.
class (HasMap f) <= Functor f
class (Functor f, HasApply f) <= Apply f
class (Apply f, HasPure f) <= Applicative f
class (Apply m, HasChain m) <= Bind m
class (Applicative m, Bind m) <= Monad m
[1]: https://github.com/tfausak/purescript-neon/blob/v0.5.4/src/N...
[2]: https://pursuit.purescript.org/packages/purescript-prelude/1...Some of these complaints are goofy like the one about the naming of Functor.
Others like the one about records being bad are perfectly legitimate. It would be awesome to see real improvement on this. Unnecessary runtime errors are evil.