plu-ts

Built with ❤️ by Harmonic Laboratories

This documentation is for plu-ts v0.1.1^, if you are using a previous version check for changes in the changelog.

Introduction

plu-ts is a library designed for building Cardano dApps in an efficient and developer friendly way.

It is composed of two main parts:

  • plu-ts/onchain: an eDSL (embedded Doamin Specific Language) that leverages Typescript as the host language; designed to generate efficient Smart Contracts.
  • plu-ts/offchain: a set of classes and functions that allow reuse of onchain types.

Design principles

plu-ts was designed with the following goals in mind, in order of importance:

  • Smart Contract efficiency
  • reduced script size
  • developer experience
  • readability

Roadmap

  • v0.1.* :
    • key syntax to build plu-ts expressions
    • compilation of smart contracts to valid UPLC
    • standard API data structures (PScriptContext, etc... ) for PlutusV1 and PlutusV2 contracts
    • standard library
    • Terms with utility methods to simplify the developer experience ( TermInt, TermBool, etc... )
  • v0.2.* :
    • functions for standard API data structures interaction
    • plu-ts/offchain functions for basic transactions
  • v0.3.* :
    • TermCont implementation to mitigate the callback hell issue
    • plu-ts/offchain complete offchain API

plu-ts language index

Core ideas

plu-ts is a strongly typed eDSL for generating Cardano Smart Contracts.

In order to allow the creation of efficient smart contracts, plu-ts is functional, allowing more control over the compiled result.

As a consequence of the functional nature of the language, everything in plu-ts is an expression.

eDSL concepts

eDSL stands for embedded Domain Specific Language.

What it means can be explained by analyzing the definition:

  • Language explains that is a programming language we are talking about.
  • Domain Specific explains that the language is meant for a specific set of tasks. The "Domain", or specific purpose of plu-ts is the creation of Cardano smart contracts.
  • embedded means that it is a language inside another language. While plu-ts is a language on its own, it is built inside of the Typescript language (which is called the host language).

Key Idea:

When writing plu-ts code it is important to distinguish what parts of the code are native to Typescript and what parts are plu-ts.

Since Typescript is the host language, Typescript will be our starting point for learning about plu-ts.

plu-ts values

It is always possible to transform a Typescript value into a plu-ts value. In this section we'll see how.

Types

Typescript Types

  • Term is a Typescript type defined in plu-ts.
  • Every value in plu-ts is a Term. In Typescript, we say each value extends Term (in the same way that "Dog" extends "Mammal").
  • A Term also keeps track of the type of the value it holds.

The possible types a Term can keep track of are defined in PTypes, and listed here:

  • PUnit a unique value that has no real meaning; you can see it as plu-ts version of undefined or null in Typescript
  • PInt a signed integer that can be as big as you want
  • PBool a boolean value
  • PByteString equivalent of a Buffer or a Uint8Array
  • PString equivalent of the Typescript string
  • PData equivalent of the object type in Typescript (it is the low level representation of PStructs that we'll cover in a moment, so you usually won't use PData)
  • PList<PType> equivalent of an Array in Typescript; note that all the elements in the list must be of the same PType
  • PPair<PType1, PType2> equivalent of a Typescript tuple ([ type1 , type2 ])
  • PDelayed<PType> a delayed computation that returns a value of type PType; the computation can be run by passing the delayed value to the pforce function
  • PLam<PInput, POutput> a function that takes one single argument of type PInput and returns something of type POutput
  • PFn<[ PInput_0 , ...PType[] ],POutput> a function that takes multiple arguments (at least one) and returns something of type POutput
  • PAlias<PType> just an alias of the provided type; it behaves exactly like the Types of its argument, so PAlias<PInt> is equivalent to a PInt. This is useful for keeping track of a different meaning the type might have.
  • PStruct<{...}> an abstraction over PData, useful to construct more complex data structures.

plu-ts Types

plu-ts would not be a strongly typed language if limited to Typescript types, because the types of Typescript are only useful during compilation to javascript; and then everything is untyped!

Important Note:

Typescript can be compiled to Javascript. When this happens, the resulting Javascript is untyped!

Therefore:

For this reason plu-ts implements its own type through some constants and functions can be imported.

In the same order of above, the plu-ts equivalents are:

  • PUnit -> unit
  • PInt -> int
  • PBool -> bool
  • PByteString -> bs
  • PString -> str
  • PData -> data
  • PList -> list( type )
  • PPair -> pair( type1, type2 )
  • PDelayed -> delayed( type )
  • PLam -> lam( from, to )
  • PFn -> fn([ ...inputs ], output )
  • aliases types and structs types will be retreived by the type static property of the classes (explained in the dedicated section for aliases and structs)

polymonrphic types (generics)

simple values

For most of the types described there is a function to transform the Typescript version to the plu-ts equivalent.

Here we cover the simple ones, leaving functions and structs to be covered later.

plu-ts typefunction namets to plu-ts function signature
unitpmakeUnitpmakeUnit(): Term<PUnit>
intpIntpInt(x: number \ bigint): Term<PInt>
boolpBoolpBool(x: boolean): Term<PBool>
bspByteStringpByteString(x: string \ ByteString \ Buffer): Term<PByteString>
strpStrpStr(x: string): Term<PStr>
datapDatapData(x: Data): Term<PData>
listpList* explained below
pair** not supported at ts level** explained below
delayed** not supported at ts level** explained below

* pList

Since PList is a generic type the pList function has a slightly more complex function signature:

function pList<ElemsT extends TermType, PElemsT extends ToPType<ElemsT = ToPType<ElemsT>( elemsT: ElemsT )
    : ( elems: Term<PElemsT>[] ) => Term<PList<PElemsT>>

In the signature above, TermType is the Typescript types of plu-ts types (which are typescript values after all) and ToPType is a utility type used internally and you should not worry about it.

From the signature we can already understand that given a plu-ts type, pList returns a function ad-hoc for terms of that type; so if we want a function to get list of integers we just do:

const pListInt: ( elems: Term<PInt>[] ) => Term<PList<PInt>> = pList( int );

And with that we now have a function that transforms an array of terms into a list.

const intList = pListInt( [1,2,3,4].map( pInt ) );

You might notice that in contrast to the other functions introduced, pListInt that we created works with terms instead of vanilla ts values; this is because pListInt acts as a macro (advanced)

** not supported

pair and delayed do not have a direct way to build a value from ts for two different reasons:

pairs can only be built using data dynamically.

delayed doesn't really have a Typescript value, so it only makes sense in the plu-ts world.

plu-ts functions

Functions can be transformed from the Typescript world to the plu-ts one just like any other value.

This can be done with two functions:

  • plam
  • pfn

plam

Just like the lam type, plam only works for functions with one input; don't worry, pfn is more powerful, but plam will help us understand the basics.

The plam signature is:

