Indexing into containers: the mathematical underpinnings

后端 未结 1 560
情话喂你
情话喂你 2021-02-05 07:34

When you want to pull an element out of a data structure, you have to give its index. But the meaning of index depends on the data structure itself.

cla         


        
相关标签:
1条回答
  • 2021-02-05 08:05

    It seems like the index into the type is an index into the set of constructors, following by an index into the product representing that constructor. This can be implemented quite naturally with e.g. generics-sop.

    First you need a datatype to represent possible indices into a single element of the product. This could be an index pointing to an element of type a, or an index pointing to something of type g b - which requires an index pointing into g and an index pointing to an element of type a in b. This is encoded with the following type:

    import Generics.SOP
    
    data ArgIx f x x' where 
      Here :: ArgIx f x x 
      There :: (Generic (g x')) => Ix g -> ArgIx f x x' -> ArgIx f x (g x') 
    
    newtype Ix f = ...
    

    The index itself is just a sum (implemented by NS for n-ary sum) of sums over the generic representation of the type (choice of constructor, choice of constructor element):

    newtype Ix f = MkIx (forall x . NS (NS (ArgIx f x)) (Code (f x)))
    

    You can write smart constructors for various indices:

    listIx :: Natural -> Ix [] 
    listIx 0 = MkIx $ S $ Z $ Z Here 
    listIx k = MkIx $ S $ Z $ S $ Z $ There (listIx (k-1)) Here  
    
    treeIx :: [Bool] -> Ix Tree 
    treeIx [] = MkIx $ S $ Z $ S $ Z Here 
    treeIx (b:bs) = 
      case b of 
        True -> MkIx $ S $ Z $ Z $ There (treeIx bs) Here 
        False -> MkIx $ S $ Z $ S $ S $ Z $ There (treeIx bs) Here 
    
    roseIx :: [Natural] -> Ix Rose 
    roseIx [] = MkIx $ Z $ Z Here  
    roseIx (k:ks) = MkIx $ Z $ S $ Z $ There (listIx k) (There (roseIx ks) Here)
    

    Note that e.g. in the list case, you cannot construct an (non-bottom) index pointing to the [] constructor - likewise for Tree and Empty, or constructors containing values whose type is not a or something containing some values of type a. The quantification in MkIx prevents the construction bad things like an index pointing to the first Int in data X x = X Int x where x is instantiated to Int.

    The implementation of the index function is fairly straightforward, even if the types are scary:

    (!) :: (Generic (f x)) => f x -> Ix f -> Maybe x 
    (!) arg (MkIx ix) = go (unSOP $ from arg) ix where 
    
      atIx :: a -> ArgIx f x a -> Maybe x 
      atIx a Here = Just a 
      atIx a (There ix0 ix1) = a ! ix0 >>= flip atIx ix1 
    
      go :: (All SListI xss) => NS (NP I) xss -> NS (NS (ArgIx f x)) xss -> Maybe x 
      go (Z a) (Z b) = hcollapse $ hzipWith (\(I x) -> K . atIx x) a b 
      go (S x) (S x') = go x x' 
      go Z{} S{} = Nothing 
      go S{} Z{} = Nothing 
    

    The go function compares the constructor pointed to by the index and the actual constructor used by the type. If the constructors don't match, the indexing returns Nothing. If they do, the actual indexing is done - which is trivial in the case that the index points exactly Here, and in the case of some substructure, both indexing operations must succeed one after the other, which is handled by >>=.

    And a simple test:

    >map (("hello" !) . listIx) [0..5]
    [Just 'h',Just 'e',Just 'l',Just 'l',Just 'o',Nothing]
    
    0 讨论(0)
提交回复
热议问题