6

It seems like there are a lot of functions that do the same thing, particularly relating to Monads, Functors, and Applicatives.

Examples (from most to least generic):

fmap == liftA == liftM
(<*>) == ap
liftA[2345] == liftM[2345]
pure == return
(*>) == (>>)

An example not directly based on the FAM class tree:

fmap == map

(I thought there were quite a few more with List, Foldable, Traversable, but it looks like most were made more generic some time ago, as I only see the old, less generic type signatures in old stack overflow / message board questions)

I personally find this annoying, as it means that if I need to do x, and some function such as liftM allows me to do x, then I will have made my function less generic than it could have been, and I am only going to notice that kind of thing by thoroughly reasoning about the differences between types (such as FAM, or perhaps List, Foldable, Traversable combinations as well), which is not beginner friendly at all, as while simply using those types isn't all that hard, reasoning about their properties and laws requires a lot more mental effort.

I am guessing a lot of these equivalencies come from the Applicative Monad Proposal. If that is the reason for them (and not some other reason I am missing for having less generic functions available for confusion), are they going to be deprecated / deleted ever? I can understand waiting a long time to delete them, due to breaking existing code, but surely deprecation is a good idea?

duplode
  • 33,731
  • 7
  • 79
  • 150
semicolon
  • 2,530
  • 27
  • 37
  • 1
    Just always use `fmap`, `pure` and `(*>)` – ZhekaKozlov Apr 11 '16 at 18:04
  • @ZhekaKozlov will do of course, but other people write code that I may have to interact with, and there are more cases than just `fmap` `pure` and `*>` that I may not know all of. – semicolon Apr 11 '16 at 19:11

4 Answers4

5

The short answers are "history" and "regularity".

Originally "map" was defined for lists. Then type-classes were introduced, with the Functor type class, so the generalised version of "map" for any functor had to be called something different, otherwise existing code would be broken. Hence "fmap".

Then monads came along. Instances of monads did not need to be functors, so "liftM" was created, along with "liftM2", "liftM3" etc. Of course if a type is an instance of both Monad and Functor then fmap = liftM.

Monads also have "ap", used in expressions like f `ap` arg1 `ap` arg2. This was very handy, but then Applicative Functors were added. (<*>) did the same job for applicative functors as 'ap', but because many applicative functors are not monads it had to be called something different. Likewise liftAx versus liftMx and "pure" versus "return".

chi
  • 111,837
  • 3
  • 133
  • 218
Paul Johnson
  • 17,438
  • 3
  • 42
  • 59
  • Mathematically, a monad is just a special type of functor (it is the composition of two adjoint functors). While one is not forced to define a `Functor` instance for a type with a `Monad` instance, it is always possible to do so (`fmap f xs == xs >>= return . f`). – chepner Apr 11 '16 at 18:48
  • That makes sense, what about my suggestion to deprecate all the less generic functions? – semicolon Apr 11 '16 at 19:05
  • 3
    Your story for `fmap` has some very slight inaccuracies. When `Functor` came out, `map` was immediately generalized to work on all `Functor`s, not just lists. However, people got upset at the type errors that would pop up when someone misused `map` (asking about Functors when people just wanted to use it for lists), so after that, `map` was renamed `fmap`, and the original `map` was re-introduced to be specialized only on lists. – Justin L. Apr 12 '16 at 03:06
  • @JustinL. I really wish there was something like `EasyPrelude` for stuff like that. I can see how it is useful for beginners, but I think it gets a little annoying. – semicolon Apr 12 '16 at 04:10
1

They aren't equivalent though. equivalent things in haskell can be interchanged with no difference at all in functionality. Consider for example pure and return

EDIT: I wrote some examples down, but they were really bad since they involved Maybe a, a type that is both an applicative and a monad, so the functions could be used pretty interchangeably.

There are types that are applicatives but not monads though (see this question for examples), and by studying the type of the following expression, we can see that this could lead to some roadbumps:

pure 1 >>= pure :: (Monad m, Num b) => m b
Community
  • 1
  • 1
