LoginSignup
0
0

More than 5 years have passed since last update.

Reflecting a record of proxies and keys of row types

Posted at

The more I use anything related to type information, the more I've found that I don't want to actually write out the proxies that I know I'll need to use, so I usually end up putting these into records.

But the problem I have is that when I put these into records, I really only want to provide the type information, and I don't want to have to always update the value, as it's just tedious. For example:

myProxies ::
  { a :: Proxy Int
  , b :: Proxy String
  , c :: Proxy Unit
  }
myProxies =
  { a: Proxy
  , b: Proxy
  , c: Proxy
  }

Since I already have this type information, surely I can just reflect this record with my proxies? And I can!

Reflecting the record proxy

We can make a class that will reflect the value directly:

class ReflectRecordProxy a where
  reflectRecordProxy :: a

Then as usual, we can make our definition such that for a record, we take the row type and make a builder for it, where the builder will be supplied by another class which will make the builder out of the RowList of the row type:

instance reflectRecordProxyInst ::
  ( RL.RowToList r rl
  , ReflectRecordProxyBuilder rl () r
  ) => ReflectRecordProxy { | r } where
  reflectRecordProxy = Builder.build builder {}
    where
      builder = reflectRecordProxyBuilder (RLProxy :: RLProxy rl)

An explanation of Record.Builder can be found in some of my older posts like here: https://github.com/justinwoo/my-blog-posts#short-composed-modified-json-parsing-for-free-with-simple-json

Then for the individual builders, we know that we will want to reflect any proxy type, so we can define another class that allows for reflecting any proxy:

class ReflectRecordProxyBuilder (rl :: RL.RowList) (i :: # Type) (o :: # Type)
  | rl -> i o where
  reflectRecordProxyBuilder :: RLProxy rl -> Builder { | i } { | o }

instance reflectRecordProxyBuilderNil :: ReflectRecordProxyBuilder RL.Nil () () where
  reflectRecordProxyBuilder _ = identity

instance reflectRecordProxyBuilderConsRoute ::
  ( ReflectRecordProxyBuilder tail from from'
  , Row.Lacks name from'
  , Row.Cons name a from' to
  , ReflectProxy a
  , IsSymbol name
  ) => ReflectRecordProxyBuilder (RL.Cons name a tail) from to where
  reflectRecordProxyBuilder _ = first <<< rest
    where
      first = Builder.insert (SProxy :: SProxy name) reflectProxy
      rest = reflectRecordProxyBuilder (RLProxy :: RLProxy tail)

-- | Various proxies that can be created
class ReflectProxy a where
  reflectProxy :: a

instance reflectProxyProxy :: ReflectProxy (Proxy a) where
  reflectProxy = Proxy

instance reflectProxySProxy :: ReflectProxy (SProxy s) where
  reflectProxy = SProxy

instance reflectProxyRProxy :: ReflectProxy (RProxy s) where
  reflectProxy = RProxy

instance reflectProxyRLProxy :: ReflectProxy (RLProxy s) where
  reflectProxy = RLProxy

And this is actually all we need.

Putting this to use

Now we can simply supply only the type annotation and reflect the record value for free:

proxies = N.reflectRecordProxy ::
  { apple :: Proxy Int
  , banana :: Proxy String
  }

And we can use this with our own proxy data type by making an instance of ReflectProxy:

data MyThing a b c d e f g = MyThing

instance myThingReflectProxy :: N.ReflectProxy (MyThing a b c d e f g) where
  reflectProxy = MyThing

things = N.reflectRecordProxy ::
  { apple :: MyThing Int Int Int Int Int Int Int
  , banana :: MyThing Unit Unit Unit Unit Unit Unit Unit
  }

Bonus: collecting row keys

Say we have a concrete record type where we want to collect the keys and make an SProxy record. This ends up being not too much work either, where we first RowToList on the row type we are working off of and build up a row type by using Row.Cons constraints on it:

class GetKeysRow (r :: # Type) (keys :: # Type) | r -> keys

instance getKeysRowInst ::
  ( RL.RowToList r rl
  , GetKeysRowInst rl keys
  ) => GetKeysRow r keys

class GetKeysRowInst (rl :: RL.RowList) (keys :: # Type)
  | rl -> keys

instance getKeysRowInstNil :: GetKeysRowInst RL.Nil ()

instance getKeysRowInstCons ::
  ( GetKeysRowInst tail keys'
  , Row.Cons name (SProxy name) keys' keys
  ) => GetKeysRowInst (RL.Cons name ty tail) keys

Then we can define a variety of functions that will extract out the keys of a row type. For our purposes, we're mostly interested in proxies of types that act as row proxies, meaning some kind of data type of the kind signature

SomeDataType :: # Type -> Type

For which we know of a few:

-- from Prim
foreign import data Record :: # Type -> Type

-- from typelevel-prelude
data RProxy (row :: # Type) = RProxy

-- from variant
foreign import data Variant :: # Type -> Type

So we'll just write a function that takes any data type of that kind:

getKeysRecord'
  :: forall proxy rproxy r keys
   . GetKeysRow r keys
  => N.ReflectRecordProxy { | keys }
  => proxy (rproxy r)
  -> { | keys }
getKeysRecord' _ = N.reflectRecordProxy

I have previously written about polymorphic proxies here: https://github.com/justinwoo/my-blog-posts#polymorphic-proxy-fun-in-purescript

Putting it all together

First, we can just write out the application of this function with a Proxy of a record type:

keys = X.getKeysRecord' (Proxy :: Proxy { a :: Int, b :: Int, c :: Int })

And the PureScript IDE Server will give us this message:

No type declaration was provided for the top-level declaration of keys.
  It is good practice to provide type declarations as a form of documentation.
  The inferred type of keys was:

    { a :: SProxy "a"
    , b :: SProxy "b"
    , c :: SProxy "c"
    }


in value declaration keys

And we can use the inferred signature by applying the IDE suggestion in our editor:

keys ::
  { a :: SProxy "a"
  , b :: SProxy "b"
  , c :: SProxy "c"
  }
keys = X.getKeysRecord' (Proxy :: Proxy { a :: Int, b :: Int, c :: Int })

And then we can use any of the properties of keys to work with things like Record.insert and other functions that take SProxy values.

Conclusion

I hope this has shown that we can really derive a lot of values if we have enough type information, and that runtime values can fill in the gaps where we have them.

I also really hope that my links to my previous posts show that all of these blog posts just small parts of a large continuous whole, so nothing here is too different from the material covered in previous posts.

Links

P.S.

Is anyone interested in helping me write a Japanese post for Qiita? I have been thinking of writing a simpler version of Type classes and instances are pattern matching for types that would be useful for those who read my posts mostly for the code but not much of the actual words. My Japanese isn't very good, but hopefully you can help me fix many details if we start working on this.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0