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... )
-
key syntax to build
- 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 ofplu-ts
is the creation of Cardano smart contracts.embedded
means that it is a language inside another language. Whileplu-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 inplu-ts
.- Every value in
plu-ts
is aTerm
. 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 asplu-ts
version ofundefined
ornull
in TypescriptPInt
a signed integer that can be as big as you wantPBool
a boolean valuePByteString
equivalent of aBuffer
or aUint8Array
PString
equivalent of the Typescriptstring
PData
equivalent of theobject
type in Typescript (it is the low level representation ofPStruct
s 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 samePType
PPair<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 thepforce
functionPLam<PInput, POutput>
a function that takes one single argument of typePInput
and returns something of typePOutput
PFn<[ PInput_0 , ...PType[] ],POutput>
a function that takes multiple arguments (at least one) and returns something of typePOutput
PAlias<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
->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 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:
pair
s 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 theplu-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:
- 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
string
type to be theName
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 sensewhen 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
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 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 TermType
s 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
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.
"plet
ting" 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
plet
ted 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 plet
ted 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 everyPType
However, given a generic
PType
we don't know what is its utility term or even if it has any
UtilityTermOf
handles all that; ifPElemsT
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>>
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:
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 off
; this is not true formap
becausemap
can understand the type directly from the parameterf
.
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 theTermList
higher order functions (list.mapTerm
,list.filterTerm
,list.everyTerm
, ... ) thenpflip
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.