Given a record of functions, and a record of data of the types acted on by the functions, how to generically apply the function record?

后端 未结 1 694
粉色の甜心
粉色の甜心 2021-01-05 15:08

The title is slightly inaccurate as my particular case is a bit more involved: rather than the functions in the first record acting directly on values in the second record,

相关标签:
1条回答
  • 2021-01-05 15:26

    Now RecSummaryFuns a should have the same "field names" (constructor arguments) as a

    This answer uses red-black-record to construct "generalized records" that have the same field names as the original Foo record. First we must auto-derive some supporting typeclasses:

    {-# LANGUAGE AllowAmbiguousTypes #-}
    {-# LANGUAGE DataKinds #-}
    {-# LANGUAGE DeriveAnyClass #-}
    {-# LANGUAGE DeriveGeneric #-}
    {-# LANGUAGE PartialTypeSignatures #-}
    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE TypeFamilies #-}
    {-# LANGUAGE TypeApplications #-}
    {-# OPTIONS_GHC -Wno-partial-type-signatures #-} -- hide some scary types
    
    import Data.RBR (FromRecord (..), Record, ToRecord (..), fromNP, insert, toNP, unit)
    import Data.SOP (I (I), NP) -- from sop-core
    import Data.SOP.NP (liftA2_NP, liftA_NP) --   useful functions for n-ary products
    import GHC.Generics
    
    data Foo
      = Foo
          { v1 :: Int,
            v2 :: Double
          }
      deriving (Show, Generic, FromRecord, ToRecord)
    

    Now we can define a value of our generalized record, whose fields will hold functions. Sadly, we can't employ the usual record syntax:

    newtype Func a = Func ([a] -> a) -- helper newtype encapsulating the function
    
    type FooFunc = Record Func (RecordCode Foo) -- wrap every field in Func
    
    exampleFunc :: FooFunc
    exampleFunc =
          insert @"v1" (Func head) -- field names give with TypeApplications
        . insert @"v2" (Func last) -- same order as in the original record
        $ unit -- unit is the empty record
    

    The next step is defining this generic apply function with the help of the n-ary product datatype provided by sop-core:

    applyFunc ::  _ => Record Func _ -> [r] -> r
    applyFunc func foos =
      let foos_NP :: [NP I _] -- a list of n-ary products. I is an identity functor
          foos_NP = toNP . toRecord <$> foos
          listfoos_NP :: [NP [] _] -- turn every component into a singleton list
          listfoos_NP = liftA_NP (\(I x) -> [x]) <$> foos_NP
          listfoo_NP :: NP [] _ -- a single n-ary product where each component is a list
          listfoo_NP = mconcat listfoos_NP
          func_NP :: NP Func _ -- turn the function record into a n-ary prod
          func_NP = toNP func
          resultFoo_NP_I :: NP I _ -- apply the functions to each list component
          resultFoo_NP_I = liftA2_NP (\(Func f) vs -> I (f vs)) func_NP listfoo_NP
       in fromRecord . fromNP $ resultFoo_NP_I -- go back to the nominal record Foo
    

    Putting it all together:

    main :: IO ()
    main =
      print $
        applyFunc exampleFunc [Foo 0 0.0, Foo 1 1.0]
    -- result: Foo {v1 = 0, v2 = 1.0}
    

    Possible disadvantages of this solution are longer compilation times, and also the fact that turning the list-of-Foos into a Foo-with-list-fields inside applyFunc might be inefficient for long lists.

    We could ditch red-black-record—we are only using it to preserve field names in the generalized records—and rely on sop-core / generics-sop directly; in that case field names would be handled differently—or we could simply rely on positional matching.

    0 讨论(0)
提交回复
热议问题