imports

module Plutarch.Docs.WorkingWithBoundFields (foo, foo', coreValidator) where

import GHC.Records (getField)
import GHC.Generics (Generic)
import Plutarch.Prelude
import Plutarch.DataRepr (HRec, HRecOf, PMemberFields)
import Plutarch.LedgerApi.V3 (PTxInfo, PScriptContext)

Working with bound fields yielded by pletFields

You may have noticed that pletFields actually returns a Haskell level heterogenous list, with all the interesting fields "bound" to it. Only the fields you actually use from these bindings are extracted and put into the resulting script. Therefore, you only pay for what you use.

pletFields ::
  forall fs a s b ps bs.
  ( PDataFields a
  , ps ~ (PFields a)
  , bs ~ (Bindings ps fs)
  , BindFields ps bs
  ) =>
  Term s a ->
  (HRecOf a fs s -> Term s b) ->
  Term s b

The real juice of that massive type is the HRecOf, which is a utility type alias you can use in functions that operate on the return value of pletFields:

newtype PFooType s
  = PFooType (Term s (PDataRecord '["frst" ':= PInteger, "scnd" ':= PBool, "thrd" ':= PString]))
  deriving stock (Generic)
  deriving anyclass (PlutusType, PDataFields)
instance DerivePlutusType PFooType where type DPTStrat _ = PlutusTypeData

foo :: HRecOf (PAsData PFooType) '[ "frst", "scnd" ] s -> Term s PInteger
foo h = pif (getField @"scnd" h) (getField @"frst" h) 0

Note: Be careful to derive PDataFields on your type if it has only one constructor

This is very useful for single use functions that you use as "branches" in your validators - they work more like macros or templates rather than real functions. For example, you might have different branches for different constructors of a redeemer, but all branches end up needing to do common field extraction. You could abstract it out using:

data PSomeRedm s
  = FirstRedm (Term s (PDataRecord '[]))
  | SecondRedm (Term s (PDataRecord '[]))
  deriving stock (Generic)
  deriving anyclass (PlutusType, PIsData)
instance DerivePlutusType PSomeRedm where type DPTStrat _ = PlutusTypeData

firstRedmCheck ::
  HRecOf PTxInfo '[ "inputs", "outputs", "mint", "datums" ] s
  -> TermCont s (Term s PUnit)
firstRedmCheck _info = do
  -- Do checks with info fields here.
  pure $ pconstant ()

secondRedmCheck ::
  HRecOf PTxInfo '[ "inputs", "outputs", "mint", "datums" ] s
  -> TermCont s (Term s PUnit)
secondRedmCheck _info = do
  -- Do checks with info fields here.
  pure $ pconstant ()

coreValidator :: Term s (PData :--> PAsData PSomeRedm :--> PScriptContext :--> PUnit)
coreValidator = plam $ \_ (pfromData -> redm) ctx' -> unTermCont $ do
  ctx <- tcont $ pletFields @'["txInfo", "purpose"] ctx'
  info <- tcont $ pletFields @'["inputs", "outputs", "mint", "datums"] $ getField @"txInfo" ctx
  pmatchC redm >>= \case
    FirstRedm _ -> firstRedmCheck info
    SecondRedm _ -> secondRedmCheck info

Without it, you may have to fallback to deconstructing info with pletFields in every single branch.

However, this is rather nominal. What if you don't need the exact same fields in all branches? Let's go back to the example with foo and FooType. What if someone has:

fooTypeHrec <- tcont $ pletFields @'["frst", "scnd", "thrd"] fooTypeValue
foo fooTypeHrec
-- uh oh

The type required by foo should morally work just fine with fooTypeHrec, but it won't! What we really want, is some sort of row polymorphism. This is where the PMemberFields type from Plutarch.DataRepr comes in:

foo' :: PMemberFields PFooType '["scnd", "frst"] s as => HRec as -> Term s PInteger
foo' h = pif (getField @"scnd" h) (getField @"frst" h) 0

Now foo merely requires the HRec to have the "scnd" and "frst" fields from PFooType, more fields are allowed just fine!