haskell

Type-level tuple: how to avoid nesting in a type argument


Considering first-class-families library, I have a code:

import Fcf.Data.Common
import Fcf.Core

type T a b c = '(a, '(b, c))                                                                                                  
data D a b t = D                                                                                                              
  { a :: a                                                                                                                    
  , b :: b                                                                                                                    
  , c :: Eval (Fst t)                                                                                                         
  , d :: Eval (Fst (Eval (Snd t)))                                                                                            
  , e :: Eval (Snd (Eval (Snd t)))                                                                                            
  }                                                                                                                           
type MyT = T Int Int Bool                                                                                                     
data C = C (D Int Int MyT)

with nesting only because no Third (similarly to Fst and Snd). Is it possible to avoid this nesting in some more intelligent way? Maybe there is some operator allowing more flat "structure"? If no, what can be used instead of it?

Or from another point of view, maybe this code is not good:

Eval (Fst (Eval (Snd t)))

and there is a way to avoid the second Eval ?

Finally, I want D taking type-level tuple of size > 2 and to pass it in a "flat" form if it's possible.


Solution

  • In case this is an X-Y problem, please note that you don't need the first-class-families package to implement simple type-level functions. You can just use type families directly. So, the following will work fine:

    {-# LANGUAGE TypeFamilies #-}
    
    type family Fst triple where
      Fst (a,b,c) = a
    type family Snd triple where
      Snd (a,b,c) = b
    type family Thd triple where
      Thd (a,b,c) = c
    
    type T a b c = (a, b, c)
    data D a b t = D
      { a :: a
      , b :: b
      , c :: Fst t
      , d :: Snd t
      , e :: Thd t
      }
    type MyT = T Int Int Bool
    data C = C (D Int Int MyT)
    

    Alternatively, you can define:

    type T a b c = '(a, b, c)
    

    and modify the type family definitions with ticks:

    type family Fst triple where
      Fst '(a,b,c) = a
    -- etc. --
    

    which will work too.

    Two observations:

    First, there's actually a big conceptual difference between the first "no ticks" version and the second "ticks" version. It's the type-level equivalent of working in a dynamically typed language ("no ticks") and a statically typed language ("ticks"), but if you don't have a compelling reason to use the "ticks" version, you should stick with the "no ticks" version, as it's more straightforward and perfectly idiomatic and -- like dynamically typed programming in general -- comes with some usability advantages, like allowing you to define:

    type family Fst tuple where
      Fst (a,b) = a
      Fst (a,b,c) = a
      Fst (a,b,c,d) = a
      -- and so on --
    

    and use the same Fst function on different length tuples.

    Second, the purpose of first-class-families is to permit partially applied type-level functions. If you needed to pass Fst to a Map function, for example:

    type Result = Map Fst '[(Double,Char,String),(Int,Double,Double)]
    

    then the type family definitions in this answer wouldn't work because type families can only appear in type expressions fully applied to all their arguments.

    If you don't need to use partially (or unapplied) type-level functions, then you don't need first-class-families.