PIsDataRepr
allows for easily constructing and deconstructing Constr
BuiltinData
/Data
values. It allows fully type safe matching on Data
encoded values, without embedding type information within the generated script - unlike PlutusTx. PDataFields
, on top of that, allows for ergonomic field access.
Aside: What's a
Constr
data value? Briefly, it's how Plutus Core encodes non-trivial ADTs intoData
/BuiltinData
. It's essentially a sum-of-products encoding. But you don't have to care too much about any of this. Essentially, whenever you have a custom non-trivial ADT (that isn't just an integer, bytestring, string/text, list, or assoc map) - and you want to represent it as a data encoded value - you should implementPIsDataRepr
for it.
For example, PScriptContext
- which is the Plutarch synonym to ScriptContext
- has the necessary instances. This lets you easily keep track of its type, match on it, deconstruct it - you name it!
-- NOTE: REQUIRES GHC 9!
{-# LANGUAGE QualifiedDo #-}
import Plutarch.Prelude
import Plutarch.Api.V1.Contexts
import qualified Plutarch.Monadic as P
foo :: Term s (PScriptContext :--> PString)
foo = plam $ \ctx -> P.do
purpose <- pmatch pfield @"purpose" # ctx
case purpose of
PMinting _ -> "It's minting!"
PSpending _ -> "It's spending!"
PRewarding _ -> "It's rewarding!"
PCertifying _ -> "It's certifying!"
Note: The above snippet uses GHC 9 features (
QualifiedDo
). Be sure to check out Do syntax withTermCont
.
Of course, just like ScriptContext
- PScriptContext
is represented as a Data
value in Plutus Core. Plutarch just lets you keep track of the exact representation of it within the type system.
Here's how PScriptContext
is defined:
newtype PScriptContext (s :: S)
= PScriptContext
( Term
s
( PDataRecord
'[ "txInfo" ':= PTxInfo
, "purpose" ':= PScriptPurpose
]
)
)
It's a constructor containing a PDataRecord
term. It has 2 fields- txInfo
and purpose
.
First, we extract the purpose
field using pfield @"purpose"
:
pfield :: Term s (PScriptContext :--> PScriptPurpose)
Note: When extracting several fields from the same variable, you should instead use
pletFields
. See: Extracting fields
Aside:
pfield
is actually return type polymorhpic. It could've returned eitherPAsData PScriptPurpose
andPScriptPurpose
. In this case, GHC correctly infers that we actually want aPScriptPurpose
, sincepmatch
doesn't work onPAsData PScriptPurpose
!Sometimes GHC isn't so smart, and you're forced to provide an explicit type annotation. Or you can simply use
pfromData $ pfield ....
.
Now, we can pmatch
on our Term s PScriptPurpose
to extract the Haskell ADT (PScriptPurpose s
) out of the Plutarch term:
pmatch :: Term s PScriptPurpose -> (PScriptPurpose s -> Term s PString) -> Term s PString
Now that we have PScriptPurpose s
, we can just case
match on it! PScriptPurpose
is defined as:
data PScriptPurpose (s :: S)
= PMinting (Term s (PDataRecord '["_0" ':= PCurrencySymbol]))
| PSpending (Term s (PDataRecord '["_0" ':= PTxOutRef]))
| PRewarding (Term s (PDataRecord '["_0" ':= PStakingCredential]))
| PCertifying (Term s (PDataRecord '["_0" ':= PDCert]))
It's just a Plutarch sum type.
We're not really interested in the fields (the PDataRecord
term), so we just match on the constructor with the familar case
. Easy!
Let's pass in a ScriptContext
as a Data
value from Haskell to this Plutarch script and see if it works!
import Plutus.V1.Ledger.Api
import Plutus.V1.Ledger.Interval
import qualified PlutusTx
mockCtx :: ScriptContext
mockCtx =
ScriptContext
(TxInfo
mempty
mempty
mempty
mempty
mempty
mempty
(interval (POSIXTime 1) (POSIXTime 2))
mempty
mempty
""
)
(Minting (CurrencySymbol ""))
> foo `evalWithArgsT` [PlutusTx.toData mockCtx]
Right (Program () (Version () 1 0 0) (Constant () (Some (ValueOf string "It's minting!"))))
Aside: You can find the definition of
evalWithArgsT
at Compiling and Running.
We caught a glimpse of field extraction in the example above, thanks to pfield
. However, that barely touched the surface.
Once a type has a PDataFields
instance, field extraction can be done with these 3 functions:
pletFields
pfield
hrecField
(when not usingOverloadedRecordDot
or record dot preprocessor)
Each has its own purpose. However, pletFields
is arguably the most general purpose and most efficient. Whenever you need to extract several fields from the same variable, you should use pletFields
:
-- NOTE: REQUIRES GHC 9!
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE OverloadedRecordDot #-}
import Plutarch.Prelude
import Plutarch.Api.V1.Contexts
import qualified Plutarch.Monadic as P
foo :: Term s (PScriptContext :--> PUnit)
foo = plam $ \ctx' -> P.do
ctx <- pletFields @["txInfo", "purpose"] ctx'
let
purpose = ctx.purpose
txInfo = ctx.txInfo
-- <use purpose and txInfo here>
pconstant ()
Note: The above snippet uses GHC 9 features (
QualifiedDo
andOverloadedRecordDot
). Be sure to check out Do syntax withTermCont
and alternatives toOverloadedRecordDot
.
In essence, pletFields
takes in a type level list of the field names that you want to access and a continuation function that takes in an HRec
. This HRec
is essentially a collection of the bound fields. You don't have to worry too much about the details of HRec
. This particular usage has type:
pletFields :: Term s PScriptContext
-> (HRec
(BoundTerms
'[ "txInfo" ':= PTxInfo, "purpose" ':= PScriptPurpose]
'[ 'Bind, 'Bind]
s)
-> Term s PUnit)
-> Term s PUnit
You can then access the fields on this HRec
using OverloadedRecordDot
.
Next up is pfield
. You should only ever use this if you just want one field from a variable and no more. Its usage is simply pfield @"fieldName" # variable
. You can, however, also use pletFields
in this case (e.g. pletFields @'["fieldName"] variable
). pletFields
with a singular field has the same efficiency as pfield
!
Finally, hrecField
is merely there to supplement the lack of record dot syntax. See: Alternative to OverloadedRecordDot
.
Note: An important thing to realize is that
pfield
andhrecField
(or overloaded record dot onHRec
) are return type polymorphic. They can return bothPAsData Foo
orFoo
terms, depending on the surrounding context. This is very useful in the case ofpmatch
, aspmatch
doesn't work onPAsData
terms. So you can simply writepmatch $ pfield ...
andpfield
will correctly choose to unwrap thePAsData
term.
If OverloadedRecordDot
is not available, you can also try using the record dot preprocessor plugin.
If you don't want to use either, you can simply use hrecField
. In fact, ctx.purpose
above just translates to hrecField @"purpose" ctx
. Nothing magical there!
We learned about type safe matching (through PlutusType
) as well as type safe field access (through PDataFields
) - how about construction? Since PIsDataRepr
allows you to derive PlutusType
, and PlutusType
bestows the ability to not only deconstruct, but also construct values - you can do that just as easily!
Let's see how we could build a PMinting
PScriptPurpose
given a PCurrencySymbol
:
import Plutarch.Prelude
import Plutarch.Api.V1
currSym :: Term s PCurrencySymbol
purpose :: Term s PScriptPurpose
purpose = pcon $ PMinting fields
where
currSymDat :: Term _ (PAsData PCurrencySymbol)
currSymDat = pdata currSym
fields :: Term _ (PDataRecord '[ "_0" ':= PCurrencySymbol ])
fields = pdcons # currSymDat # pdnil
All the type annotations are here to help!
This is just like regular pcon
usage you've from PlutusType
/PCon
. It takes in the Haskell ADT of your Plutarch type and gives back a Plutarch term.
What's more interesting, is the fields
binding. Recall that PMinting
is a constructor with one argument, that argument is a PDataRecord
term. In particular, we want: Term s (PDataRecord '["_0" ':= PCurrencySymbol ])
. It encodes the exact type, position, and name of the field. So, all we have to do is create a PDataRecord
term!
Of course, we do that using pdcons
- which is just the familiar cons
specialized for PDataRecord
terms.
pdcons :: forall label a l s. Term s (PAsData a :--> PDataRecord l :--> PDataRecord ((label ':= a) ': l))
It takes a PAsData a
and adds that a
to the PDataRecord
heterogenous list. We feed it a PAsData PCurrencySymbol
term and pdnil
- the empty data record. That should give us:
pdcons # currSymDat # pdnil :: Term _ (PDataRecord '[ label ':= PCurrencySymbol ])
Cool! Wait, what's label
? It's the field name associated with the field, in our case, we want the field name to be _0
- because that's what the PMinting
constructor wants. You can either specify the label with a type application or you can just have a type annotation for the binding (which is what we do here). Or you can let GHC try and match up the label
with the surrounding environment!
Now that we have fields
, we can use it with PMinting
to build a PScriptPurpose s
and feed it to pcon
- we're done!
Implementing these is rather simple with generic deriving and PIsDataReprInstances
. All you need is a well formed type using PDataRecord
. For example, suppose you wanted to implement PIsDataRepr
for the Plutarch version of this Haskell type:
data Vehicle
= FourWheeler Integer Integer Integer Integer
| TwoWheeler Integer Integer
| ImmovableBox
You'd declare the corresponding Plutarch type as:
import Plutarch.Prelude
data PVehicle (s :: S)
= PFourWheeler (Term s (PDataRecord '["_0" ':= PInteger, "_1" ':= PInteger, "_2" ':= PInteger, "_3" ':= PInteger]))
| PTwoWheeler (Term s (PDataRecord '["_0" ':= PInteger, "_1" ':= PInteger]))
| PImmovableBox (Term s (PDataRecord '[]))
Each field type must also have a PIsData
instance. We've fulfilled this criteria above as PInteger
does indeed have a PIsData
instance. However, think of PBuiltinList
s, as an example. PBuiltinList
's PIsData
instance is restricted to only PAsData
elements.
instance PIsData a => PIsData (PBuiltinList (PAsData a))
Thus, you can use PBuiltinList (PAsData PInteger)
as a field type, but not PBuiltinList PInteger
.
Note: The constructor ordering in
PVehicle
matters! If you usedmakeIsDataIndexed
onVehicle
to assign an index to each constructor - the Plutarch type's constructors must follow the same indexing order.In this case,
PFourWheeler
is at the 0th index,PTwoWheeler
is at the 1st index, andPImmovableBox
is at the 3rd index. Thus, the correspondingmakeIsDataIndexed
usage should be:PlutusTx.makeIsDataIndexed ''FourWheeler [('FourWheeler,0),('TwoWheeler,1),('ImmovableBox,2)]
And you'd simply derive PIsDataRepr
using generics. However, you must also derive PIsData
and PlutusType
using PIsDataReprInstances
. For single constructor data types, you should also derive PDataFields
.
Combine all that, and you have:
{-# LANGUAGE UndecidableInstances #-}
import qualified GHC.Generics as GHC
import Generics.SOP
import Plutarch.Prelude
import Plutarch.DataRepr (PIsDataReprInstances (PIsDataReprInstances))
data PVehicle (s :: S)
= PFourWheeler (Term s (PDataRecord '["_0" ':= PInteger, "_1" ':= PInteger, "_2" ':= PInteger, "_3" ':= PInteger]))
| PTwoWheeler (Term s (PDataRecord '["_0" ':= PInteger, "_1" ':= PInteger]))
| PImmovableBox (Term s (PDataRecord '[]))
deriving stock (GHC.Generic)
deriving anyclass (Generic, PIsDataRepr)
deriving
(PlutusType, PIsData)
via PIsDataReprInstances PVehicle
Note: You cannot derive
PIsDataRepr
for types that are represented using Scott encoding. Your types must be well formed and should be usingPDataRecord
terms instead.
That's it! Now you can represent PVehicle
as a Data
value, as well as deconstruct and access its fields super ergonomically. Let's try it!
-- NOTE: REQUIRES GHC 9!
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE OverloadedRecordDot #-}
import Plutarch.Prelude
import qualified Plutarch.Monadic as P
test :: Term s (PVehicle :--> PInteger)
test = plam $ \veh' -> P.do
veh <- pmatch veh'
case veh of
PFourWheeler fwh' -> P.do
fwh <- pletFields @'["_0", "_1", "_2", "_3"] fwh'
fwh._0 + fwh._1 + fwh._2 + fwh._3
PTwoWheeler twh' -> P.do
twh <- pletFields @'["_0", "_1"] twh'
twh._0 + twh._1
PImmovableBox _ -> 0
Note: The above snippet uses GHC 9 features (
QualifiedDo
andOverloadedRecordDot
). Be sure to check out Do syntax withTermCont
and alternatives toOverloadedRecordDot
.
What about types with singular constructors? It's quite similar to the sum type case. Here's how it looks:
{-# LANGUAGE UndecidableInstances #-}
import qualified GHC.Generics as GHC
import Generics.SOP
import Plutarch.Prelude
import Plutarch.DataRepr (
PDataFields,
PIsDataReprInstances (PIsDataReprInstances),
)
newtype PFoo (s :: S) = PMkFoo (Term s (PDataRecord '["foo" ':= PByteString]))
deriving stock (GHC.Generic)
deriving anyclass (Generic, PIsDataRepr)
deriving
(PlutusType, PIsData, PDataFields)
via PIsDataReprInstances PFoo
Just an extra PDataFields
derivation compared to the sum type usage!