Skip to main content

plet

Up until this part of the documentation we wrote plu-ts code that didn't need to re-use values, 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.

Use plet Luke!

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

const pdoubleFactorial = plam( int, int )
( n => {
const factorialResult = plet( pfactorial.$( n ) )

return 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 behavior in the first place.

How does plet work?

When used as in the snippet above the compiler will take a look at how the value you stored in a variable is used and might decide to inline it if it decides that it is more efficient.

This will most of the time due to the value being used a single time.

So even if you are using the result of pfactorial.$( n ) a single time using plet won't store the result in a variable because there's no advantage.

But still if you use it two or more time it will be stored in a variable instead so that it is computed only once.

tip

When in doubt use plet;

The compiler is smart enough to understand if it should be inlined or stored in a variable

force execution with plet().in()

You can opt out the compiler taking control using the plet's in method.

info

The plet( stuff ).in( myVar => {/*...*/}) construct forces the term to be stored in a variable even if used once.

There are some cases where this might be the desired behavior;

as an example using the in method makes clear the scope of the variable;

but the most common use case is when you have a single reference in a piece of code that is recursive.

recursive example
const fancyDoubleMult = phoist(
pfn([ int, int ], int)
((a, b) => {

const myVar = plet( pInt(a).add(a) );

// how you would implement multiplication using only additions
return precursive(
pfn([
lam( int, int ),
int
], int)
(( self, n ) => {

return pif( int ).$( n.ltEq( 0 ) )
.then( 0 )
.else(
// we only have a single reference here;
// so this `myVar` might be inlined
myVar.add( self.$( n.sub( 1 ) ) )
);
})
).$( b )
})
)

Even though the compiler will try to do its best, if you want to be sure that you are not re executing some code for each recursive call you can use plet( stuff ).in( myVar => {/*...*/}), as follows:

recursive example
const fancyDoubleMult = phoist(
pfn([ int, int ], int)
((a, b) => {

// here we force `myVar` to be evaluated
// and stored in a variable
return plet( pInt(a).add(a) ).in( myVar =>
precursive(
pfn([
lam( int, int ),
int
], int)
(( self, n ) => {

return pif( int ).$( n.ltEq( 0 ) )
.then( 0 )
.else(
// same expression but we know
// we are not re-running `myVar`'s expression
myVar.add( self.$( n.sub( 1 ) ) )
);
})
).$( b )
);

})
)

"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( 0 ) )
.$( list.at( 1 ) )
);

What you are actually writing there is:

const addFirstTwos = lam( list( int ), int )
( list =>
padd
.$( pindex( int ).$( list ).$( 0 ) )
.$( pindex( int ).$( list ).$( 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 =>
// store the function to access the elements of the list
// in the `elemAt` variable
plet( list.atTerm ).in( elemAt =>
padd
.$( elemAt.$( 0 ) )
.$( elemAt.$( 1 ) )
)
);

When is convenient NOT to plet?

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

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