function plam<A extends TermType, B extends TermType >( inputType: A, outputType: B )
    : ( termFunc : ( input: Term<ToPType<A>> ) => Term<ToPType<B>> ) => Term<PLam<ToPType<A>,ToPType<B>>>

If this seems familiar it's because it works on the same principle of pList we saw in the explanation of simple values.

plam first requires us to specify the plu-ts types we are working with and it gives back a function ad-hoc for those types.

const makeLambdaFromIntToBool: ( tellMeHow: ( int: Term<PInt> ) => Term<PBool> ): Term<PLam<PInt, PBool>> = plam( int, bool )

The function we get back expects a typescript function as input that describe how to "transform" the input to the output.

Since the tellMeHow function should return a Term; we need some way to "build" a new term.

In plu-ts you never need to write anything like new Term(...); rather you use plu-ts functions to build new plu-ts terms.

Wait what? Aren't plu-ts functions also Terms? How do I build new Terms if I need other Terms to build new Terms?

Fortunately for us there are some builtin functions that form the fundamentals of the language. We can use these to describe the body of our lambda.

const pintIsZero = makeLambdaFromIntToBool(
    someInt => peqInt.$( someInt ).$( pInt( 0 ) )
);

NOTE: is convention to name plu-ts functions starting with a lower case "p"; indicating that we are in the plu-ts world and not the typescript one

