The isomorphism between makeIsDataIndexed, Haskell ADTs, and PIsDataRepr

When implementing PIsDataRepr for a Plutarch type, if the Plutarch type also has a Haskell synonym (e.g. ScriptContext is the Haskell synonym to PScriptContext) that uses makeIsDataIndexed - you must make sure the constructor ordering is correct.

In particular, with makeIsDataIndexed, you can assign indices to your Haskell ADT’s constructors. This determines how the ADT will be represented in Plutus Core. It’s important to ensure that the corresponding Plutarch type knows about these indices so it can decode the ADT correctly - in case you passed it into Plutarch code, through Haskell.

For example, consider Maybe. Plutus assigns these indices to its constructors:

makeIsDataIndexed ''Maybe [('Just, 0), ('Nothing, 1)]

0 to Just, 1 to Nothing. So the corresponding Plutarch type, PMaybeData is defined as:

data PMaybeData a (s :: S)
  = PDJust (Term s (PDataRecord '["_0" ':= a]))
  | PDNothing (Term s (PDataRecord '[]))

It’d be a very subtle mistake to instead define it as:

data PMaybeData a (s :: S)
  = PDNothing (Term s (PDataRecord '[]))
  | PDJust (Term s (PDataRecord '["_0" ':= a]))

The constructor ordering is wrong!

It’s not just constructor ordering that matters - field ordering does too! Though this is self explanatory. Notice how PTxInfo shares the exact same field ordering as its Haskell synonym - TxInfo.

newtype PTxInfo (s :: S)
  = PTxInfo
      ( Term
          s
          ( PDataRecord
              '[ "inputs" ':= PBuiltinList (PAsData PTxInInfo)
               , "outputs" ':= PBuiltinList (PAsData PTxOut)
               , "fee" ':= PValue
               , "mint" ':= PValue
               , "dcert" ':= PBuiltinList (PAsData PDCert)
               , "wdrl" ':= PBuiltinList (PAsData (PTuple PStakingCredential PInteger))
               , "validRange" ':= PPOSIXTimeRange
               , "signatories" ':= PBuiltinList (PAsData PPubKeyHash)
               , "datums" ':= PBuiltinList (PAsData (PTuple PDatumHash PDatum))
               , "id" ':= PTxId
               ]
          )
      )
data TxInfo = TxInfo
  { txInfoInputs      :: [TxInInfo]
  , txInfoOutputs     :: [TxOut]
  , txInfoFee         :: Value
  , txInfoMint        :: Value
  , txInfoDCert       :: [DCert]
  , txInfoWdrl        :: [(StakingCredential, Integer)]
  , txInfoValidRange  :: POSIXTimeRange
  , txInfoSignatories :: [PubKeyHash]
  , txInfoData        :: [(DatumHash, Datum)]
  , txInfoId          :: TxId
  }

The field names don’t matter though. They are merely labels that don’t exist at runtime.

What about newtypes?

Of course, this does not apply when you’re using newtype derivation (e.g derive newtype ...) to derive FromData or ToData for your PlutusTx types. In that case, the Data representation is simply the same as the inner type.

import qualified PlutusTx
import PlutusTx.Prelude

newtype CurrencySymbol = CurrencySymbol { unCurrencySymbol :: BuiltinByteString }
  deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData)

Here, for example, CurrencySymbol has the very same Data representation as BuiltinByteString. No extra information is added.

note that in plutarch what matters is not whether you declare a datatype as haskell data or as haskell newtype but in what way you derive the plutuscore representation