imports

module Plutarch.Docs.UsingHaskellLevelFunctions (pelimList, pelimList') where
import Data.Kind (Type)
import Plutarch.Prelude hiding (pelimList)

When to use Haskell level functions?

Although you should generally prefer Plutarch level functions, there are times when a Haskell level function is actually much better. However, figuring out when that is the case - is a delicate art.

There is one simple and straightforward use case though, when you want a function argument to be lazily evaluated. In such a case, you should use a Haskell level function that pdelays the argument before calling some Plutarch level function. Recall that Plutarch level functions are strict.

Outside of that straightforward use case, figuring out when to use Haskell level functions is quite complex. Haskell level functions will always be inlined when generating the Plutus Core. Unless the function is used only once, this sort of inlining will increase the script size - which is problematic.

However, if the function is used only once, and making it Plutarch level causes extra plams and #s to be introduced - you should just make it Haskell level. For example, consider the pelimList implementation:

pelimList ::   PElemConstraint PBuiltinList a => Term s (a :--> PBuiltinList a :--> r) -> Term s r -> Term s (PBuiltinList a) -> Term s r
pelimList match_cons match_nil ls = pmatch ls $ \case
  PCons x xs -> match_cons # x # xs
  PNil -> match_nil

It takes in a Plutarch level function, let's see a typical usage:

pelimList
  (plam $ \x xs -> pcons # x # (self # xs))
  pnil
  ls

This is rather redundant, the above snippet will translate to:

pmatch ls $ \case
  PCons x xs -> (plam $ \x xs -> pcons # x # (self # xs)) # x # xs
  PNil -> match_nil

Extra plams and #s have been introduced. Really, pelimList could have taken a Haskell level function instead:

pelimList' ::
  forall (a :: S -> Type) (r :: S -> Type) (s :: S).
  PElemConstraint PBuiltinList a
  => (Term s a -> Term s (PBuiltinList a) -> Term s r)
  -> Term s r -> Term s (PBuiltinList a)
  -> Term s r
pelimList' match_cons match_nil ls = pmatch ls $ \case
  PCons x xs -> match_cons x xs
  PNil -> match_nil

Now, the following usage:

pelimList
  (\x xs -> pcons # x # (self # xs))
  pnil
  ls

would turn into:

pmatch ls $ \case
  PCons x xs -> pcons # x # (self # xs)
  PNil -> match_nil

It turns out that using pelimList almost always involves using a one-off Haskell level function (and therefore a redundant plam). As such, pelimList benefits greatly from just taking a Haskell level function directly.

However, not all higher order functions benefit from taking Haskell level functions. In many higher order function use cases, you could benefit from passing a commonly used function argument, rather than a one-off function argument. Imagine map, you don't always map with one-off functions - often, you map with existing, commonly used functions. In these cases, that commonly used function ought to be a Plutarch level function, so it can be hoisted and map can simply reference it.