This document serves as a guide for understanding and working with BuiltinData
/Data
. It's primarily meant for Pluto users and Plutarch developers/contributors.
Note: If you spot any mistakes/have any related questions that this guide lacks the answer to, please don't hesitate to raise an issue. The goal is to have high quality documentation for Pluto and Plutarch users!
- What is Data/BuiltinData?
- Useful Links
Friendly Reminder: For general Plutarch usage, you won't need to use ANY of the methods here. There are safer interfaces provided to do that (see:
PIsDataRepr
,PAsData
).
This is what BuiltinData looks like-
data Data
= Constr Integer [Data]
| Map [(Data, Data)]
| List [Data]
| I Integer
| B ByteString
Aside: The direct Plutarch synonym to
BuiltinData
/Data
isPData
. However, you should preferPAsData
as it also preserves type information. The functions you need to manually work with these are exported fromPlutarch.Builtin
.
We discuss each of these constructors, and how to work with them, in the following sections.
Common Plutarch imports:
Plutarch
,Plutarch.Builtin
,qualified PlutusCore as PLC
Constr
is responsible for representing most Haskell ADTs. It's a sum of products representation. Constr 0 []
- designates the 0th constructor with no fields. Each field is represented as a Data
value.
For example, when you implement IsData
for your Haskell ADT using-
import PlutusTx
data Foo = Bar Integer | Baz ByteString
PlutusTx.makeIsDataIndexed
''Foo
[ ('Bar, 0),
('Baz, 1)
]
It essentially means that PlutusTx.toData (Bar 42)
translates to Constr 0 [PlutusTx.toData 42]
. Whereas PlutusTx.toData (Baz "A")
translates to Constr 1 [PlutusTx.toData "A"]
.
Aside: The integer literal, list literal, and (byte-)string literals you see in that Haskell code are the Plutus Tx builtin types.
Let's look at the IsData
implementation for the Maybe
type-
import PlutusTx
PlutusTx.makeIsDataIndexed ''Maybe [('Just,0),('Nothing,1)]
This means that PlutusTx.toData Nothing
translates to Constr 1 []
- and PlutusTx.toData (Just x)
translates to Constr 0 [PlutusTx.toData x]
.
IMPORTANT: newtype constructors (generally) don't persist when you do toData. Their inner value's toData result is yielded instead.
Now, when you receive a BuiltinData
/Data
inside your function - if you are sure what you have received is a Constr
- you can work with it accordingly.
The builtin function used to take apart a Constr
data value, is UnConstrData
.
-- test.pluto
UnConstrData (data sigma0.[1, 0x4d])
This will yield a pair, the first member of which, is an integer representing the constructor id. The second member is a list of fields associated with the constructor.
$ pluto run test.pluto
Constant () (Some (ValueOf pair (integer) (list (data)) (0,[I 1,B "M"])))
You can extract the constructor id using FstPair
, you must force it twice first-
-- test.pluto
let
x = UnConstrData (data sigma0.[1, 0x4d])
in ! ! FstPair x
$ pluto run test.pluto
Constant () (Some (ValueOf integer 0))
As you would expect, retrieving the fields is similar. All you need is SndPair
, which also needs two forces.
-- test.pluto
let
x = UnConstrData (data sigma0.[1, 0x4d])
in ! ! SndPair x
$ pluto run test.pluto
Constant () (Some (ValueOf list (data) [I 1,B "M"]))
It results in a builtin list of Data
elements. See Working with Builtin Lists.
How about we load it up in Haskell? Let's make a Pluto function that returns the constructor id of the given ADT!
-- test.pluto
\x -> ! ! FstPair (UnConstrData x)
Load it up and bind it to a variable!
plutoSc :: Script
> [PlutusTx.toData (Nothing :: Maybe Integer)] `evalWithArgs` plutoSc
Right (ExBudget {exBudgetCPU = ExCPU 597830, exBudgetMemory = ExMemory 1164},[],Constant () (Some (ValueOf integer 1)))
> [PlutusTx.toData (Just 1 :: Maybe Integer)] `evalWithArgs` plutoSc
Right (ExBudget {exBudgetCPU = ExCPU 597830, exBudgetMemory = ExMemory 1164},[],Constant () (Some (ValueOf integer 0)))
In Plutarch, pasConstr
is the synonym to UnConstrData
-
pasConstr :: Term s (PData :--> PBuiltinPair PInteger (PBuiltinList PData))
pasConstr = punsafeBuiltin PLC.UnConstrData
This will yield a pair, the first member of which, is the constructor id. You can extract the constructor id using pfstBuiltin
, which is a synonym to FstPair
-
pfstBuiltin :: Term s (PBuiltinPair a b :--> a)
pfstBuiltin = phoistAcyclic $ pforce . pforce . punsafeBuiltin $ PLC.FstPair
Aside: Recall that
FstPair
requires 2 forces.
Here's a Plutarch function that takes in a BuiltinData/Data
, assumes it's a Constr
, and returns its constructor id-
import Plutarch.Builtin
import Plutarch.Integer
import Plutarch.Prelude
constructorIdOf :: Term s (PData :--> PInteger)
constructorIdOf = plam $ \x -> pfstBuiltin #$ pasConstr # x
As you would expect, retrieving the fields is also trivial once you're armed with the above knowledge-
fieldsOf :: Term s (PData :--> PBuiltinList PData)
fieldsOf = plam $ \x -> psndBuiltin #$ pasConstr # x
Let's test those functions!
> constructorIdOf `evalWithArgsT` [PlutusTx.toData (Nothing :: Maybe ())]
Right (Program () (Version () 1 0 0) (Constant () (Some (ValueOf integer 1))))
Aside: You can find the definition of
evalWithArgsT
above - Compiling and Running.
That's a roundabout way of saying "1". But you get the idea. In this case, the constructor id of Nothing
, is indeed 1.
> fieldsOf `evalWithArgsT` [PlutusTx.toData (Nothing :: Maybe ())]
Right (Program () (Version () 1 0 0) (Constant () (Some (ValueOf list (data) []))))
And that's another roundabout way of saying []
! That is, no fields associated with the constructor.
Extracting fields from values they would actually be present in, is also straight-forward now-
> fieldsOf `evalWithArgsT` [PlutusTx.toData (Just 1 :: Maybe Integer)]
Right (Program () (Version () 1 0 0) (Constant () (Some (ValueOf list (data) [I 1]))))
There we go! [I 1]
- We'll discuss the I
constructor in its own section below. But it's just a BuiltinData
/Data
value. It's not Constr
though! It's I
.
You can create Constr
data values using sigma
literals.
You can also use the ConstrData
builtin function to create Constr
data values. It takes 2 arguments - the constructor id, and its fields as a list of Data
elements.
ConstrData 0 (MkNilData ())
is the same as data sigma0.[]
.
Aside: What's that
MkNilData ()
? That's how you create anil
list ofData
elements!MkNilData
takes in a unit and returns anil
list ofData
. You can add moreData
elements to it usingMkCons
. See Working with Builtin Lists.
You can manually build Constr
data values using the ConstrData
builtin like above-
pconstrData :: Term s (PInteger :--> PBuiltinList PData :--> PData)
pconstrData = punsafeBuiltin PLC.ConstrData
Or, you can avoid a builtin function call and build the PData
directly using pconstant
-
import PlutusTx (Data (Constr))
> pconstant @PData (Constr 0 [])
The Map
constructor is for """Haskell maps""". In the Plutus world, maps are apparently just assoc lists. You've seen assoc lists already; they're just a list of pairs. These pairs consist of two Data
values.
The common example of this is Value
. But anytime you see Plutus Assoc Maps - you can be sure that it's actually going to end up as a Map
data.
You can unwrap the Map
data value to obtain the inner builtin list of builtin pairs with the UnMapData
builtin function. You can then work with the resulting builtin lists. It contains pairs of Data
. See Working with Builtin Lists.
-- test.pluto
UnMapData (data { 1 = 0xfe })
$ pluto run test.pluto
Constant () (Some (ValueOf list (pair (data) (data)) [(I 1,B "\254")]))
In Plutarch, pasMap
is the synonym to UnMapData
-
pasMap :: Term s (PData :--> PBuiltinList (PBuiltinPair PData PData))
pasMap = punsafeBuiltin PLC.UnMapData
You can build Map
data values using map literals.
You can also use the MapData
builtin function to create Map
data values. It takes in a builtin list of builtin pairs of Data
.
MapData (MkNilPairData ())
This is the same as data {}
.
Aside: What's that
MkNilPairData ()
? Similar toMkNilData
. This one is for creating anil
list ofData
pairs. You can add moreData
pairs to it usingMkCons
. See Working with Builtin Lists.
Much like above, and in the case of Constr
, you can use the MapData
builtin. Or you can use pconstant
.
import PlutusTx (Data (Map, I, B))
> pconstant @PData (Map [(I 1, B "x")])
The List constructor is a wrapper around a builtin list of Data
. Notice that it is specifically a monomorphic list. Its elements are of type Data
. PlutusTx.toData [1, 2, 3]
translates to List [I 1, I 2, I 3]
. Those elements are I
data values.
One interesting thing to note here is that when you convert a Haskell list to a Data
value, and it ends up as a List
data value, all the elements within the builtin list will be the same "species" of Data
. What does that mean? Well, Haskell lists are homogenous, e.g- [Int]
, turning [Int]
into a Data
consists of two steps-
- Map
PlutusTx.toData
over all elements of the list. - Wrap the list into a
List
data value.
PlutusTx.toData
on an Int
value will just yield an I
data value. Due to the fact that lists are homogenous, all of those Int
elements will just be I
data value, so in the end - the Data
representation of [1, 2, 3]
looks like - List [I 1, I 2, I 3]
. The data values have the same "species"! It is totally and completely valid to create a List [I 1, B "f", Constr 0 []]
in Plutus Core - but you're not going to get that botched version from a Haskell list (and therefore, most of your data types)!
You can unwrap the List
data value to obtain the inner builtin list using the UnListData
builtin function. Then, you can use the resultant builtin list with the builtin functions that work on lists. See Working with Builtin Lists.
-- test.pluto
UnListData (data [1, 0xab, { 42 = [1, 2] }])
$ pluto run test.pluto
Constant () (Some (ValueOf list (data) [I 1,B "\171",Map [(I 42,List [I 1,I 2])]]))
In Plutarch, pasList
is the synonym to UnListData
.
pasList :: Term s (PData :--> PBuiltinList PData)
pasList = punsafeBuiltin PLC.UnListData
You can build List
data values using list literals.
You can also use the ListData
builtin function to create List
data values. It takes in a builtin list of Data
elements.
ListData (MkNilData ())
This is the same as data []
.
Much like before, you can use the ListData
builtin. Or you can use pconstant
.
import PlutusTx (Data (List, I))
> pconstant @PData (List [I 1, I 2])
The I
constructor wraps a builtin integer to create a Data
value. When you do PlutusTx.toData 123
- you obtain an I
data.
You can unwrap an I
data value to obtain the inner builtin integer using the UnIData
builtin function.
-- test.pluto
UnIData (data 42)
$ pluto run test.pluto
Constant () (Some (ValueOf integer 1))
In Plutarch, pasInt
is the synonym to UnIData
.
pasInt :: Term s (PData :--> PInteger)
pasInt = punsafeBuiltin PLC.UnIData
You can build I
data values using integer literals preceded by data
.
You can also use the IData
builtin function to create I
data values. It takes in a builtin integer.
IData 42
This is the same as data 42
.
Much like before, you can use the IData
builtin. Or you can use pconstant
.
import PlutusTx (Data (I))
> pconstant @PData (I 42)
Similar to I
, the B
constructor wraps a builtin bytestring to create a Data
value.
You can unwrap a B
data value to obtain the inner builtin bytestring using the UnBData
builtin function.
-- test.pluto
UnBData (data 0x4f)
$ pluto run test.pluto
Constant () (Some (ValueOf bytestring "O"))
In Plutarch, pasByteStr
is the synonym to UnBData
.
pasByteStr :: Term s (PData :--> PByteString)
pasByteStr = punsafeBuiltin PLC.UnBData
You can build B
data values using integer literals preceded by data
.
You can also use the BData
builtin function to create B
data values. It takes in a builtin integer.
BData 0x42
This is the same as data 0x42
.
Much like before, you can use the BData
builtin. Or you can use pconstant
.
import PlutusTx (Data (B))
> pconstant @PData (B 42)
What happens when you don't know what kind of Data
you have? In many cases, you know the exact structure of the Data
you receive (e.g ScriptContext
structure is known). But if you have no way to know whether the Data
is a Constr
, or Map
, or List
, and so on - you can use the ChooseData
builtin function. It takes one force!
-- test.pluto
! ChooseData (data 42) 0 1 2 3 4
$ pluto run test.pluto
Constant () (Some (ValueOf integer 3))
Each argument corresponds to a branch. Details are discussed at Plutus Core builtin functions reference.
In this case, the Data
value had an I
constructor (data 42
creates an I
data). That corresponds to the 5th argument, which was 3
.
Aside: As is the case for all other function calls, all those arguments will be evaluated strictly. You should use delay to avoid this.
Here's how you could implement a chooseData
synonym-
pchooseData :: Term s (PBuiltinData -> a -> a -> a -> a -> a -> a)
pchooseData = phoistAcyclic $ pforce $ punsafeBuiltin PLC.ChooseData
It works all the same as above!