Haskell中Identity Monad与Maybe Monad的行为差异探究
Great question! The difference between Identity and Maybe here boils down to two key factors: how their Monad instances work and the standard typeclass instances provided for each. Let's break this down step by step.
1. Why Identity acts differently in your first bind example
First, let's recall the core definition of the Identity monad:
newtype Identity a = Identity { runIdentity :: a }
Its Monad instance is extremely minimal—it just unwraps the wrapped value and passes it directly to the bind function:
instance Monad Identity where return = Identity Identity x >>= f = f x
So when you write:
let f = \x -> Identity 2 >>= \y -> x * y
This simplifies almost entirely to \x -> (\y -> x * y) 2, which is just a shorthand for \x -> x * 2.
Now, why does x * 2 work when x is an Identity b? Because Identity has a built-in Num instance for any underlying numeric type a:
instance Num a => Num (Identity a) where (+) = liftA2 (+) (*) = liftA2 (*) fromInteger n = Identity (fromInteger n) -- Other Num methods follow similar lifting logic
This instance lets you treat Identity b like a regular numeric type:
- When you multiply
x(anIdentity b) by2(a plain integer),fromIntegerautomatically wraps2into anIdentity b. - The
(*)operation lifts standard numeric multiplication into theIdentitycontext, soIdentity 6 * 2becomesIdentity (6*2) = Identity 12. - GHCi also automatically unwraps
Identityvalues when printing, so you see12instead ofIdentity 12.
That's why :t f gives Num b => Identity b -> Identity b—your function is just multiplying an Identity-wrapped number by 2 and returning the wrapped result.
2. Why Maybe throws an error in the same bind pattern
The Maybe monad's bind logic (handling Nothing cases) is part of the story, but the real issue is Maybe has no standard Num instance. Let's look at your first g definition:
let g = \x -> Just 2 >>= \y -> x * y
For this to typecheck, the expression x * y must return a Maybe c (since Maybe's bind expects a function a -> Maybe b). That means:
yis a plainb(fromJust 2 :: Maybe b), soxwould need to be a type wherex * bproduces aMaybe c.- This requires the constraint
Num (Maybe c)—but there's no built-inNuminstance forMaybe, so GHC throws an error.
When you add return (or explicitly wrap with Just), you change the expression to return $ x * y. Now x is a plain b, x*y is b, and return wraps it into Maybe b—so the typechecker is happy, giving you g :: Num b => b -> Maybe b.
3. Why your explicit Identity wrapper changes the type
When you rewrite the Identity function to:
let f = \x -> Identity 2 >>= \y -> Identity $ x * y
Now the lambda \y -> Identity $ x * y expects y to be a plain b (from Identity 2), so x must also be a plain b (since x*y needs to be b to wrap into Identity b). The bind simplifies to applying \y -> Identity $ x*y to 2, resulting in \x -> Identity (x*2)—hence the type Num b => b -> Identity b, which matches your initial expectation.
4. Numeric operations: Identity vs Maybe
The numeric operation differences again stem from the Num instance:
Identity'sNuminstance lifts all numeric operations into the monad, soIdentity 5 + Identity 6becomesIdentity (5+6), andIdentity 5 +6usesfromIntegerto turn6intoIdentity 6before adding.Maybehas no standardNuminstance, soJust 5 + Just 6has no valid(+)operation to use. Even enablingFlexibleContextsdoesn't fix this—FlexibleContextsjust allows non-type-variable constraints (likeNum (Maybe b)), but you still need to define the actualNuminstance forMaybeyourself.
Can we make Maybe behave like Identity with FlexibleContexts?
Sort of—but it requires defining a custom Num instance for Maybe, and it's not necessarily idiomatic Haskell. Here's how you could do it:
First, enable the required extensions:
{-# LANGUAGE FlexibleInstances, FlexibleContexts #-} instance Num a => Num (Maybe a) where Nothing + _ = Nothing _ + Nothing = Nothing Just x + Just y = Just (x + y) Nothing * _ = Nothing _ * Nothing = Nothing Just x * Just y = Just (x * y) fromInteger n = Just (fromInteger n) -- You'd need to implement all other Num methods (negate, abs, signum, etc.) similarly
With this instance, your original g definition would typecheck:
let g = \x -> Just 2 >>= \y -> x * y -- :t g => Num b => Maybe b -> Maybe b
And numeric operations like Just 5 + Just 6 would return Just 11, while Nothing + Just 6 returns Nothing (consistent with Maybe's failure-propagation purpose).
That said, most Haskell programmers would prefer using liftA2 (+) or do notation instead of defining a custom Num instance for Maybe, as it keeps code more explicit about monadic operations.
内容的提问来源于stack exchange,提问作者pls

