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-tsexpressions - 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... )
-
key syntax to build
- v0.2.* :
- functions for standard API data structures interaction
-
plu-ts/offchainfunctions for basic transactions
- v0.3.* :
-
TermContimplementation to mitigate the callback hell issue -
plu-ts/offchaincomplete 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:
Languageexplains that is a programming language we are talking about.Domain Specificexplains that the language is meant for a specific set of tasks. The "Domain", or specific purpose ofplu-tsis the creation of Cardano smart contracts.embeddedmeans that it is a language inside another language. Whileplu-tsis 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
Termis a Typescript type defined inplu-ts.- Every value in
plu-tsis aTerm. In Typescript, we say each value extends Term (in the same way that "Dog" extends "Mammal"). - A
Termalso 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:
PUnita unique value that has no real meaning; you can see it asplu-tsversion ofundefinedornullin TypescriptPInta signed integer that can be as big as you wantPBoola boolean valuePByteStringequivalent of aBufferor aUint8ArrayPStringequivalent of the TypescriptstringPDataequivalent of theobjecttype in Typescript (it is the low level representation ofPStructs that we'll cover in a moment, so you usually won't usePData)PList<PType>equivalent of an Array in Typescript; note that all the elements in the list must be of the samePTypePPair<PType1, PType2>equivalent of a Typescript tuple ([ type1 , type2 ])PDelayed<PType>a delayed computation that returns a value of typePType; the computation can be run by passing the delayed value to thepforcefunctionPLam<PInput, POutput>a function that takes one single argument of typePInputand returns something of typePOutputPFn<[ PInput_0 , ...PType[] ],POutput>a function that takes multiple arguments (at least one) and returns something of typePOutputPAlias<PType>just an alias of the provided type; it behaves exactly like the Types of its argument, soPAlias<PInt>is equivalent to aPInt. This is useful for keeping track of a different meaning the type might have.PStruct<{...}>an abstraction overPData, 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->unitPInt->intPBool->boolPByteString->bsPString->strPData->dataPList->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
typestatic 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 type | function name | ts to plu-ts function signature |
|---|---|---|
unit | pmakeUnit | pmakeUnit(): Term<PUnit> |
int | pInt | pInt(x: number \ bigint): Term<PInt> |
bool | pBool | pBool(x: boolean): Term<PBool> |
bs | pByteString | pByteString(x: string \ ByteString \ Buffer): Term<PByteString> |
str | pStr | pStr(x: string): Term<PStr> |
data | pData | pData(x: Data): Term<PData> |
list | pList | * 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:
plampfn
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-tsfunctions 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-tsfunctions starting with a lower case "p"; indicating that we are in theplu-tsworld 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:
- the function we want to pass the argument to
- 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
stringtype to be theNametype
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
Ageto be non-negative; since a negative age doesn't really make sensewhen an alias will be constrained
plu-tswill 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
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
Dogwithout anything breakingchanging 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?

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 term | term 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:
- is more readable
- 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
pstrictIffunction 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 ) ) // KABOOMgenerally 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.
pletallows 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;extractalready 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:
othertype:Term<PInt>returns
TermIntequivalent expression:
padd.$( term ).$( other )
adds other to the term is defined on and returns the result
sub
parameter:
othertype:Term<PInt>returns
TermIntequivalent expression:
psub.$( term ).$( other )
subtracts other to the term is defined on and returns the result
mult
parameter:
othertype:Term<PInt>returns
TermIntequivalent expression:
pmult.$( term ).$( other )
multiplies other to the term is defined on and returns the result
div
parameter:
othertype:Term<PInt>returns
TermIntequivalent 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:
othertype:Term<PInt>returns
TermIntequivalent 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:
othertype:Term<PInt>returns
TermIntequivalent 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:
othertype:Term<PInt>returns
TermIntequivalent expression:
pmod.$( term ).$( other )
returns the term the method is defined on, in modulo other.
exaxmple:
pInt( -20 ).mod( pInt( 3 ) ) // == 1
eq
parameter:
othertype:Term<PInt>returns:
TermBoolequivalent expression:
peqInt.$( term ).$( other )
integer equality
lt
parameter:
othertype:Term<PInt>returns:
TermBoolequivalent expression:
plessInt.$( term ).$( other )
returns pBool( true ) if term is strictly less than other; pBool( false ) otherwise
ltEq
parameter:
othertype:Term<PInt>returns:
TermBoolequivalent expression:
plessEqInt.$( term ).$( other )
returns pBool( true ) if term is less or equal to other; pBool( false ) otherwise
gt
parameter:
othertype:Term<PInt>returns:
TermBoolequivalent expression:
pgreaterInt.$( term ).$( other )
returns pBool( true ) if term is strictly greater than other; pBool( false ) otherwise
gtEq
parameter:
othertype:Term<PInt>returns:
TermBoolequivalent 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:
othertype:Term<PBool>returns
TermBoolequivalent expression:
por.$( term ).$( other )
OR (||) boolean expression
and
parameter:
othertype:Term<PBool>returns
TermBoolequivalent 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
TermIntequivalent expression:
plengthBs.$( term )
utf8Decoded
returns
TermStrequivalent expression:
pdecodeUtf8.$( term )
concat
parameter:
othertype:Term<PByteString>returns:
TermBSequivalent 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:
bytetype:Term<PInt>returns:
TermBSequivalent 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:
fromInclusivetype:Term<PInt>parameter:
ofLengthtype:Term<PInt>returns:
TermBSequivalent 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:
fromInclusivetype:Term<PInt>parameter:
toExclusivetype:Term<PInt>returns:
TermBSequivalent 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:
indextype:Term<PInt>returns:
TermIntequivalent expression:
pindexBs.$( term ).$( index )
returns an integer in range 0 <= byte <= 255 representing the byte at position index
eq
parameter:
othertype:Term<PByteString>returns:
TermBoolequivalent expression:
peqBs.$( term ).$( other )
bytestring equality
lt
parameter:
othertype:Term<PByteString>returns:
TermBoolequivalent 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:
othertype:Term<PByteString>returns:
TermBoolequivalent expression:
plessEqBs.$( term ).$( other )
returns pBool( true ) if term is less or equal than other; pBool( false ) otherwise
gt
parameter:
othertype:Term<PByteString>returns:
TermBoolequivalent expression:
pgreaterBS.$( term ).$( other )
returns pBool( true ) if term is strictly greater than other; pBool( false ) otherwise
gtEq
parameter:
othertype:Term<PByteString>returns:
TermBoolequivalent 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
TermStrequivalent expression:
pencodeUtf8.$( term )
concat
parameter:
othertype:Term<PString>returns:
TermStrequivalent expression:
pappendStr.$( term ).$( other )
returns the result of concatenating the term on which the method is defined on and the other argument,
eq
parameter:
othertype:Term<PString>returns:
TermBoolequivalent 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-tsgenerics
What is
UtilityTermOf?
TermListis a generic, and it works for everyPTypeHowever, given a generic
PTypewe don't know what is its utility term or even if it has any
UtilityTermOfhandles all that; ifPElemsTis 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>>
head
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:
TermIntequivalent expression:
plength( elemsT ).$( term )
O(n)
returns the number of elements present in the list.
at
parameter:
indextype:Term<PInt>returns:
UtilityTermOf<PElemsT>throws if
index>=lengthequivalent expression:
pindexList( elemsT ).$( term ).$( index )
returns the element at position index.
find
parameter:
predicatetype: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:
predicatetype: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:
elemtype: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:
ftype: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
mapTermrequires the return type off; this is not true formapbecausemapcan understand the type directly from the parameterf.
every
parameter:
predicatetype:Term<PLam<PElemsT, PBool>>returns:
TermBoolequivalent 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:
predicatetype:Term<PLam<PElemsT, PBool>>returns:
TermBoolequivalent 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
...Termversion of theTermListhigher order functions (list.mapTerm,list.filterTerm,list.everyTerm, ... ) thenpflipis 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.