Here we used the peqInt builtin function; the $ method is a short form for the papp function and is how we pass arguments to a plu-ts function (we'll cover function application in the very next section).

What matters for now is that we succesfully transformed an int into a bool using only plu-ts; and we now have a new function that we can re-use when needed.

pintIsZero.$( pInt(42) ) // this is a Term<PBool> equivalent to `pBool( false )`

pfn

Now that we know how the plam machinery works let's look at the more useful pfn.

The signature (a bit simplified; this is not Typescript) is

function pfn<InputsTypes extends [ TermType, ...TermType[] ], OutputType extends TermType>( inputsTypes: InputsTypes, outputType: OutputType )
    : ( termFunction: ( ...inptus: PInputs ) => POutput ) => 
        Term<PFn<PInputs, POutput>>

and with the exception of an array of types as input rather than a single type we see it is doing the exact same thing as plam but with more inputs.

So if we want a function that builds a plu-ts level function for us of type int -> int -> list( int ) we just write

const makeListFromTwoInts = pfn( [ int, int ], list( int ) );

and just like the plam case, we use the function we just got to build a plu-ts one.

const pTwoIntegersList = makeListFromTwoInts(
    ( int1, int2 ) => pList([ int1, int2 ])
);

papp

Lambdas and functions in general in plu-ts are often just constants seen from the typescript world, however we usually know that what we have is more than just a constant and that it can take arguments.

For this particular reason we have the papp function (which stands for "plu-ts applicaiton")and all it does is tell Typescript that we want to apply one term to another, or in other words pass an argument to a function.

The type signature of papp is something like:

function papp<Input extends PType, Output extends PType>( a: Term<PLam<Input,Output>>, b: Term<Input> ): Term<Output>

as we'll see in the next section, functions can be partially applied so, to preserve this behaviour, papp only takes two arguments:

  1. the function we want to pass the argument to
  2. the argument

then it checks the types are matching, evaluates the argument and applies the result of the evaluation and finally returns the result.

the "$" method

However, having to use an external function in order to pass arguments tends to make the code hard to read.

Here is an example of code if all we had was papp:

papp(
    papp(
        pTwoIntegersList,
        pInt(42)
    ),
    pInt(69)
);

For this reason, often you'll encounter Terms that have a type that looks like this:

type LambdaWithApply =
    Term<PLam<SomeInput, SomeOutput>> // this is our usual type
    & { // extended with some functionalities
        $: ( input: Term<SomeInput> ) => Term<SomeOutput>
    }

where the $ method definition is often nothing more than:

myTerm["$"] = ( someInput: Term<SomeInput> ) => papp( myTerm, someInput );

At first glance, this seems like it does nothing fancy, but it allows us to transform the previous code in something more readable like:

pTwoIntegersList.$( pInt(42) ).$( pInt(69) )

partial function application

When a plu-ts function takes more than one argument, like the pTwoIntegersList we built a moment ago, it is possible to get new functions from the first by passing only some of the parameters.

Since the type of pTwoIntegersList was something like int -> int -> list( int ), pTwoIntegersList expects 2 arguments; however if we pass only 1 the result will be a valid Term of type int -> list( int ); which is another plu-ts function!

 // this is a Term from PInt to PList<PInt>!
const pListWith42First = pTwoIntegersList.$( pInt(42) );

In particular, the new function we get behaves just like the first but with the arguments already passed that are fixed and can't be changed.

// equivalent to pTwoIntegersList.$( pInt(42) ).$( pInt( 69 ) )
const niceList = pListWith42First.$( pInt( 69 ) );

This not only reduces the number of new functions you need to create but is also more efficient than wrapping the first function inside of a new lambda.

// THIS IS BAD
const pInefficientListWith42First = plam( int, list( int ) )
    ( int2 =>
        pTwoIntegersList.$( pInt(42) ).$( int2 ) // BAD
    );

Even if the compiler is smart enough to optimize some trivial cases, it is still best practice to avoid doing this.

builtins

Fortunately UPLC does have some basic functions that allow us to build more complex ones when needed.

We already encountered peqInt while introducing plam exactly because we needed a way to interact with our terms.

The Plutonomicon open source repository has some great docs explaining the behavior of each builtin available.

aliases

In some cases it might be useful to define aliases for already existing types.

In the current implementation, aliases do not really have any specific advantage other than making your code more expressive. Currently, aliases can be used everywhere the aliased type is accepted and vice-versa.

generally speaking you may want to use aliases to define a subset of values that are meant to have a specific meaning

example: you might need a type that describes the name of a person; every name is a string; but not every string is a name;

to make clear the distinction you define an alias of the string type to be the Name type

We define new aliases using the palias ts function:

const Age = palias( int );

Now we have a new type to specfically represent ages.

To get a term of the aliased type you can use the from static method of the class you got from calling palias:

const someAge: Term<typeof Age> = Age.from( pInt(18) ); 

NOTE: in a future version aliases will be able to add constraints over the type the are alias of as an example whe might want to force every Age to be non-negative; since a negative age doesn't really make sense

when an alias will be constrained plu-ts will check for the constraints to be met each time a term with an alias is constructed and will fail the computation if the constraints are not met

What's the plu-ts type of my alias?

As explained in the types section, aliases and structs have different plu-ts level types. To access them we need to use the type static method defined in the Alias class:

const agePlutsType = Age.type;

So if we want to define a function that accepts an Age as input we would write:

const pisAdult = plam( Age.type, bool )
( age => 
    pgreaterEqInt.$( age as Term<PInt> ).$( pInt(18) )
);

structs

When writing programs we often need to access data that is more articulate than simple integers or booleans; for this reason plu-ts allows ytou to define custom data-types.

pstruct

To define your own type all you need is the pstruct typescript function.

pstruct takes as an argument an object that describes the structure of the new data-type and returns a Typescript class that represents our new type.

the type of a struct definition (which is teh argument of pstruct) is something like:

type StructDefiniton = {
    [constructor: string]: {
        [field: string]: TermType
    }
};

From this type we can already see that a struct can have multiple constructors (at least one) and each constructor can have 0 or more named fields.

This characteristic of having multiple constructors will allow for the creation of custom control flows through the use of pmatch described in its own section.

For now let's focus on defining some new structs and say we wanted to define a datatype that describes a Dog.

We could do so by writing:

// structs with single constructors acts in a similar way of plain typescript object
const Dog = pstruct({
    // single constructor
    Dog: {
        name: str,
        age: Age.type
    }
});

but our dog needs some toys to play with when we are out. So we define a structure that describes some toys:

const Toy = pstruct({
    Stick: {},
    Ball: {
        size: int,
        isSoft: bool
    },
    Mailman: {
        name: str,
        age: Age.type
    }
})

So now we can add a new field to better describe our dog:

const Dog = pstruct({
    Dog: {
        name: str,
        age: Age.type,
        favouriteToy: Toy.type
    }
});

IMPORTANT

When defining a struct the order of the constructors and the order of the fields matters

infact at UPLC level there are no names

this does have two important implications

  1. structs with similar definition will be interchangeable; meaning that something like

    const Kid = pstruct({
       Kid: {
            name: str,
            age: Age.type,
            toy: Toy.type
        }
    });
    

    can be used in place of a Dog without anything breaking

  2. changing the order of constructors or fields gives back a totally different struct

struct values

To build a plu-ts value that represents a struct you just use one of the constructors you defined.

So if you where to create an instance of a Dog you'd just write:

const myDog = Dog.Dog({

    name: pStr("Pluto"),
    age:  Age.from( pInt(3) ),

    favouriteToy: Toy.Mailman({
        name: pStr("Bob"),
        age:  Age.from( pInt(42) )
    })

})

struct plu-ts type

Like aliases, structs also do have custom plu-ts types; which can be accessed using the type static property

const plutsTypeOfToy = Toy.type;

generic structs

Sometimes it might be necessary to define custom types that are able to work with any other type; often acting as containers.

A great example are lists; which can work with elements of any type; and for this reason we have list( int ), list( bs ), etc...

But lists are built into the language; how do we define our own containers?

pgenericStruct is meant exactly for this.

As we know structs can have multiple constructors and the same is true for generic ones; so let's try to define a type that can hold either one or two instances of the same type:

const POneOrTwo = pgenericStruct( ty => {
    return {
        One: { value: ty },
        Two: { fst: ty, snd: ty }
    };
});

pgenericStruct returns a funciton (and not a class like pstruct does) that takes as input as many TermTypes as in the definition (the arguments of thefunction passed to `pgenericStruct')

and only then returns a class; which represents the actual struct type.

const OneOrTwoInts = POneOrTwo( int ),

const OneOrTwoBS = POneOrTwo( bs );

const OneOrTwoOneOrTwoInts = POneOrTwo( POneOrTwo( int ).type );

But can't I just use a function that returns a new pstruct based on different type arguments?

Well yes, but actually no

You could but each time you'd get a different struct with the same definition;

as an example you could do something like

const makeOneOrTwo = ( ty ) => pstruct({
    One: { value: ty },
    Two: { fst: ty, snd: ty }
});

but now you'd get a BRAND NEW struct each time you call makeOneOrTwo; meaning that you might not be able to assign one to the other; even if the two are basically the same.

To prevent this pgenericStruct caches results for the same inputs; so that the same class is returned:

console.log(
    makeOneOrTwo( int ) === makeOneOrTwo( int ) // false
);

console.log(
    POneOrTwo( int ) === POneOrTwo( int ) // true
);

make Typescript happy

The fact that pgenericStruct works with type arguments that need to be used in the struct definion makes it really hard for typescript to infer the correct types of a generic struct.

for this reason you may want to explicitly tell to typescript what is your type once instatiated; and this requires some bolireplate:

// define a type that makes clear where
// the different type arguments are supposed
// to be in the struct definition
export type POneOrTwoT<TyArg extends ConstantableTermType> = PStruct<{
    One: { value: TyArg },
    Two: { fst: TyArg, snd: TyArg }
}>

// this is the actual definiton
const _POneOrTwo = pgenericStruct( ty => {
    return {
        One: { value: ty },
        Two: { fst: ty, snd: ty }
    };
});

// this is a wrapper that is typed correctly
function POneOrTwo<Ty extends ConstantableTermType>( tyArg: Ty ): POneOrTwoT<Ty>
{
    return _POneOrTwo( tyArg ) as any;
}

// export the wrapper with the type that is defined on the actual definition.
export default Object.defineProperty(
    POneOrTwo,
    "type",
    {
        value: _POneOrTwo.type,
        writable: false,
        enumerable: true,
        configurable: false
    }
)

The comments should help understand why this is needed; but you can just copy the snippet above and adapt it to you generic struct.

terms with methods

Like in the case of papp that is meant to work with a plu-ts function as the first argument, there are functions that are meant to work with specific types.

The functions can of course be used as normal but sometimes some arguments can be made implicit.

As an example, the built-in padd is meant to work with integers, so it would be great if instead of writing:

padd.$( int1 ).$( int2 )

we could make the first argument implicit and just do:

int1.add( int2 )

Turns out plu-ts implements some special terms that extend the normal Term functionalities, adding some methods to them. For most of the types there is a special Term type with extended functionalities:

normal termterm with methods
Term<PUnit>
Term<PInt>TermInt
Term<PBool>TermBool
Term<PByteString>TermBS
Term<PStr>TermStr
Term<PData>
Term<PList<PElemsType>>TermList<PElemsType>
Term<PPair<Fst,Snd>>
Term<PDelayed<PType>>
Term<PLam<In,Out>>
Term<PFn<Ins,Out>>TermFn<Ins,Out>
Term<Alias<PType>>depends by PType
Term<PStruct<StructDef>>TermStruct<StructDef>

These are callde "utility terms" and are covered more in depth in the standard library section; but is good having in mind that these exsists as they makes our life much easier while writing a program.

I see two properties that look similar, which one should I use?

Every utility term exposes two variants for each property it has; one is a plain function and the other (the one that ends with "...Term") that is the plu-ts version of it.

Let's take a look at the TermInt definition:

type TermInt = Term<PInt> & {

    readonly addTerm:       TermFn<[PInt], PInt>
    readonly add:           ( other: Term<PInt> ) => TermInt

    readonly subTerm:       TermFn<[PInt], PInt>
    readonly sub:           ( other: Term<PInt> ) => TermInt

    readonly multTerm:      TermFn<[PInt], PInt>
    readonly mult:          ( other: Term<PInt> ) => TermInt

    // 
    // ... lots of other methods
    // 
}

Generally speaking you want to use the ts function version for two reasons:

  1. is more readable
  2. might produce slightly less code (hence is more efficient)

However, the fact that is defined as a function makes it unable to be passed as argument to plu-ts higher oreder functions (or a normal ts functions that expects Term<PLam> as argument).

In that case you want to use the "...Term" alternative which is optimized exactly for that.

control flow

Every programming language that wants to be turing complete has to include some sort of way to:

  • execute code conditionally
  • repeat specific parts of the code

To satisfy those requirements, plu-ts implements

if then else

As a solution to condtitional code execution plu-ts exposes an if then else construct.

However, since everything in plu-ts is an expression, the if then else construct does not allow stuff that in typescript would have been written as

if( my_condition )
{
    doSomething();
}

because we don't really know what to do if the condtion is false.

So the if then else we have in plu-ts is more similar to the typescript ? : ternary operator, so at the end of the day, if then else is just a function.

Let's look at a simple if then else construct:

pif( int ).$( pBool( true ) )
.then( pInt(42) )
.else( pInt(69) )

This plu-ts expression checks the condition (pBool(true)) and if it is a Term<Bool> equivalent to true it returns pInt(42) otherwhise it returns pInt(69).

Why pif is a typescript function and not a constant like other plu-ts funcitons?

Since the type of if then else is something like bool -> a -> a -> a, we need to specify the type of a prior to the actual expression.

This ensures type safety so that Typescript can warn you if one of the results is not of the type you expect it to be.

What happens if one of the two branches raises an error?

plu-ts is a strict language as we saw while having a look at papp; that means that arguments are evaluated prior being passed to a function.

what happens if one of the argument returns an error?

The answer is what you expect to happen. Or, to be more precise, if the error rose in the branch selected by the boolean, the computation results in an error; if not it returns the result.

This is because even if by default functions are strict, pif is lazy; meaning that it evaluates only the argument it needs and not the others.

This is done using pforce and pdelay so the compiled funcion is a bit larger than the one you'd expect.

if you don't need lazyness you can use the pstrictIf function that emits slightly less code but evaluates both the arguments.

so something like

pstrictIf( int ).$( pBool( true ) )
.$( pInt(42) )
.$( pInt(69) )

is just fine but something like

// this results in an error, even if the conditional is true
pstrictIf( int ).$( pBool( true ) )
.then( pInt(42) )
.else( perror( int ) ) // KABOOM

generally speaking you should always prefer the plain pif

pmatch

When we had our first look at pstruct, we hinted at the possibility of custom control flows.

These are possible thanks to the pmatch construct.

To understand why this is extremely useful, let's take our Toy struct we defined looking at pstruct.

const Toy = pstruct({
    Stick: {},
    Ball: {
        size: int,
        isSoft: bool
    },
    Mailman: {
        name: str,
        age: Age.type
    }
})

And let's say we want to have a function that extracts the name of the mailman our dog plays with when we're out. It would look something like this:

const getMailmanName = plam( Toy.type, str )
( toy =>
    pmatch( toy )
    .onMailman( rawFields =>
        rawFields.extract("name").in( mailman =>
            mailman.name
    ))
    .onStick( _ => pStr("") )
    .onBall(  _ => pStr("") )
)

What pmatch basically does is take a struct and returns an object with all the names of possible constructors that struct has based on its definition. Based on the actual constructor used to build the struct passed, only that branch is executed.

A pmatch branch gets as input the raw fields of the constructor, under the form of a Term of type list( data ).

Since extracting the fields might turn out to be an expensive operation to do, the rawFields object provides a utility function called extract which takes the names of the fields you actually need and ignores the rest, finally making the extracted fields available in an object passed to the final function.

This way the defined function returns the name of the mailman if the Toy was actually constructed using the Mailman constructor; otherwise it returns an empty string.

the underscore (_) wildcard

pmatch will force you to cover the cases for all constructors; but many times we only want to do something if the struct was built using one specific constructor without regard for any other constructors.

In fact we found ourselves in a very similar case in the example above: we want to do something only in the Mailman case but not in the others.

For situations like these there is the underscore (_) wildcard, that allows us to rewrite our function as follows:

const getMailmanName = plam( Toy.type, str )
( toy =>
    pmatch( toy )
    .onMailman( rawFields =>
        rawFields.extract("name").in( mailman =>
            mailman.name
    ))
    ._( _ => pStr("") )
)

This not only makes the code more readable but in the vast majority of the cases it also makes it more efficient!

inline extracts

Now that we have a way to extract the name of the mailman from a Toy, we need to pass the actual toy to the fuction we just defined.

Using the pmatch function, our code would look like this:

// remember the definition of `Dog`
const Dog = pstruct({
    Dog: {
        name: str,
        age: Age.type,
        favouriteToy: Toy.type
    }
});

const getMailmanNameFromDog = plam( Dog.type, str )
( dog =>
    pmatch( dog )
    .onDog( rawFields =>
        rawFields.extract("favouriteToy").in( fields =>
            getMailmanName.$( fields.favouriteToy )
    ))
)

This works just fine but is a lot of code just to get a field of a constructor we know is unique...

Fortunately for us, if the value is a utility term for the PStruct type, what we have is actually something of type TermStruct<{...}>.

This utility term directly exposes the extract method if it notices that the struct can only be built by a single constructor.

Generally speaking, plam will always try to pass a utility term if it can recognize the type; so what we have there is actually a TermStruct<{...}>!

This allows us to rewrite the function as

const getMailmanNameFromDog = plam( Dog.type, str )
( dog =>
    dog.extract("favouriteToy").in( fields =>
        getMailmanName.$( fields.favouriteToy )
    )
)

which is a lot cleaner!

recursion

The other thing we are missing to have a proper language is some way to repeat the execution of some code.

The functional paradigm doesn't really like things like the loops we have in Typescript but that is not a big deal, because we can use recursive functions instead.

Wait a second!

Don't we need to reference the same function we are defining in order to make it recursive?

How do we do that if we need what we are defining while defining it?

Turns out someone else already came up with a solution for that so that we don't have to.

That solution is the Y combinator (actually we'll use the Z combinator but whatever).

We'll not go in the details on how it works, but if you are a curious one here's a great article that explains the Y combinator in javascript terms

All you need to know is that it allows functions to have themselves as parameters, and this solves everything!

In plu-ts there is a special typescript function that makes plu-ts functions recursive, and it's named, you guessed it, precursive.

All precursive requires to make a plu-ts function recursive is that we pass the function as the first parameter, and then we can do whatever we want with it.

So let's try to define a plu-ts function that caluclates the factorial of a positive number:

const pfactorial = precursive(
    pfn([
        // remember that the first argument is the function itself?
        // for this reason as first type we specify
        // what will be the final type of the function
        // because what we have here IS the function
        lam( int, int ),
        int
    ],  int)
    (( self, n ) =>
        pif( int ).$(
            // here `n` is of type `TermInt`;
            // which is the utility term for integers
            // the `ltEq` property stands for the `<=` ts operator
            n.ltEq( pInt(1) )
        )
        .then( pInt(1) )
        .else(
            // n * pfactorial.$( n - 1 )
            n.mult(
                papp(
                    self,
                    n.sub( pInt(1) )
                )
            )
        )
    )
)

Now we can use pfactorial just like a normal function; this is because precursive takes care of passing the first argument, so that the actual type of pfactorial is just lam( int, int )

The next step is to learn how to evaluate expressions so that we can be sure that pfactorial is working as we expect.

evaluate a plu-ts expression

plu-ts implements its own version of the CEK machine for the UPLC language. This allows any plu-ts term to be compiled to an UPLC Term and evaluated.

The function that does all of this is called evalScript, and that's literally all you need to evaluate a term.

evalScript will return an UPLCTerm because that's what it works with.

A UPLCTerm can be a lot of things, but if everything goes right you'll only encounter UPLCConst instances if you expect a concrete value, or some Lambda if you instead expect some functions. If instead the plu-ts term you passed as argument fails the computation you will get back an instance of ErrorUPLC.

To test it we'll use the pfactorial we defined in the recursion section

console.log(
    evalScript(
        pfactorial.$( 0 )
    )
); // UPLCConst { _type: [0], _value: 1n }

console.log(
    evalScript(
        pfactorial.$( 3 )
    )
); // UPLCConst { _type: [0], _value: 6n }

console.log(
    evalScript(
        pfactorial.$( 5 )
    )
); // UPLCConst { _type: [0], _value: 120n }

console.log(
    evalScript(
        pfactorial.$( 20 )
    )
); // UPLCConst { _type: [0], _value: 2432902008176640000nn }

evalScript is especially useful if you need to test your plu-ts code; regardless of the testing framework of your choice you will be always able to run evalScript.

errors and traces

Optimizations

Now that we are familiar with the core concepts of plu-ts, let's have a look at how we can get the best out of our programs.

plet

Up until this part of the documentation we wrote plu-ts code that didn't need to re-use the values we got, but in a real case scenario that is quite common.

One might think that storing the result of a plu-ts function call can solve the issue, but it actually doesn't.

Let's take a look at the following code:

const pdoubleFactorial = plam( int, int )
    ( n => {
        // DON'T COPY THIS CODE; THIS IS REALLY BAD
        const factorialResult = pfactorial.$( n )

        return factorialResult.add( factorialResult );
    });

At first glance, the code above is not doing anything bad, right? WRONG!

From the plu-ts point of view the function above is defined as:

const pdoubleFactorial = plam( int, int )
    ( n => 
        pfactorial.$( n ).add( pfactorial.$( n ) ) 
    );

which is calling pfactorial.$( n ) twice!

The intention of the above code is to store the result of pfactorial.$( n ) in a variable and then re-use that result, but that is not what is going on here.

Fortunately plu-ts exposes the plet function that does exactly that; we can rewrite the above code as:

const pdoubleFactorial = plam( int, int )
    ( n => 
        plet( pfactorial.$( n ) ).in( factorialResult =>
            factorialResult.add( factorialResult )
        )
    );

This way plu-ts can first execute the pfactorial.$( n ) function call and store the result in the factorialResult which was the intended behaviour in the first place.

plet allows to reuse the result of a computation at costs near to 0 in terms of both script size and execution cost, and for this reason is an extremly powerful tool.

"pletting" utility terms methods

When working with utility terms it's important not to forget that the methods are just partially applied functions so if you plan to use some of the methods more than once is a good idea to plet them.

As an example, when working with the TermList<PElemsT> utility term, intuition might lead you to just reuse the length property more than once in various places; but actually, each time you do something like list.length (where list is a TermList); you are just writing plength.$( list ) (as in the first case introduced here) which is an O(n) operation!

What you really want to do in these cases is something like:

plet( list.length ).in( myLength => {
    ...
})

This is also true for terms that do require some arguments.

Say you need to access different elements of the same list multiple times:

const addFirstTwos = lam( list( int ), int )
    ( list => 
        padd
        .$( list.at( pInt(0) ) ) 
        .$( list.at( pInt(1) ) ) 
    );

What you are actually writing there is:

const addFirstTwos = lam( list( int ), int )
    ( list => 
        padd
        .$( pindexList( int ).$( list ).$( pInt(0) ) ) 
        .$( pindexList( int ).$( list ).$( pInt(1) ) ) 
    );

If you notice, you are re-writing pindexList( int ).$( list ) which is a very similar case of calling the pfactorial function we saw before twice.

Instead is definitely more efficient something like:

const addFirstTwos = lam( list( int ), int )
    ( list => plet( list.atTerm ).in( elemAt =>
        padd
        .$( elemAt.$( pInt(0) ) )
        .$( elemAt.$( pInt(1) ) ) 
    ));

When is convenient NOT to plet?

You definitely don't want to plet everything that is already in a variable; that includes:

  • arguments of a function
  • terms already pletted before
  • terms that are already hoisted (see the next section)
  • terms extracted from a struct using pmatch/extract; extract already wraps the terms in variables

phoist

Another great tool for optimizations is phoist and all hoisted terms.

Hoisting

( source: MDN Docs/Hoisting )

Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables or classes to the top of their scope, prior to execution of the code.

You can think of hoisted terms as terms that have been pletted but in the global scope.

So once you use a hoisted term once, each time you re-use it you are adding almost nothing to the script size.

You can create a hoisted term by using the phoist function. This allows you to reuse the term you hoisted as many times as you want.

This makes phoist a great tool if you need to develop a library for plu-ts; because is likely your functions will be used a lot.

Let's say we wanted to create a library for math functions. We definitely want to have a way to calculate factorials; we already defined pfactorial while introducing recursion, however that definition is not great if we need to re-use it a lot because the term is always inlined.

But now we know how to fix it:

const pfactorial = phoist(
    precursive(
        pfn([
            lam( int, int ),
            int
        ],  int)
        (( self, n ) =>
            pif( int ).$(
                n.ltEq( pInt(1) )
            )
            .then( pInt(1) )
            .else(
                n.mult(
                    papp(
                        self,
                        n.sub( pInt(1) )
                    )
                )
            )
        )
    )
)

If you compare this definiton with the previous one you'll see that nothing has changed except for the phoist, that's it; now we can use pfactorial as many times we want.

Can I use phoist everywhere?

No

phoist only accepts closed terms (aka. Terms that do not contain external variables); if you pass a term that is not closed to phoist it throws a BasePlutsError error.

So things like:

const fancyTerm = plam( int, int )
    ( n => 
        phoist( n.mult( pInt(2) ) ); // error.
    )

will throw because the variable n comes from outside the phoist function, hence the term is open (not closed).

pforce and pdelay

plet and phoist are the two main tools to use when focusing on optimizations; this is because they significantly reduce both script size and cost of execution.

pforce and pdelay do slightly increase the size of a script but when used properly they can save you quite a bit on execution costs.

As we know, plu-ts is strictly evaluated, meaning that arguments are evaluated before being passed to a function. We can opt out of this behaviour using pdelay which wraps a term of any type in a delayed type so a term of type int becomes delayed( int ) if passed to pdelay. A delayed type can be unwrapped only using pforce; that finally executes the term.

There are two main reasons for why we would want to do this:

  • delaying the execution of some term we might not need at all
  • prevent to raise unwanted errors

One example of the use of pforce and pdelay is the pif function.

In fact, the base if then else function is pstrictIf, however when we use an if then else statement we only need one of the two arguments to be actually evaluated.

So when we call pif, it is just as if we were doing something like:

pforce(
    pstrictIf( delayed( returnType ) )
    .$( myCondtion )
    .$(
        pdelay( caseTrue )
    )
    .$(
        pdelay( caseFalse )
    )
)

so that we only evaluate what we need.

Not only that, but if one of the two branches throws an error but we don't need it, everything goes through smoothly:

pforce(
    pstrictIf( delayed( int ) )
    .$( pBool( true ) )
    .$(
        pdelay( pInt( 42 ) )
    )
    .$(
        pdelay( perror( int ) )
    )
)

Here, everything is ok. If instead we just used the plain pstrictIf

    pstrictIf( int )
    .$( pBool( true ) )
    .$( pInt( 42 ) )
    .$( perror( int ) ) // KABOOM !!!

this would have resulted in an error, because the error is evaluated before being passed as argument.

stdlib

In this section we cover what is present in the standard library.

Here are the present functions that might be needed in any general program but might be more complex than functions like built-ins.

utility terms

We already saw Utility terms in general while explaining the language.,

We also saw how every method of an utility terms basically translates into a function that has that term as one of the arguments.

Here we cover those functions.

The aviable utility terms are:

TermInt

type definition:


type TermInt = Term<PInt> & {
    
    readonly addTerm:       TermFn<[PInt], PInt>
    readonly add:           ( other: Term<PInt> ) => TermInt

    readonly subTerm:       TermFn<[PInt], PInt>
    readonly sub:           ( other: Term<PInt> ) => TermInt

    readonly multTerm:      TermFn<[PInt], PInt>
    readonly mult:          ( other: Term<PInt> ) => TermInt

    readonly divTerm:       TermFn<[PInt], PInt>
    readonly div:           ( other: Term<PInt> ) => TermInt

    readonly quotTerm:      TermFn<[PInt], PInt>
    readonly quot:          ( other: Term<PInt> ) => TermInt

    readonly remainderTerm: TermFn<[PInt], PInt>
    readonly remainder:     ( other: Term<PInt> ) => TermInt

    readonly modTerm:       TermFn<[PInt], PInt>
    readonly mod:           ( other: Term<PInt> ) => TermInt

    
    readonly eqTerm:    TermFn<[PInt], PBool>
    readonly eq:        ( other: Term<PInt> ) => TermBool
        
    readonly ltTerm:    TermFn<[PInt], PBool>
    readonly lt:        ( other: Term<PInt> ) => TermBool
        
    readonly ltEqTerm:  TermFn<[PInt], PBool>
    readonly ltEq:      ( other: Term<PInt> ) => TermBool
        
    readonly gtTerm:    TermFn<[PInt], PBool>
    readonly gt:        ( other: Term<PInt> ) => TermBool
        
    readonly gtEqTerm:  TermFn<[PInt], PBool>
    readonly gtEq:      ( other: Term<PInt> ) => TermBool
        
};

add

parameter: other type: Term<PInt>

returns TermInt

equivalent expression:

padd.$( term ).$( other )

adds other to the term is defined on and returns the result

sub

parameter: other type: Term<PInt>

returns TermInt

equivalent expression:

psub.$( term ).$( other )

subtracts other to the term is defined on and returns the result

mult

parameter: other type: Term<PInt>

returns TermInt

equivalent expression:

pmult.$( term ).$( other )

multiplies other to the term is defined on and returns the result

div

parameter: other type: Term<PInt>

returns TermInt

equivalent expression:

pdiv.$( term ).$( other )

performs integer division using the term is defined on and other as divisor; returns the result rounded towards negative infinity:

exaxmple:

pInt( -20 ).div( pInt( -3 ) ) // ==  -7

quot

parameter: other type: Term<PInt>

returns TermInt

equivalent expression:

pquot.$( term ).$( other )

performs integer division using the term is defined on and other as divisor; returns the quotient rounded towards zero:

exaxmple:

pInt( -20 ).quot( pInt( 3 ) ) // ==  -6

remainder

parameter: other type: Term<PInt>

returns TermInt

equivalent expression:

prem.$( term ).$( other )

performs integer division using the term is defined on and other as divisor; returns the remainder:

exaxmple:

pInt( -20 ).remainder( pInt( 3 ) ) // ==  -2

mod

parameter: other type: Term<PInt>

returns TermInt

equivalent expression:

pmod.$( term ).$( other )

returns the term the method is defined on, in modulo other.

exaxmple:

pInt( -20 ).mod( pInt( 3 ) ) // ==  1

eq

parameter: other type: Term<PInt>

returns: TermBool

equivalent expression:

peqInt.$( term ).$( other )

integer equality

lt

parameter: other type: Term<PInt>

returns: TermBool

equivalent expression:

plessInt.$( term ).$( other )

returns pBool( true ) if term is strictly less than other; pBool( false ) otherwise

ltEq

parameter: other type: Term<PInt>

returns: TermBool

equivalent expression:

plessEqInt.$( term ).$( other )

returns pBool( true ) if term is less or equal to other; pBool( false ) otherwise

gt

parameter: other type: Term<PInt>

returns: TermBool

equivalent expression:

pgreaterInt.$( term ).$( other )

returns pBool( true ) if term is strictly greater than other; pBool( false ) otherwise

gtEq

parameter: other type: Term<PInt>

returns: TermBool

equivalent expression:

pgreaterEqInt.$( term ).$( other )

returns pBool( true ) if term is greater or equal to other; pBool( false ) otherwise

TermBool

type definition:

type TermBool = Term<PBool> & {
    
    readonly orTerm:    TermFn<[PBool], PBool>
    readonly or:        ( other: Term<PBool> ) => TermBool

    readonly andTerm:   TermFn<[PBool], PBool>
    readonly and:       ( other: Term<PBool> ) => TermBool

}

or

parameter: other type: Term<PBool>

returns TermBool

equivalent expression:

por.$( term ).$( other )

OR (||) boolean expression

and

parameter: other type: Term<PBool>

returns TermBool

equivalent expression:

pand.$( term ).$( other )

AND (&&) boolean expression

TermBS

type definition:

type TermBS = Term<PByteString> & {

    readonly length: TermInt
    
    readonly utf8Decoded: TermStr
    
    readonly concatTerm: TermFn<[PByteString], PByteString>
    readonly concat: ( other: Term<PByteString>) => TermBS

    readonly prependTerm: TermFn<[PInt], PByteString>
    readonly prepend: ( byte: Term<PInt> ) => TermBS

    readonly subByteStringTerm: TermFn<[PInt, PInt], PByteString>
    readonly subByteString: ( fromInclusive: Term<PInt>, ofLength: Term<PInt> ) => TermBS
    
    readonly sliceTerm: TermFn<[PInt, PInt], PByteString>
    readonly slice:     ( fromInclusive: Term<PInt>, toExclusive: Term<PInt> ) => TermBS
    
    readonly atTerm:    TermFn<[PInt], PInt>
    readonly at:        ( index: Term<PInt> ) => TermInt


    readonly eqTerm:    TermFn<[PByteString], PBool>
    readonly eq:        ( other: Term<PByteString> ) => TermBool

    readonly ltTerm:    TermFn<[PByteString], PBool>
    readonly lt:        ( other: Term<PByteString> ) => TermBool

    readonly ltEqTerm:  TermFn<[PByteString], PBool>
    readonly ltEq:      ( other: Term<PByteString> ) => TermBool

    readonly gtTerm:    TermFn<[PByteString], PBool>
    readonly gt:        ( other: Term<PByteString> ) => TermBool

    readonly gtEqTerm:  TermFn<[PByteString], PBool>
    readonly gtEq:      ( other: Term<PByteString> ) => TermBool

}

length

returns TermInt

equivalent expression:

plengthBs.$( term )

utf8Decoded

returns TermStr

equivalent expression:

pdecodeUtf8.$( term )

concat

parameter: other type: Term<PByteString>

returns: TermBS

equivalent expression:

pappendBs.$( term ).$( other )

concatenates the bytestring on which the method is defined on with the one passed as argument and returns a new bytestring as result of the operation

prepend

parameter: byte type: Term<PInt>

returns: TermBS

equivalent expression:

pconsBs.$( byte ).$( term )

expects the byte argument to be an integer in the range 0 <= byte <= 255

adds a single byte at the start of the term the method is defined on and returns a new bytestring as result.

subByteString

parameter: fromInclusive type: Term<PInt>

parameter: ofLength type: Term<PInt>

returns: TermBS

equivalent expression:

psliceBs.$( fromInclusive ).$( ofLength ).$( term )

takes fromInclusive as index of the first byte to include in the result and the expected length as ofLength as second parameter.

returns ofLength bytes starting from the one at index fromInclusive.

somewhat more efficient than slice as it maps directly to the builtin psliceBs function.

slice

parameter: fromInclusive type: Term<PInt>

parameter: toExclusive type: Term<PInt>

returns: TermBS

equivalent expression:

psliceBs.$( fromInclusive ).$( psub.$( toExclusive ).$( fromInclusive ) ).$( term )

takes fromInclusive as index of the first byte to include in the result and toExclusive as the index of the first byte to exclude

returns the bytes specified in the range

at

parameter: index type: Term<PInt>

returns: TermInt

equivalent expression:

pindexBs.$( term ).$( index )

returns an integer in range 0 <= byte <= 255 representing the byte at position index

eq

parameter: other type: Term<PByteString>

returns: TermBool

equivalent expression:

peqBs.$( term ).$( other )

bytestring equality

lt

parameter: other type: Term<PByteString>

returns: TermBool

equivalent expression:

plessBs.$( term ).$( other )

returns pBool( true ) if term is strictly less than other; pBool( false ) otherwise

NOTE bytestrings are ordered lexicographically

meaning that two strings are compared byte by byte

if the the byte of the first bytestring is less than the byte of the second; the first is considered less;

if it the two bytes are equal it checks the next

if the second is less than the first; the second is considered less;

ltEq

parameter: other type: Term<PByteString>

returns: TermBool

equivalent expression:

plessEqBs.$( term ).$( other )

returns pBool( true ) if term is less or equal than other; pBool( false ) otherwise

gt

parameter: other type: Term<PByteString>

returns: TermBool

equivalent expression:

pgreaterBS.$( term ).$( other )

returns pBool( true ) if term is strictly greater than other; pBool( false ) otherwise

gtEq

parameter: other type: Term<PByteString>

returns: TermBool

equivalent expression:

pgreaterEqBS.$( term ).$( other )

returns pBool( true ) if term is greater or equal than other; pBool( false ) otherwise

TermStr

type definition:

type TermStr = Term<PString> & {

    readonly utf8Encoded: TermBS
    
    readonly concatTerm:    TermFn<[ PString ], PString>
    readonly concat:        ( other: Term<PString> ) => TermStr

    readonly eqTerm:    TermFn<[ PString ], PBool >
    readonly eq:        ( other: Term<PString> ) => TermBool
}

utf8Encoded

returns TermStr

equivalent expression:

pencodeUtf8.$( term )

concat

parameter: other type: Term<PString>

returns: TermStr

equivalent expression:

pappendStr.$( term ).$( other )

returns the result of concatenating the term on which the method is defined on and the other argument,

eq

parameter: other type: Term<PString>

returns: TermBool

equivalent expression:

peqStr.$( term ).$( other )

string equality

TermList<PElemsType>

type definition:

type TermList<PElemsT extends PDataRepresentable> = Term<PList<PElemsT>> & {

    readonly head: UtilityTermOf<PElemsT>

    readonly tail: TermList<PElemsT>

    readonly length: TermInt

    readonly atTerm:        TermFn<[PInt], PElemsT>
    readonly at:            ( index: Term<PInt> ) => UtilityTermOf<PElemsT> 
    
    readonly findTerm:      TermFn<[PLam<PElemsT,PBool>], PMaybeT<PElemsT>>
    readonly find:          ( predicate: Term<PLam<PElemsT,PBool>> ) => Term<PMaybeT<PElemsT>>

    readonly filterTerm:    TermFn<[PLam<PElemsT,PBool>], PList<PElemsT>>
    readonly filter:        ( predicate: Term<PLam<PElemsT,PBool>> ) => TermList<PElemsT>

    readonly preprendTerm:  TermFn<[PElemsT], PList<PElemsT>>
    readonly preprend:      ( elem: Term<PElemsT> ) => TermList<PElemsT>
    
    readonly mapTerm: <ResultT extends ConstantableTermType>( resultT: ResultT ) =>
        TermFn<[PLam<PElemsT, ToPType<ResultT>>], PList<ToPType<ResultT>>>
    readonly map:     <PResultElemT extends PType>( f: Term<PLam<PElemsT,PResultElemT>> ) => 
        TermList<PResultElemT>

    readonly everyTerm: TermFn<[PLam<PElemsT, PBool>], PBool>
    readonly every:     ( predicate: Term<PLam<PElemsT, PBool>> ) => TermBool
    
    readonly someTerm:  TermFn<[PLam<PElemsT, PBool>], PBool>
    readonly some:      ( predicate: Term<PLam<PElemsT, PBool>> ) => TermBool

}

NOTE most of the equivalent expressions and some of the terms that requre some other informations are plu-ts generics

What is UtilityTermOf?

TermList is a generic, and it works for every PType

However, given a generic PType we don't know what is its utility term or even if it has any

UtilityTermOf handles all that; if PElemsT is something that can have an utility term it returns that utility term; otherwise returns the plain term.

example

UtilityTermOf<PByteString> === TermBS

UtilityTermOf<PDelayed<PByteString>> === Term<PDelayed<PByteString>>

returns: UtilityTermOf<PElemsT>

throws if the list is empty ([])

equivalent expression:

phead( elemsT ).$( term )

returns the first element of the list

tail

returns: UtilityTermOf<PElemsT>

throws if the list is empty ([])

equivalent expression:

ptail( elemsT ).$( term )

returns a new list with the same elements of the term except for the first one.

length

returns: TermInt

equivalent expression:

plength( elemsT ).$( term )

O(n)

returns the number of elements present in the list.

at

parameter: index type: Term<PInt>

returns: UtilityTermOf<PElemsT>

throws if index >= length

equivalent expression:

pindexList( elemsT ).$( term ).$( index )

returns the element at position index.

find

parameter: predicate type: Term<PLam<PElemsT,PBool>>

returns: Term<PMaybeT<PElemsT>>

equivalent expression:

pfind( elemsT ).$( predicate ).$( term )

returns PMaybe( elemsT ).Just({ val: elem }) where elem is the first element of the list that satisfies the predicate; returns PMaybe( elemsT ).Nothing({}) if none of the elements satisfies the predicate.

filter

parameter: predicate type: Term<PLam<PElemsT,PBool>>

returns: TermList<PElemsT>

equivalent expression:

pfilter( elemsT ).$( predicate ).$( term )

returns a new list containing only the elements that satisfy the predicate.

prepend

parameter: elem type: Term<PElemsT>

returns: TermList<PElemsT>

equivalent expression:

pprepend( elemsT ).$( elem ).$( term )

returns a new list with the elem element added at the start of the list.

map

parameter: f type: Term<PLam<PElemsT,PResultElemT>>

returns: TermList<PResultElemT>

equivalent expression:

pmap( elemsT, resultT ).$( f ).$( term )

returns a new list containing the result of applying f to the element in the same position.

NOTE mapTerm requires the return type of f; this is not true for map because map can understand the type directly from the parameter f.

every

parameter: predicate type: Term<PLam<PElemsT, PBool>>

returns: TermBool

equivalent expression:

pevery( elemsT ).$( predicate ).$( list )

applies the predicate to each term of the list and returns pBool( false ) if any of them is pBool( false ); pBool( true ) otherwise;

some

parameter: predicate type: Term<PLam<PElemsT, PBool>>

returns: TermBool

equivalent expression:

psome( elemsT ).$( predicate ).$( list )

applies the predicate to each term of the list and returns pBool( true ) if any of them is pBool( true ); pBool( false ) otherwise;

TermFn<Ins,Out>

The type definition of TermFn is more complex than what it actually does; but that is only because it automatically handles functions with an unspecified (potentially infinite) number of parameters;

All it does tough is just adding the "$" method that replaces the papp call.

to give an idea here how the case of a function from a single input to a single output looks like;

we'll call this type TermLam even if there's no type called so in plu-ts

type TermLam<PIn extends PType, POut extends PType> =
    Term<PLam<PIn,POut>> & {
        $: ( input: Term<PIn> ) => Term<POut>
    }

TermStruct<StructDef>

TermStruct is an other type that is unnecessarely complicated. This time because it has to mess around with the struct definition; however if we clean all tha complexity to what is strictly needed, TermStruct would look something like this:

type TermStruct<SDef extends ConstantableStructDefinition> = Term<PStruct<SDef>> & 
    IsSingleKey<SDef> extends true ?
    {
        extract: ( ...fields: string[] ) => {
            in: <PExprResult extends PType>
                ( expr: ( extracted: StructInstance<...> ) => Term<PExprResult> )
                 => Term<PExprResult>
        }
    }
    : {}

even with these semplifications it might seem a bit complex but really all is telling us is that it adds the extract method only if the struct can only have one single constructor; and adds nothing if it has more.

Infact we already encountered this method while introducing pmatch; we just didn't know that it was an utility term.

combinators

Combinators are functions that take functions as input and return new funcitons as output.

The plu-ts standard library exposes the most common of them so that can be hoisted and reused usually at cost near to zero.

This is because often defining a combinator implies defining a "wrapping" function over the input(s) and then apply it; whereas by hoisting them we just need an application.

NOTE since combinators may work with functions of different types; their types are polymorphic;

In contrast to the rest of the standard library; combinators types are made polymorphic using the tyVar aproach; this is because ofthen is possible to infer the result type from the inputs themselves (which are functions)

pcompose

pcompose type looks like this

fn([
    lam( b, c ),
    lam( a, b )
],  lam( a, c ))

so it takes two funcions: the first that goes from b to c and the second from a to b; and finnally returns a function from a to c.

the type should already tell us a lot of what pcompose is doing; in partiular we notice that the return type of the second function is the input type of the first;

not only that, the result function takes as input the same type of the second function and returns the same thing of the first.

So what pcompose is doing is just creating a function that is equivalent to the following expression:

fstFunc.$( sndFunc.$( a ) )

pflip

pflip type is:

lam(
    fn([ a, b ], c ),
    fn([ b, a ], c )
)

and all is doing is flipping the position of the first two arguments; so that the second can be passed as first.

NOTE if you are using the ...Term version of the TermList higher order functions ( list.mapTerm, list.filterTerm, list.everyTerm, ... ) then pflip is already in the scope and you can use it a cost 0.

PMaybe

definition:

const PMaybe = pgenericStruct( tyArg => {
    return {
        Just: { val: tyArg },
        Nothing: {}
    }
});

as we see PMaybe is a pgenericStruct with one type argument.

It rapresents an optional value.

Infact in plu-ts there is no such thing as the undefined that we have in typescript/javascript; however there are computations that can't be sure to actually return a proper value; as an example pfind that works with lists, might actually not find anything; in that case in typescript we might want to reutrn undefined; in plu-ts instead we just retutn Nothing.

pisJust

the plu-ts type is:

lam( PMaybe( tyVar("any") ), bool )

and what it does is really simple:

returns pBool( true ) if the argument was constructed using the Just constructor of the PMaybe generic struct;

returns pBool( false ) otherwise.

API - Core ideas

structure of a smart contract

Where are the inputs coming from?

PScriptContext

makeValidator

compiling

Examples

Advanced Concepts

typescript values as plu-ts macros