开发者

One value constructor belonging to two different types

开发者 https://www.devze.com 2023-01-26 06:37 出处:网络
Let\'s say I have three value constructors: A { a :: Int } B { b :: Char } C { c :: Bool } I would like to create tw开发者_如何学运维o types X and Y such that a value of type X can be an A, B or C,

Let's say I have three value constructors:

A { a :: Int }
B { b :: Char }
C { c :: Bool }

I would like to create tw开发者_如何学运维o types X and Y such that a value of type X can be an A, B or C, something like this:

data X = A {...} | B {...} | C {...}

and a value of type Y can be only an A or B, something like this:

data Y = A {...} | B {...}

so that I can code something like this:

foo :: X -> Int -- can pattern match
foo (A _) = 1
foo (B _) = 2
foo (C _) = 3
bar :: Y -> Bool -- also can pattern match with the same constructors
bar (A _) = true
bar (B _) = false
baz = A 1 -- baz is inferred to be a type that can fit in both X and Y

I know that I can wrap the constructors in the definitions of X and Y like this:

data X = XA A | XB B | XC C
data Y = YA A | YB B

but this seems untidy (having to type XA A etc. all the time). I could expand the contents of A, B, and C into the definitions of X and Y, but A etc. are quite complicated and I would prefer not to duplicate the definition.

Is this possible with Haskell, including any GHC extensions?

Edit

It seems that GADTs can answer my question as asked (so I marked heatsink's answer as correct), but still aren't flexible enough for what I need. For example, as far as I know, you can't do something like:

func1 :: [XY Y_] -- returns a list of items that can only be A or B
func1 = ...
func2 = func1 ++ [C True] -- adding a C item to the list

func2 should be typed as [XY X_], but that isn't possible in Haskell (unless my experiment was wrong).

After more web searching, what I really want is OCaml's polymorphic variants which (as far as I know) exist only in OCaml (looking at the more "practical" as opposed to "academic" languages).

Edit 2

See comonad's answer. It seems that it really can be done, but I think I'd better not rewrite this question too many times. :-)


Type classes, as jetxee described, are probably the appropriate approach.

If you also want the ability to pattern match and use constructors, then you can define all the constructors within one data type using GADTs and empty data declarations. If you take this approach, all the constructors will be members of the same data type, while letting you restrict the domain to only a subset of the constructors.

data X_
data Y_
data XY a where
  A :: Int -> XY a
  B :: Char -> XY a
  C :: Bool -> XY X_
type X = XY X_ -- Contains values built with constructors A, B, and C 
type Y = XY Y_ -- Contains only values built with constructors A and B

Now a function that uses only A and B works with both types X and Y. A function that uses C will work only with type X.


baz = A 1 -- baz is inferred to be a type that can fit in both X and Y

This would require Haskell to support some form of subtyping, which it does not. There are no ghc extensions that enable this either.

The best thing you can do is probably something like this:

data Y = A ... | B ...
data X = XY Y | C ...

This way you don't have to repeat the constructors A and B and you don't have to write Y (A foo) either - you can just write A foo to get a value of type Y.

However you will have to write X (A foo) to get a value of type X which contains an A. This isn't quite what you want, but the closest you will get, I'm afraid.


Your definitions for foo and bar won't type check, because A _ is by definition a value of type A, not X or Y. You cannot have another type (X) with the same constructor. So, the correct thing is what you've written:

data X = XA A | XB B | XC C
data Y = YA A | YB B

But let's approach it from another perspective. Why do you need that? You want to express, that X can be either, A, B, or C, and Y can be either A or B. You don't care about values of A, B and C respectively. So being A and being B are features common to both X and Y.

When you have a common trait beteen two types (X and Y in this case), you can often express it with type classes. Please note that type classes are open, so as many types can implement them as you like.

For example, we can define three type classes which allow to check if the type has A, B or C:

class HasA t where hasA :: t -> Bool
class HasB t where hasB :: t -> Bool
class HasC t where hasC :: t -> Bool

Now for our types we still have to use distinct data constructors:

data A = A Int
data B = B Char
data C = C Bool
data X = XA A | XB B | XC C
data Y = YA A | YB B

But we can define class instances both for X and for Y:

instance HasA X where
  hasA (XA _) = True
  hasA _ = False
instance HasB X where 
  hasB (XB _) = True
  hasB _ = False
instance HasC X where
  hasC (XC _) = True
  hasC _ = False

instance HasA Y where 
  hasA (YA _) = True
  hasA _ = False
instance HasB Y where 
  hasB (YB _) = True
  hasB _ = False
instance HasC Y where 
  hasC = const False

With these type classes you can write foo and bar which accept both Xs and Ys.

foo :: (HasA t, HasB t, HasC t) => t -> Int
foo v | hasA v = 1
      | hasB v = 2
      | hasC v = 3
      | otherwise = undefined
bar :: (HasA t, HasB t) => t -> Bool
bar v | hasA v = True
      | hasB v = False
      | otherwise = undefined

xs = [ XA (A 1), XB (B '1'), XC (C True) ]
ys = [ YA (A 1), YB (B '1') ]

More precisely, foo accepts anything which implements HasA, HasB and HasC, and bar accepts anything which implements HasA and HasB (whether or not HasC is implemented is irrelevant in the context of bar). If the implementation happens to return False in every case, then foo and bar are undefined.

For example:

ghci> map foo xs
[1,2,3]
ghci> map foo ys
[1,2]
ghci> map bar xs
[True,False,*** Exception: Prelude.undefined
ghci> map bar ys
[True,False]

Please note that bar accepts also Xs, but it is undefined if it happens to be something else except A or B. You as a programmer, not the compiler, are responsible to think about exhaustiveness of the guards in this case.

If you need also the values of A, B and C, then you have to design the type classes differently, e.g. like

class HasA t where getA :: t -> Maybe A

but the idea is the same.


Using Heatsink's answer, I kame up with this:

{-# LANGUAGE GADTs,EmptyDataDecls #-}
module Test where


data NotThatY
data XY a where
  A :: Int -> XY a
  B :: Char -> XY a
  C :: Bool -> XY NotThatY

type Y a = XY a
type X a = XY NotThatY -- or type X =..., but (X a) looks better alongside (Y a).

func1 :: [Y a]
func1 = [A 5, B 'ö']
func2 :: [X a]
func2 = func1 ++ [C True]

The restriction to Y is removed. Now it works, but it looks somehow strange with that a in the type.

type Y = forall a. XY a -- does not work.

0

精彩评论

暂无评论...
验证码 换一张
取 消