'Data.Typeable cast and Maybe pattern match behavior in Haskell

Based on this QA;

A new type for all a (normal value) and nothing in Haskell?

Here's the first working code:

import Data.Typeable (Typeable, cast)

data None = None

none :: None
none = None

f :: Typeable a => a -> a
f = \x ->
  case (cast x :: Maybe None) of
    Just None -> undefined
    Nothing -> x

test :: IO ()
test = do
  print $ f (5 :: Int)  --5
  pure $ f none
  print "done"

This code works as I intended.

Now, I want to make this optionMap and here is the non-working code with an error:

import Data.Typeable (Typeable, cast)

data None = None

none :: None
none = None

optionMap :: Typeable a => (a -> b) -> a -> b
optionMap = \f -> \x ->
  case (cast x :: Maybe None) of
    Just None -> undefined
    Nothing -> f x

test :: IO ()
test = do
  print $ optionMap (\a -> a + 1) (5 :: Int) -- 6
  pure $ optionMap (\a -> a + 1) none
  -- No instance for (Num None) arising from a use of ‘+’
  print "done"

Surely, I understand the meaning of the error, however, I expect it pattern-matched as Just None as the first working example, then the value should be undefined.

Is it possible to fix?



Solution 1:[1]

Well, I specifically said in my answer to your other question that Typeable isn't a good tool to address this goal. But sure, let's see if we can fix it.

For starters, this is pretty silly:

optionMap :: Typeable a => (a -> b) -> a -> b
optionMap = \f -> \x ->
  case (cast x :: Maybe None) of
    Just None -> undefined
    Nothing -> f x

If the function f can handle arguments of type a, and we have an x of type a, there's no need to check for None at all; f x is valid. If you really want optionMap, you don't want it to have this type.

To see why, let's look at the way you were calling it. With optionMap (\a -> a + 1) none you need all these types to unify:

(\a -> a + 1) :: Num t =>   t -> t
none ::                                None
optionMap :: Typeable a => (a -> b) -> a    -> b

I've lined them up (and use globally unique names for type variables) to make it clear what type inference is going to happen. We're going to decide that a ~ t, and b ~ t, but also that a ~ None. If any of these facts were not true, we would have a type error. So GHC assumes they are all true, but that gives us as a combined type of optionMap:

optionMap :: (Typeable None, Num None) => (None -> None) -> None -> None

Not very helpful! The whole point of None was that the function wouldn't be applied if it was None, so we don't want any of the constraints from the function to end up as constraints on the a when we pass a None in that position. The only way we can achieve that is by having the type of the function parameter's parameter be a different type variable from the second parameter of optionMap. So:

optionMap :: (Typeable a, Typeable a') => (a' -> b) -> a -> b

But now optionMap accepts any type a, not just the a' values that can be passed to its function parameter. What we need is to then use cast the other way around from the way you used it; check if it's safe to pass x to f and handle that specially (by returning f x), and put our do-nothing case in the branch where the cast failed. Something like:

optionMap :: forall a a' b. (Typeable a, Typeable a') => (a' -> b) -> a -> b
optionMap f x =
  case (cast x :: Maybe a') of
    Just x' -> f x'
    Nothing -> undefined

Note that doing that requires the ScopedTypeVariables extension (otherwise in the cast x :: Maybe a' the a' is a new local type variable, not a reference to the a' from optionMap's signature), and requires we explicitly introduce the type variables with forall (that's how you declare a larger-scope type variable that can be referenced from more than one signature, with ScopedTypeVariables).

Lets try it:

{-# LANGUAGE ScopedTypeVariables #-}

import Data.Typeable

data None = None
none = None

optionMap :: forall a' a b. (Typeable a, Typeable a') => (a' -> b) -> a -> b
optionMap f x =
  case (cast x :: Maybe a') of
    Just x' -> f x'
    Nothing -> undefined


main :: IO ()
main = do
  print $ optionMap (\a -> a + 1) (5 :: Int)
  pure $ optionMap (\a -> a + 1) none
  print "done"

Unfortunately we get a bunch of messages like:

foo.hs:17:3: warning: [-Wdeferred-type-errors]
    • Ambiguous type variable ‘a0’ arising from a use of ‘print’
      prevents the constraint ‘(Show a0)’ from being solved.
      Probable fix: use a type annotation to specify what ‘a0’ should be.

With the Typeable constraints, GHC is no longer willing to default the ambiguous types to Integer. Plus we've disconnected the type of the function parameter and the other paramter to optionMap, so the explicit annotation in (5 :: Int) tells us nothing about the type of the function (which we need to know in order for cast to tell us whether 5 :: Int is the same type!), so it' probably good that GHC won't default types of the functions or it would have picked Integer -> Integer, which doesn't match. I frequently try to use Bool or Char in these small test programs rather than numbers, for exactly this reason; Num and defaulting just make everything a little more complicated. But ets try that again with more types:

{-# LANGUAGE ScopedTypeVariables #-}

import Data.Typeable

data None = None
none = None

optionMap :: forall a' a b. (Typeable a, Typeable a') => (a' -> b) -> a -> b
optionMap f x =
  case (cast x :: Maybe a') of
    Just x' -> f x'
    Nothing -> undefined


main :: IO ()
main = do
  print $ optionMap (\a -> a + (1 :: Int)) (5 :: Int)
  pure $ optionMap (\a -> a + (1 :: Int)) none
  print "done"

Great it compiles, and if we run it:

? runhaskell foo.hs
6
"done"

But there's a problem:

main :: IO ()
main = do
  print $ optionMap (\a -> a + (1 :: Int)) (5 :: Int)
  pure $ optionMap (\a -> a + (1 :: Int)) 'a'
  pure $ optionMap (\a -> a + (1 :: Int)) True
  pure $ optionMap (\a -> a + (1 :: Int)) (5 :: Integer)
  pure $ optionMap (\a -> a + (1 :: Int)) (print False)
  print "done"

All of these also happily take the non-equal types path. We haven't written optionMap so that it accepts either None or the type that matches the function's argument, we've made it accept any type at all, and applies the function if the types happen to match. Which means the type checker is completely unable to detect any mistakes we make. Any mistake where we intended the types to match but we messed up will be silently ignored.

We can try explicitly handling None:

optionMap :: forall a' a b. (Typeable a, Typeable a') => (a' -> b) -> a -> b
optionMap f x =
  case (cast x :: Maybe a') of
    Just x' -> f x'
    Nothing ->
      case (cast x :: Maybe None) of
        Just None -> undefined
        Nothing -> error "what do we put here?"

But no matter how many layers deep of cast and case we go, we're going to be left with that final "it could be absolutely anything else" case; we don't end up with a clean optionMap that accepts the correct type to apply the function or None, and nothing else.

What we need is a way to make a type for that second parameter that isn't just a universal a, but rather is a specific type for None or a'. That's exactly what you asked the other question about, and is exactly what I said Typeable wouldn't really help you do.

Haskell of course has an extremely uncomplicated and easy way of representing the choice between two types: discriminated unions, AKA algebraic data types with multiple constructors. That's exactly what you have already rejected because "the complication that deliver to code is far from justified at least for my purpose".


Before I address how you can actually address your need, I need to come back to another problem with your attempt: why are you using undefined? undefined does not "do nothing"; it isn't a neutral value can use when you haven't got what you need, as null is often used in other programming languages. When Haskell hits a place you've written undefined it terminates the program with an error message. I suspect you know that, because you didn't write print in the line of your test program that was supposed to handle the none example of using optionMap, you wrote pure.

pure of course wraps up the result in an IO Int, but because we do anything with it nothing actually happens. We'd get exactly the same result from:

main :: IO ()
main = do
  print $ optionMap (\a -> a + (1 :: Int)) (5 :: Int)
  pure $ error "didn't run"
  print "done"

So it's a pretty pointless test of optionMap. What actually happens if we try to use it, changing the pure to a print:

? runhaskell foo.hs
6
foo.hs: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at foo.hs:12:16 in main:Main

Using optionMap _ none isn't a type error, but if it ever actually gets evaluated it terminates the program with an error. If we were happy with that we could skip this whole attempt and just use:

print $ (\a -> a + 1) 5
print $ (\a -> a + 1) undefined

Because undefined is a member of every type (along with infinite loops, and error "whatever", and a whole host of other ways of writing the bottom value), we can already just pass a "wrong" value when we haven't got a real one, if we're happy with this "error out at runtime" behaviour (which I personally am not).

So even if we could put in even more work and somehow get a type for optionMap that works the way you want, it still doesn't give you the ability to feed in a none to a computation expecting some other type and have the program ignore it. To do that you'd need to also have some way of "mapping" print across your "might be nothing, might be a value"; making it apply print (or any other IO action) if the value is there, and just do nothing if the value was None. Something like:

main :: IO ()
main = do
  print `ioOption` optionMap (\a -> a + 1) (Some 5)
  print `ioOption` optionMap (\a -> a + 1) None
  print "done"

Where I've just swapped out the $ operator, which was directly applying print to the result of optionMap, for some new ioOption that applies print to the result of optionMap only if it is not None. You'll have just as much trouble implementing that as you do implementing optionMap.


Now, I know you said that using discriminated unions just added too much complexity to your code for you to bother with for your present purposes (although you seem to be willing to put in far more effort to avoid Maybe than you are to put into using it). But for completeness, I am going to show how these problems are solved using discriminated unions, in all their horrible "complexity":

data Option a = Some a | None

optionMap :: (a -> b) -> Option a -> Option b
optionMap _ None = None
optionMap f (Some x) = Some (f x)

ioOption :: (a -> IO ()) -> Option a -> IO ()
ioOption _ None = pure ()
ioOption f (Some x) = f x


main :: IO ()
main = do
  print `ioOption` optionMap (\a -> a + 1) (Some 5)
  print `ioOption` optionMap (\a -> a + 1) None
  print "done"

And running this:

? runhaskell bar.hs
6
"done"

Things to note here:

  1. The types are much simpler, and say exactly what they do; we still get the full power of Haskell's error checking (try changing Some 5 to Some True)
  2. The implementation of optionMap is much simpler than any of our attempts with Typeable; we just write a case for None and a case for Some and we're done, which is about the minimal number of moving parts it would be possible to use
  3. That's just about the only possible implementation we can give for optionMap. Haskell is (almost) forcing us to write correct code. (The only other things we could do that would pass the typechecker are return Nothing even in the Some case, return bottom/undefined, return bottom/undefined inside the Some constructor, or start messing with unsafe functions; all of those are obviously incorrect, so it's not hard to avoid accidentally writing any of them)
  4. No extensions needed
  5. We don't have to explicitly type annotate our numbers; Haskell's normal defaulting mechanism just works for us again

Of course, everything above (other than main) is already implemented for you. So you don't even need to write Option, optionMap, or ioOption. Option is Maybe, of course, optionMap is fmap, and ioOption is mapM_ from Data.Foldable (or traverse_ - they're the same thing only traverse_ has a more general type; they both exist for historical reasons). So we can just write:

import Data.Foldable

main :: IO ()
main = do
  print `mapM_` fmap (\a -> a + 1) (Just 5)
  print `mapM_` fmap (\a -> a + 1) Nothing
  print "done"

Basically, whatever complexity you think you're avoiding by avoiding Maybe (or other discriminated unions), I guarantee you the alternatives are more complicated. You may just be not seeing how you can use the interfaces of Maybe (and other types) to write simpler code, in which case I'd be happy to answer further questions here. If you really just don't like writing constructor tags so much that things along the lines of our attempts at optionMap here seem preferable (if they could work), then you're really not going to enjoy using Haskell; it is a fundamental part of the language.


For the sake of completeness, I did think of one way to get an optionMap that really accepts "a function and either a value that can be passed to the function or None". It's not pretty, but it does work:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, TypeFamilies #-}

data None = None

type family OptionResult a b
  where OptionResult None _ = None
        OptionResult _ b = b

class OptionMap a b a'
  where optionMap :: (a -> b) -> a' -> OptionResult a' b

instance OptionMap a b None
  where optionMap _ _ = None

instance (OptionResult a b ~ b) => OptionMap a b a
  where optionMap f x = f x


class OptionTraverse a a'
  where optionTraverse :: Applicative f => (a -> f ())-> a' -> f ()

instance OptionTraverse a None
  where optionTraverse _ _ = pure ()

instance OptionTraverse a a
  where optionTraverse f x = f x


main :: IO ()
main = do
  (print :: Int -> IO ()) `optionTraverse` optionMap (\a -> a + (1 :: Int)) (5 :: Int)
  (print :: Int -> IO ()) `optionTraverse` optionMap (\a -> a + (1 :: Int)) None
  print "done"

The problem with this approach is it destroys type inference. GHC needs to know the type of everything very precisely in order to resolve the instance and type family we're using. GHC won't use the input and output types of the various function's we're using to constrain each other's types because we don't directly connect them with each other; if their types were wrong we wouldn't get a type-mismatch error, we'd get an error about it being unable to resolve the type class. And thus any polymorphic thing (like print) can't have its type resolved by connecting with something else; we have to explicitly provide a type annotation.

So this approach is not something I recommend. Programming with it with any scale will be very difficult. I include it only to show you the lengths you have to go to get your idea of how this should be done to work in Haskell, as compared to the extremely simple example using Maybe that does what you want, just not How you wanted it to work. (I would argue that the "how" of the Maybe approach is also a better way for it to work, but that is subject to taste, so I won't say that is definitive; I think you will have to agree it is simpler and easier to use, however)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1