sara
  • 3,521
  • 14
  • 34
  • actually both of my examples are bad, `pure 1 >>= safedivide 0` DOES typecheck, again, because `Maybe` is both an applicative and a monad. – sara Apr 11 '16 at 18:25
  • They are equivalent in the sense that `return` can ALWAYS be replaced with `pure`, and so on. Maybe I used the wrong word but my intended meaning is not wrong. I figured my use of "(from most to least generic)" made it clear that I understood they could only be replaced in one direction. – semicolon Apr 11 '16 at 19:01
  • Also when both type-check, they ARE equivalent. The only difference that I know of is how generic the types are. – semicolon Apr 11 '16 at 19:13
  • I guess it comes down to the fact that they were not all added to the language at the same time. also, there are cases when restricting the type signature of a function is desireable (well it's imaginable at any rate). – sara Apr 11 '16 at 19:22
  • 1
    I would say that while it is sometimes desirable, it should not be done for you in the standard library. It should be something that YOU do when desired, by simply adjusting your top level function's type signature to what you want. Such as `monadicAddTwo :: (Monad m, Num b) => f b -> fb)` then `monadicAddTwo = fmap (+ 2)`. Since you should write out top level type signatures anyway. – semicolon Apr 11 '16 at 19:41
  • 1
    @kai The problem is: Why should something like `return` exist, when it is completely subsumed by `pure` (since all monads are applicatives)? There really isn't a technical reason. It exists for historical reasons (the `Monad` type class was implemented before `Functor` and `Applicative`) and things like `ap` can sometimes be useful when implementing instances: if you already wrote a `Monad` instance, you can say `instance Applicative ... where pure = return; (<*>) = ap`. As for restricting type signatures, the restriction would go in the opposite direction, towards less general not more. – David Young Apr 11 '16 at 20:55
  • @DavidYoung like you say, `return` was there first, and simply removing it, replacing it with `pure` would break a lot of existing code for no real good reason. this is a thing that simply happens with programming languages. what I try to highlight in my answer is that even though the are largely interchangeable, they are not equivalent, and so we can't simply substitute one for the other in all cases. – sara Apr 11 '16 at 21:29
  • @kai But you CAN substitute `return` for `pure` in all cases, even if the opposite isn't always true. And I would love to see a deprecation warning on `return`, honestly even if there was no timeline or even plan at all for it to be removed, just getting it out of active codebases would be nice IMO. It would also limit the confusion felt by people coming from C-like languages where return means something similar-ish but not similar enough to not cause some confusion. – semicolon Apr 12 '16 at 02:11
  • correction: you can't always substitute one for the other without changing the semantics of the program (if only ever so slightly). I agree though, that it's probably a good idea to encourage the use of `pure` over `return` – sara Apr 12 '16 at 06:02
  • @kai Can you give an example of when changing `return` to `pure` breaks something? – semicolon Apr 14 '16 at 21:12
  • none that I can think of, but then again, I am still a beginner, so it's not impossible. a better point would be that you can't remove an existing function without breaking stuff, but I'll have to concede that substitution always works in that direction until I see some evidence for the contrary (I really underestimated how strong haskells type inference is). – sara Apr 14 '16 at 21:30
0

I personally find this annoying, as it means that if I need to do x, and some function such as liftM allows me to do x, then I will have made my function less generic than it could have been

This logic is backwards.

Normally you know in advance the type of the thing you want to write, be it IO String or (Foldable f, Monoid t, Monad m) => f (m t) -> m t or whatever. Let's take the first case, getLineCapitalized :: IO String. You could write it as

getLineCapitalized = liftM (map toUpper) getLine

or

getLineCapitalized = fmap (fmap toUpper) getLine

Is the former "less generic" because it uses the specialized functions liftM and map? Of course not. This is intrinsically an IO action that produces a list. It cannot become "more generic" by changing it to the second version since those fmaps will have their types fixed to IO and [] anyways. So, there is no advantage to the second version.

By writing the first version, you provide contextual information to the reader for free. In liftM (map foo) bar, the reader knows that bar is going to be an action in some monad that returns a list. In fmap (fmap foo) bar, it could be any sort of doubly-nested structure whatsoever. If bar is something complicated rather than just getLine, then this kind of information is helpful for understanding more easily what is going on in bar.

In general, you should write a function in two steps.

  1. Decide what the type of the function should be. Make it as general or as specific as you want. The more general the type of the function, the stronger guarantees you get on its behavior from parametricity.

  2. Once you have decided on the type of your function, implement it using the most specific available functions. By doing so, you are providing the most information to the reader of your function. You never lose any generality or parametricity guarantees by doing so, since those only depend on the type, which you already determined in step 1.


Edit in response to comments: I was reminded of the biggest reason to use the most specific function available, which is catching bugs. The type length :: [a] -> Int is essentially the entire reason that I still use GHC 7.8. It's never happened that I wanted to take the length of an unknown Foldable structure. On the other hand, I definitely do not want to ever accidentally take the length of a pair, or take the length of foo bar baz which I think has type [a], but actually has type Maybe [a].

In the use cases for Foldable that are not already covered by the rest of the Haskell standard, lens is a vastly more powerful alternative. If I want the "length" of a Maybe t, lengthOf _Just :: Maybe t -> Int expresses my intent clearly, and the compiler can check that the program actually matches my intent; and I can go on to write lengthOf _Nothing, lengthOf _Left, etc. Explicit is better than implicit.

Reid Barton
  • 14,951
  • 3
  • 39
  • 49
  • By that logic do you define `lengthList :: [a] -> Int` and `foldrTraversable :: Traversable t => (a -> b -> b) -> b -> t a -> b`, what about `(+.+) :: Double -> Double -> Double`. Now obviously you don't because that would be idiotic, so why use `liftM`? Now you are probably going to respond "but those aren't already available", well if they were, would you use them? Also why aren't they available when `liftM`, `liftA` and `>>` are? It seems as though you either have to include specific functions for every combination, not have any unnecessarily specific functions, or be inconsistent. – semicolon Apr 11 '16 at 20:16
  • Also I would argue that the increased "clarity" you get from using functions that have needlessly specific types is not worth the increased number of functions you have to have memorized. Taken to the logical extreme as in my previous comment, there would be something like an order of magnitude more functions (Functor can become Monad, Applicative, List, Maybe, IO, Either, ZipList etc.), all of which you would want memorized. By simply not having any overly specific functions, you save a lot of mental overhead, and sure you have to do a tiny bit more reasoning, but it really isn't difficult. – semicolon Apr 11 '16 at 20:20
  • I don't define my own type-restricted synonyms. I just use the ones from the Haskell standard, that Haskell programmers already know, because I am writing a Haskell program. Of course the exact contents of any standard library are inevitably somewhat arbitrary. In the long run, learning a few extra names like `map = fmap` or `(.) = (<$>)` is a lot cheaper than a lifetime of wading through `fmap` soup. – Reid Barton Apr 11 '16 at 21:01
  • So basically you only use historical accidents, and making one intentionally would be a bad idea? And if the historical accident never occurred, you would be perfectly OK with not having any of those needlessly restricted versions. So by that logic historical accidents are a good thing? As they arbitrarily throw in some needlessly type restricted functions and people then get used to them? – semicolon Apr 11 '16 at 21:02
  • 2
    The contents of the standard library are not entirely a historical accident. They also reflect the way human programmers think about their programs. Function composition `(.)` is an essential concept, even though `(.)` is itself a type-restricted form of `(<$>)` or `fmap`. Would you argue that one should not use `(.)` either? – Reid Barton Apr 11 '16 at 21:14
  • Let me try to summarize. Any standard library function has some cost and some value. The cost is that one needs to learn the function. The value can be letting you do something more conveniently, or it can be letting you express your intent more clearly. This answer explains that a function can have value of the latter sort by supplying additional type information. This value is partly dependent on what you call the "historical accident" that other Haskell programmers know the function, just like English words (all historical accident) have value because other English speakers understand them. – Reid Barton Apr 11 '16 at 21:58
  • 1
    On the topic of catching bugs - using more general functions allows you to give a more general type signature (i.e., more parametric) to the function you are trying to write. This can rule out a very large number of bogus implementations by virtue of typechecking. There is only one interesting function of type `Functor f => (a -> b) -> f a -> f b` but infinitely many of type `Monad m => (a -> b) -> m a -> m b`. – user2407038 Apr 12 '16 at 00:07
  • I mean `(.)` also has `infixr 9` instead of `infixl 4` so using `<$>` everywhere can potentially break things. `(.)` is also more concise and quicker to type. It admittedly still is an example of one of the extremely few cases where I could understand having a type restricted variant. Although honestly if `<$>` was never made and `(.)` was used for both mapping and composition, I wouldn't really complain. I would argue things like `liftA` and `liftM` are not worth the cost though. – semicolon Apr 12 '16 at 02:03
  • @user2407038, the benefits you mention are a direct result of the type you chose in step 1 for the function you are writing. (By their very nature, they can only depend on the type, not the implementation!) It's true that in order to implement a general function, you need to use other general functions or your program won't type check. But there's no further benefit to using an overly general function, and there are costs (e.g. masking bugs). So pick a general type in step 1, but always use the most specific functions that will let your program type check in step 2. – Reid Barton Apr 12 '16 at 13:54
0

There are some "redundant" functions like liftM, ap, and liftA that have a very real use and taking them out would cause loss of functionality --- you can use liftM, ap, and liftA to implement your Functor or Applicative instances if all you've written is a Monad instance. It lets you be lazy and do, say:

instance Monad Foo where
    return = ...
    (>>=) = ...

Now you've done all of the rewarding work of defining a Monad instance, but this won't compile. Why? Because you also need a Functor and Applicative instance.

So, because you're quickly prototyping, or lazy, or can't think of a better way, you can just get a free Functor and Applicative instance:

instance Functor Foo where
    fmap = liftM

instance Applicative Foo where
    pure  = return
    (<*>) = ap

In fact, you can just copy-and-paste that chunk of code everywhere you need to quickly define a Functor or Applicative instance when you already have a Monad instance defined.

The same goes for fmapDefault from Data.Traversable. If you've implemented Traversable, you can also implement Foldable and Functor:

instance Functor Bar where
    fmap = fmapDefault

no extra work required!

There are some redundant functions, however, that really have no actual usage other than being historical accidents from a time when Functor was not a superclass of Monad. These have literally zero use/point in existing...and include things like the liftM2, liftM3 etc., and (>>) and friends.

Justin L.
  • 13,510
  • 5
  • 48
  • 83
  • Wouldn't a better alternative to that whole "using redundant functions to implement superclasses" be allowing automatic deriving of those superclasses. So if you don't manually specify `instance Functor Foo where` or whatever, but you have `instance Monad Foo where`, have the compiler simply derive `fmap`, `pure` and `<*>`. – semicolon Apr 12 '16 at 04:03
  • To extend that a little bit, I think `instance Monad Foo where`, `(>>=) = ...`, `pure = ...` should be a valid way to make `Foo` a `Monad`, an `Applicative`, and a `Functor`. – semicolon Apr 12 '16 at 04:04