I really like using coerce. It allows you to skip tedious manual wrapping and unwrapping of newtypes. The instance for functions is especially useful, avoiding you the drudgery of going through all the positional parameters before reaching the result that you want to change.
Yet sometimes coerce
is a bit too powerful. When using it with complex types, I start to second-guess myself, hoping that I’m not mistakenly plugging some value which happens to fit because some newtype I hadn’t considered is unexpectedly unwrapped.
For example, recently I had the need to work with deeply nested compositions of functors. Something like:
import Data.Functor.Compose
type Phases = IO `Compose` IO `Compose` IO `Compose` IO
defining values of this type is very tedious because of the Compose
spam:
value :: Phases Int
value = Compose (pure (Compose (pure (Compose (pure (pure 5))))))
-- in the real case, each applicative layer performs actual effects
It’s simpler to define a newtype-less value, and then coerce
it:
bareValue :: IO (IO (IO (IO Int)))
bareValue = pure $ pure $ pure $ pure 5
-- in the real case, each applicative layer performs actual effects
value2 :: Phases Int
value2 = coerce bareValue
Alas, suppose we had some other value like this lying around:
-- We don't want this newtype to be coerced, but we might do it by accident.
newtype DoNotUnwrap = DoNotUnwrap Int
dangerousBareValue :: IO (IO (IO (IO DoNotUnwrap)))
dangerousBareValue = pure $ pure $ pure $ pure (DoNotUnwrap 5)
coerce
works fine with it:
-- bad!
value3 :: Phases Int
value3 = coerce dangerousBareValue
What we need is a restricted form of coerce
which only removes the Compose
newtypes, and nothing more. To express "only removes the Compose
" we can use a type family:
type family Bare x where
Bare (Compose outer inner x) = Bare (outer (Bare (inner x)))
Bare other = other
With Bare
, we can define a restricted coerce
and use it like this:
fromBare :: Coercible x (Bare x) => Bare x -> x
fromBare = coerce
value4 :: Phases Int
value4 = fromBare bareValue
-- doesn't compile (good!)
-- value5 :: Phases Int
-- value5 = fromBare dangerousBareValue
Another example. The Servant library expects its handlers to work in ExceptT
. But suppose that all your candidate handlers work by returning an Either
:
type Handler = Int -> String -> ExceptT String IO Int
eitherHandler :: Int -> String -> IO (Either String Int)
eitherHandler = undefined
-- ugh! tedious to write
manuallyAdaptedHandler :: Handler
manuallyAdaptedHandler i s = ExceptT (eitherHandler i s)
As before, we can use coerce
directly:
handler2 :: Handler
handler2 = coerce eitherHandler
but again we have problematic cases:
dangerousEitherHandler :: Int -> String -> IO (Either String DoNotUnwrap)
dangerousEitherHandler = undefined
-- bad!
handler3 :: Handler
handler3 = coerce dangerousEitherHandler
We can play the same type family trick:
type family BareH x where
BareH (ExceptT e IO r) = IO (Either e r)
BareH (input -> output) = input -> BareH output
BareH other = TypeError (Text "unexpedted value at the tip: "
:<>: ShowType other)
fromBareH :: Coercible x (BareH x) => BareH x -> x
fromBareH = coerce
handler4 :: Handler
handler4 = fromBareH eitherHandler
-- doesn't compile (good!)
-- handler5 :: Handler
-- handler5 = fromBareH dangerousEitherHandler
The full gist is here.
So, how do you restrain the unbridled power of coerce
?