Is this a bug in Haskell implementation?

112 views Asked by At

While studying the Haskell implementation of MaybeT, I stumbled upon an intriguing issue. Some function executions yield results that differ from my expectations.

I'm unsure whether this is a bug or something else entirely.

Here's a description of the problem:

liftM(definition) is the fmap function under the Monad type. Its type signature is:

liftM :: (a -> b) -> m a -> m b

Just is a constructor for the Maybe type.

Just 1 results in Just(1).

Just Nothing results in Just(Nothing).

However, when I use liftM Just (equivalent to liftM $ Just), their type signatures are the same:

liftM Just :: Monad m => m a1 -> m (Maybe a1)

Now, when I execute the following line of code:

(liftM Just) $ Just(1)

The result is:

Just(Just 1) :: Maybe (Maybe t)  -- this aligns with my expectations.

But when I execute this line of code:

(liftM Just) $ Nothing   -- I expected: Just(Nothing) :: Maybe(Maybe a), 
                         -- but actual result: Nothing :: Maybe a ??  
 
(liftM Just) $ (Left 1)  -- I expected: Left(Just 1), but actual result: Left 1 ??

The problem is the returned type doesn't match the function's type signature. (liftM Just)

I expected the result to be: Just(Nothing) :: Maybe (Maybe t). However, in GHCi, the actual result is Nothing :: Maybe t — this is what I got.

The two types are different.

Just(Nothing) :: Maybe (Maybe a)
Nothing :: Maybe a

Left(Just 1) :: Either (Maybe a) (Maybe a)
Left 1 :: Either 

I feel that Just(Nothing) and Nothing represent distinct values, right?

They shouldn't be conflated, should they?

So, Wouldn't it be more appropriate for liftM Just $ Nothing to return Just(Nothing) :: Maybe (Maybe t)?

If I want to get a Just(Nothing) as a result, What Can I do?

Is this a bug for Haskell, or is there some other reason behind this discrepancy?

Thanks for your help!

2

There are 2 answers

2
leftaroundabout On

First: this does not have anything to do with liftM in particular. For any well-behaving monad (which includes Maybe) this is indeed equivalent to fmap, which is what you should always use instead.

Let's introduce a type equivalent to Maybe but differently named, this will make it clearer what's going on.

data Maybe' a = Nothing' | Just' a
 deriving (Show, Functor)     -- requires {-# LANGUAGE DeriveFunctor #-} on top of your source file

Now, what you wrote first is equivalent to

> fmap Just $ Just' 1
Just' (Just 1) :: Maybe' (Maybe Integer)

First to emphasize that there is no interaction between the Maybe and Maybe' monads. (In fact we haven't even made Maybe' a monad!) You're simply mapping some function over the Maybe' functor, and that function happens to have type Integer -> Maybe Integer.

In the same framework, your second attempt is

> fmap Just Nothing'
Nothing' :: Maybe' (Maybe Integer)

In this case it doesn't really matter at all that you pass Just, because mapping anything over Nothing' always gives you back Nothing'. It's the same as with the perhaps less confusing

> fmap (+1) Nothing'
Nothing' :: Maybe' (Maybe Integer)

For the Either example it's basically the same story, except that the Left constructor contains an additional value that Nothing' does not have, but this does not partake in any Functor semantics.

0
lsmor On

The thing is that liftM uses de Functor instance of the second argument. Let me clarify:

-- notice you should use fmap, not liftM. 
-- But I'll keep your original concerns

-- for the sake of example, let's do something easier
addOne :: Int -> Int
addOne n = n + 1 

-- apply this func to this arg
--       |            |
> liftM addOne $ Just 3 
Just 4

-- apply this func to this arg, but there is no arg
--       |            |
> liftM addOne $ Nothing
Nothing

Given the above example, is clear that liftM f (Just x) == Just (f x) and liftM f Nothing == Nothing. (Actually, this is the exact implementation of Maybe functor's instance)

Now I think you get confused when doubling the Maybes. So lets apply the same argument

-- first notice that Just is a function!!!
-- in ghci type
> :t Just
Just :: a -> Maybe a

-- so Just is a plain function, in the same meaning addOne
-- is a plain function in the example above, hence

-- apply this func to this arg
--       |            |
> liftM Just  $  Just 4
Just (Just 4)

-- apply this func to this arg, but there is no arg
--       |            |
> liftM Just  $  Nothing
Nothing

As you can see, there is nothing special in using addOne or Just as functions. They behave the same way. Now your problem with Left is due to the fact that Either type Functor's intance is only for the Right constructor. Let me give an example:

-- again: use fmap, not liftM. 

addOne :: Int -> Int
addOne n = n + 1 

-- apply this func to this arg
--       |             |
> liftM addOne $ Right 3
Right 4

-- Do not apply addOne. The Either functor is intended behave like this.
> liftM addOne $ Left 1
Left 1 -- !!

-- Apply on the right, do not apply on the left. Why? The reasons are beyond 
-- the scope of this answer, but is related among other things with the
-- usage of either as an error monad, so Left constructor is like Nothing
-- in Maybe

-- The same happens when using Just.

-- apply this func to this arg
--       |             |
> liftM Just  $  Right 3
Right (Just 3)

> liftM Just $ Left 1
Left 1