Why is the type system refusing my seemingly valid program?

后端 未结 2 947
清歌不尽
清歌不尽 2021-01-30 12:05

Mind this program:

class Convert a b where
    convert :: a -> b

data A = A deriving Show
data B = B deriving Show
data C = C deriving Show
data D = DA A | D         


        
相关标签:
2条回答
  • 2021-01-30 12:30

    If your program really seemed valid to you, then you would be able to write the type of get that does the job you want in Haskell, not in handwave. Let me help you improve your handwave and uncover the reason you are asking for the moon on a stick.

    What I want to express is: get :: (Convert a_contained_by_D b) => D -> b, which seems impossible.

    As stated, that's not quite as precise as you would need. Indeed, it's what Haskell gives you now, in that

    get :: (Convert A b, Convert B b, Convert C b) => D -> b
    

    any a which can be contained by D is required, one at a time, to be convertible to that b. And that's why you're getting classic sysadmin logic: no D is allowed to be gotten unless they all can b.

    The problem is that you need to know the status not of the type which might be contained in any old D, but rather the type contained in the particular D that you receive as the input. Right? You want

    print (get (DB B) :: A)  -- this to work
    print (get (DC C) :: A)  -- this to fail
    

    but DB B and DC C are just two different elements of D, and as far as the Haskell type system is concerned, within each type everything different is the same. If you want to discriminate between elements of D, then you need a D-pendent type. Here's how I'd write it in handwave.

    DInner :: D -> *
    DInner (DA a) = A
    DInner (DB b) = B
    DInner (DC c) = C
    
    get :: forall x. pi (d :: D) -> (Convert (DInner d) x) => x
    get (DA x) = convert x
    get (DB x) = convert x
    get (DC x) = convert x
    

    where pi is the binding form for data which are passed at run time (unlike forall) but on which types may depend (unlike ->). Now the constraint is talking not about arbitrary Ds but the very d :: D in your hand, and the constraint can compute exactly what is needed by inspecting its DInner.

    There is nothing you can say that will make it go away but my pi.

    Sadly, whilst pi is rapidly descending from the sky, it has not yet landed. None the less, unlike the moon, it can be reached with a stick. No doubt you will complain that I am changing the setup, but really I am just translating your program from Haskell in approximately 2017 to Haskell in 2015. You'll get it back, one day, with the very type I handwaved.

    There is nothing you can say, but you can sing.

    Step 1. Switch on DataKinds and KindSignatures and build the singletons for your types (or get Richard Eisenberg to do it for you).

    data A = A deriving Show
    data Aey :: A -> * where  -- think of "-ey" as an adjectival suffix
      Aey :: Aey 'A           -- as in "tomatoey"
    
    data B = B deriving Show
    data Bey :: B -> * where
      Bey :: Bey 'B
    
    data C = C deriving Show
    data Cey :: C -> * where
      Cey :: Cey 'C
    
    data D = DA A | DB B | DC C deriving Show
    data Dey :: D -> * where
      DAey :: Aey a -> Dey (DA a)
      DBey :: Bey b -> Dey (DB b)
      DCey :: Cey c -> Dey (DC c)
    

    The idea is (i) that datatypes become kinds, and (ii) that singletons characterize the type-level data which have a run time presentation. So type level DA a exists at run time provided a does, etc.

    Step 2. Guess who's coming to DInner. Switch on TypeFamilies.

    type family DInner (d :: D) :: * where
      DInner (DA a) = A
      DInner (DB b) = B
      DInner (DC c) = C
    

    Step 3. Get you some RankNTypes, and now you can write

    get :: forall x. forall d. Dey d -> (Convert (DInner d) x) => x
    --               ^^^^^^^^^^^^^^^^^^
    -- this is a plausible fake of pi (d :: D) ->
    

    Step 4. Try to write get and screw up. We have to match on the run time evidence that the type level d is representable. We need that to get the type level d specialised in the computation of DInner. If we had proper pi, we could match on a D value that serves double duty, but for now, match on Dey d instead.

    get (DAey x) = convert x   -- have x :: Aey a, need x :: A
    get (DBey x) = convert x   -- and so on
    get (DCey x) = convert x   -- and so forth
    

    Maddeningly, our xes are now singletons, where, to convert, we need the underlying data. We need more of the singleton apparatus.

    Step 5. Introduce and instantiate the singleton class, to "demote" type level values (as long as we know their run time representatives). Again, Richard Eisenberg's singletons library can Template-Haskell the boilerplate out of this, but let's see what's going on

    class Sing (s :: k -> *) where   -- s is the singleton family for some k
      type Sung s :: *               -- Sung s is the type-level version of k
      sung :: s x -> Sung s          -- sung is the demotion function
    
    instance Sing Aey where
      type Sung Aey = A
      sung Aey = A
    
    instance Sing Bey where
      type Sung Bey = B
      sung Bey = B
    
    instance Sing Cey where
      type Sung Cey = C
      sung Cey = C
    
    instance Sing Dey where
      type Sung Dey = D
      sung (DAey aey) = DA (sung aey)
      sung (DBey bey) = DB (sung bey)
      sung (DCey cey) = DC (sung cey)
    

    Step 6. Do it.

    get :: forall x. forall d. Dey d -> (Convert (DInner d) x) => x
    get (DAey x) = convert (sung x)
    get (DBey x) = convert (sung x)
    get (DCey x) = convert (sung x)
    

    Be assured, when we have proper pi, those DAeys will be actual DAs and those xs will no longer need to be sung. My handwave type for get will be Haskell, and your code for get will be fine. But in the meantime

    main = do
      print (get (DCey Cey) :: B)
      print (get (DBey Bey) :: A)
    

    typechecks just fine. That's to say, your program (plus DInner and the correct type for get) seems like valid Dependent Haskell, and we're nearly there.

    0 讨论(0)
  • 2021-01-30 12:30

    For that code to work, it would have to work with any argument of the same type. That is, if

    get (DB B) :: A
    

    works then

    get anyValueOfTypeD :: A
    

    has to work, including

    get (DC C) :: A
    

    which can not work because of a missing instance from C to A.

    The first line

    get anyValueOfTypeD :: B
    

    works because we do have all the three instances to convert A,B,C to B.

    I do not think there's any workaround that allows you to leave the type D as it is. If instead you can change that, you could use e.g.

    data D a = DA a | DB a | DC a
    

    (mind that it's quite different from the original), or even the GADT

    data D x where
      DA :: A -> D A
      DB :: B -> D B
      DC :: C -> D C
    
    0 讨论(0)
提交回复
热议问题