I want to specify function result type based on the type of first argument. To define this dependecy I define type family MaybeWrapper. It wrap some result type in Maybe if first argument is Opt type (Optional). Opt and Req a singelton types. And I want them to define result type Opt - > Maybe v and Req -> v
Here the code:
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
import Data.Typeable
import Data.Type.Equality
-- singelton types:
data Opt = Opt deriving Show
data Req = Req deriving Show
type family MaybeWrapper a v where
MaybeWrapper Opt v = Maybe v
MaybeWrapper _ v = v
f1 :: forall a v.Typeable a => a -> v -> MaybeWrapper a v
f1 a vv
| Just Refl <- eqT @a @Opt = Just vv
| otherwise = vv
f2 :: a -> v -> MaybeWrapper a v
f2 Opt vv = Just vv
f2 _ vv = vv
But all my attemts 1 and 2 fail.
Error:
• Couldn't match expected type ‘MaybeWrapper a v’ with actual type ‘v’
I expect that "MaybeWrapper a v" is not a type but a type function which must resolve to v type with Opt argument and types must match.
How can I fix this?
Added:
So I found that i can fix it with typeclasses:
class CMaybe a where
type MWrap a v
vf :: a -> v -> MWrap a v
instance CMaybe Opt where
type MWrap Opt v = Maybe v
vf _ = Just
instance CMaybe Req where
type MWrap Req v = v
vf _ v = v
But why haskell can't infer types without them here?
First, your second attempt is a complete non-starter. You simply cannot match a value whose type is simply a variable like
aagainst a pattern for one specific type (such asOpt, which is a pattern for a type namedOpt). There are loads of questions and answers on Stack Overflow going over this in more detail, so I won't elaborate here.Your first attempt is more interesting, so let's consider why that doesn't work.
First, I hope it is obvious why this doesn't work:
When compiling the expression
vvon the right hand side the compiler has no information about what typeais (orvfor that matter). So when it looks at the definition ofMaybeWrapper a v, it doesn't have any proof thataisOptand thus cannot take the first branch and conclude thatMaybeWrapper a v = Maybe v. But it also doesn't have any proof thatais notOpt; so it can't take the second branch either.What it is left with is that the type of the right hand side is expected to be simply
MaybeWrapper a v; with no information to go on it can't reduce the type family to any specific type. That's why you get an error like:It's not saying that
MaybeWrapper a vis some concrete type, it's saying it has no idea what concrete type that evaluates to so it cannot match anything else with it.(If
f1took an argument that had typeMaybeWrapper a v, it would be able to conclude that the argument value had the same type as the return value, and so you could return that even without reducing the type family. But you can't really do anything else with a value whose type is an unreduced type family)So then we can look at your actual
f1:In the first branch, you use
TypeableandeqT. This allows you to write a specific branch of the code where the compiler considers that it has evidence thata ~ Opt. This means that it can reduceMaybeWrapper a vtoMaybe v, and so you can returnJust vvon that branch. If you look carefully at the error message you get, you will see that it is not complaining about this line; the compiler is happy here.But the additional evidence only applies inside the scope of the
Just Reflpattern match. Outside the pattern match all the code must compile with the same total lack of any information about the typea. You might wish that being in a branch that could only be reached ifeqTfailed to match would give the compiler evidence of type inequality, but that's simply not how it works.(When
eqTtests the types and finds thay are equal, it gives you backJust Refl, where theReflis a special value that provides evidence of type equality to the compiler. But wheneqTfinds the types aren't equal, it just gives you backNothing, which doesn't contain any evidence the type checker can use; and you didn't even pattern match on it so you wouldn't have got the evidence even if it was there).So you can sortof make your code work by using
eqTagain:Now both of your two branches are compiled with additional knowledge about the type of
a. Knowing thataisReqis enough to reduceMaybeWrapper a vtov, so on that branch you can returnvvas you wanted.The problem with this is that now your guards are not exhaustive (and the compiler will warn you about this if you use
-Wall). You know that you want this parameter to beOptorReq, but what you've actually said is that it can be any typeathat has aTypeableinstance. Try writing a file where you call things likef1 True (), orf1 (putStrLn "whoops") (); the type checker will happily allow it, but running that code will fail at runtime withNon-exhaustive patterns in function f1. So this is not very type-safe.A better way is to define
OptandReqtogether as the only possible members of a new kind, instead of defining them as separate types. Then you can define a GADT of singletons that provide the ability to pattern match on a value to find out whether a type parameter wasOptorReq(and disallow any other type). With that setup, all you need is regular pattern matching to do what you want; noTypeableor custom type class. It looks like this (compiles as-is with GHC 9.4.8; you might need to enable some more extensions in older versions):(Note that I've used annoyingly long names for clarity of the example; normally when applying this pattern people use much shorter names;
SingorSas prefix/suffix are quite common)You're probably wondering why you couldn't just use
OptandReqas the first argument tof; why did I mess around with the whole separateOptionalitySingletontype? But if we used the value levelOptandReqthen the type of that paramter would have to be justOptionality, the type offwould have to look like this:Here the
ainMaybeWrapper a vis totally unrelated to theOptionalityfirst parameter, so we don't learn anything aboutaby pattern matching on theOptionalityvalue. And there's no variable in the typeOptionality, so there's nothing we can connect toa.Whereas the type-level
OptandReqdon't have any values at all (only types in the kindTypehave values; type-levelOptandReqare in the kindOptionalityinstead), so there's nothing we could pass tof. Even if we could, they're different types, so there are no patterns that would be valid for both of them that we could use when we don't know what the type parameter is yet.So instead we have to define a separate type like
OptionalitySingleton(using thewheresyntax to define a type is a feature known as GADT or Generalised Algebraic Data Type syntax, if you haven't seen it and want to google for more information). This pattern takes a type parameter and then defines the type so that there is only a single possible constructor for each value of the type parameter (hence the term "singleton", which is completely unrelated to the term singleton in object-oriented programming). This means that pattern matching on the constructors completely identifies what the type parameter must be, and there is special support built into the compiler for making use of the evidence gained by pattern matching on GADTs.So
fcan just go ahead and pattern match on theOptionalitySingletonvalue and then use different return types in each branch. As above with the twoeqTbranches you still need to be explicit about what type you're matching in each branch. There's no way to use_orotherwiseand have a catch-all branch because we can still only write branches with positive evidence of type equality, we can't write a branch with usable evidence of type inequality. But now we have limited it so that the only possible types areOptandReq(or rather the only possible values areSingletonForOptandSingletonForReq); the compiler will warn us (with-Wall) if we forget to handle all the cases (very useful when you later extend theOptionalityto have a 3rd possibility and need to update every place you pattern match on it), and will in fact also warn us if we add another case inf(the compiler will notice that it must be redundant, as we've already matched everything that possibly could be matched).