jcpetruzza / barbies

BSD 3-Clause "New" or "Revised" License
92 stars 15 forks source link

Support for capability-style records #19

Open schell opened 4 years ago

schell commented 4 years ago

I really don't know if this is possible, but I'm referring to capability records like:

data LogCap (m :: Type -> Type) 
  = LogCap 
  { logInfo :: String -> String -> m ()
  , logError :: String -> String -> m () 
  } deriving Generic

From the outside these records look like HKDs - every field contains the type variable, but you might consider function parameters being "hidden" from it (one of the disclaimers from ProductB).

The first problem to solve is the FunctorB instance, as GFunctorB doesn't cut it:

• No instance for (barbies-1.1.3.0:Data.Barbie.Internal.Functor.GFunctorB
                         *
                         *
                         f
                         g
                         (Rec
                            *
                            (String
                             -> String
                             -> barbies-1.1.3.0:Data.Generics.GenericN.Param * * 0 f ())
                            (String -> String -> f ()))
                         (Rec
                            *
                            (String
                             -> String
                             -> barbies-1.1.3.0:Data.Generics.GenericN.Param * * 0 g ())
                            (String -> String -> g ())))

Intuitively though, it seems that we should be able to traverse an arrow with (f a -> g a), drilling deeper by creating lambdas until we come to an m a.

The point of all this is that at the end of the day it would be nice to be able to write a generic lift function for any of these forms of HKDT.

Given:

newtype Apply (f :: Type -> Type) (g :: Type -> Type) a
  = Apply { runApply :: f a -> g a }

bapply
  :: ProductB b
  => b (Apply i o)
  -> b i
  -> b o
bapply =
  bzipWith runApply

You could easily do something like:

let 
  ioLogCap :: LogCap IO 
  ioLogCap = ioLoggingCapabilities 

  resourceTLogCap :: LogCap (ResourceT IO) 
  resourceTLogCap = bapply (buniq $ Apply lift) ioLogCap
in 
  runResourceT $ f resourceTLogCap

which could kill a pleasant amount of boilerplate when using capability records.

Thoughts? I might be crazy ¯_(ツ)_/¯

jcpetruzza commented 4 years ago

I think you are definitely right, and we should be able to handle this case. Conceptually, the main thing that we are missing is to allow the functor argument (e.g. m in LogCap m) to occur under another functor when deriving FunctorB (and similarly for the other classes). At the moment we allow a barbie type to occur under a functor (e.g. Maybe (LogCap m)), but not the functor argument itself. This shouldn't be hard to fix.

Because we have an instance Functor (-> r), this addition alone would let us handle simpler capability records such as:

data LogCap (m :: Type -> Type)
  { logInfo  :: String -> m ()
  , logError :: String -> m ()
  }
  deriving (Generic)

instance FunctorB LogCap

Of course, one would like to handle functions with an arbitrary number of parameters (not just one) or, more generally, an unlimited number of nested functors. This is in fact a bit tricky; but ultimately doable, I think (e.g., one could identify arbitrary chains of functor applications in the generic representation of the type, coerce the type into a version where all the functors were smashed into one using Compose, operate on that single functor, and then coerce it back to the original representation).

A quicker workaround, would be to do a manual unrolling defining k instances of the form "the functor argument occurs under {1,2,3,...k} functors".