DEV Community

Stefano Fago
Stefano Fago

Posted on

Experimenting around FP in PHP

Warn: in this Post, it's tried to use as much typing as possible also if sometimes this means to take some distance from official documentation

The evolution of the PHP language has had an effective contribution from some powerful specifications:

These are now enriched by new RFC:

Note: These RFCs make possible a lot of different constructs that go further than what will be exposed in this Post: remember that the "White Rabbit hole" is deep!

Callable, Anonymous Function, Closure, and FP!

Callables are generally used as the main type hint to define callbacks: a summary of how they can be defined/used can be seen in the official documentation. Introducing the __invoke function, even at the interface or anonymous class level, makes it possible to manage an object as a function transforming it into a Callable.


class Butterfly {
  public function __invoke():void {echo "flying...";}
}
$butterfly = new Butterfly();
$butterfly(); // flying...

Enter fullscreen mode Exit fullscreen mode

Closure is the class used to represent anonymous functions: this kind of construct, also in the form of arrow function, is really useful not only as a callback, in place of a callable, but also more usage since the ability to capture a different scope or to simulate FP constructs such as currying like in the following code:


class Person{
  private function __construct(
                     public readonly string $name, 
                     public readonly string $familyName,
                     public readonly int $age){}

    public static function create():\Closure
    {
     return 
          static fn(string $name):\Closure
          => static fn(string $familyName):\Closure
            => static fn(int $age):Person
             => new Person($name,$familyName,$age);
    }
}

$person = Person::create()('Frank')('Jamison')(50);

Enter fullscreen mode Exit fullscreen mode

The introduction of First-Class Callable Syntax makes it possible to create Closure from any kind of function, defining a sort of function reference usable in places different from the definition (what follows reuse the example seen above)


$personBuilder = Person::create(...);
$frank = $personBuilder()('Frank')('Jamison')(45);
$john= $personBuilder()('John')('Smith')(40);

Enter fullscreen mode Exit fullscreen mode

Arrays, arrays functions, and the anonymous functions

The use of anonymous functions is particularly useful when we refer to native functions on arrays: the potential is notable and makes the use of loops superfluous as in the following code.

$stock = [
    ['name' => 'Phone', 'qty' => 0],
    ['name' => 'Notebook', 'qty' => 1],
    ['name' => 'SDD Drive', 'qty' => 0],
    ['name' => 'HDD Drive', 'qty' => 3]
];

$inStock = array_filter($stock, static fn($item) => $item['qty'] > 0);
Enter fullscreen mode Exit fullscreen mode

Native functions on arrays bring out the functional world when it comes down to it to the fold operation (the array_reduce function) that allows us to be a point on which to build other fundamental functions such as map and filter:


/**
 *
 *@template T
 *@template R
 *@param array<T> $data
 *@param callable(T):R $mapper
 *@return array<R>
 *
 */
function map(array $data, callable $mapper):array
{
 return array_reduce($data, 
/** @param T $element */ 
function(array $accumulator, $element) use ($mapper)
             {
              $accumulator[] = $mapper($element);
              return $accumulator;
            }, []);
}

/**
 *
 *@template T
 *@param array<T> $data
 *@param callable(T):bool $predicate
 *@return array<T>
 *
 */
function filter(array $data, callable $predicate):array
{
 return array_reduce($data, 
/** @param T $element */ 
function(array $accumulator, $element)use($predicate){                    
      if($predicate($element)){   
        $accumulator[]=$element;
      } 
 return $accumulator;},[]);
}

Enter fullscreen mode Exit fullscreen mode

The improved performance on arrays, starting from PHP 7.X, and the handling of anonymous functions are convenient from both a performance and memory consumption point of view although the practice of only using arrays sometimes doesn't make you consider the multiple benefits of alternative data structures.

Anonymous functions and closures are easy to distinguish given the use clause that allows you to capture elements from outside the given anonymous function. The value of the captured element is defined when the closure is declared and cannot be changed.


class Product{ ... }    

final class PriceRange {

   private function __construct(private float $minimumValue,
                                private float $maximumValue){}

    public static function of(
                     float $minimumValue, 
                     float $maximumValue): PriceRange{
     return new PriceRange($minimumValue,$maximumValue);
    }

    public function priceFilter():\Closure{
        return fn(Product $value)=> 
               $value->price() >= $this->minimumValue 
                             &&
               $value->price() < $this->maximumValue;
    }
}

/** @var array<Product> $prodcuts */
$products = [ ... ];

$range = PriceRange::of(5.0,10.0);

array_filter($products, $range->priceFilter());

Enter fullscreen mode Exit fullscreen mode

Closures are still handy in design where you want to isolate the visibility of data, always in the light of the FP paradigm, as in the following example.


/** @return \Closure():int */
function rndNumber():\Closure{
    return fn()=> rand(0,getrandmax()-1);
}

/** @return \Closure():int */
function rndConstant():\Closure{
    $constant = rand(0,getrandmax()-1);
    return fn()=> $constant; 
}

$rnd1 = rndNumber();
$c1 = rndConstant();
$c2 = rndConstant();

echo $rnd1(); // 505035335
echo $rnd1(); // 1353685165
echo $c1();   // 2030702172
echo $c1();   // 2030702172

Enter fullscreen mode Exit fullscreen mode

These possibilities are also useful in approaching internal domain-specific languages (DSL) or adopting the higher-order function (in FP slang): the Loan Pattern and the Execute Around Pattern are examples of this concept.

//
//Example of Execute-Around...
// ...also used in DSL definition for nested-dsl
//
 MailSender::Send(
        fn(Mail $config):Mail=>
            $config
                    ->From("john.black@kmail.com")
                    ->To("jack.white@jmail.com")
                    ->Subject("Test message")
                    ->Body("Hello World!")
        );

//
// Example of Load Pattern
//
class ToyBox{

   ...

    /**
     * 
     * @param \Closure(ToyBox):ToyBox $boxFillerPolicy
     * @param \Closure(ToyBox):ToyBox $playActions
     * @return \Closure():void
     */
    public static final function play(\Closure $boxFillerPolicy, \Closure $playActions):\Closure
    {
        return function()use($boxFillerPolicy,$playActions):void{   
                  $box = new ToyBox();
                  $box->Open();
                  try{
                      $box = $boxFillerPolicy($box);
                      $box = $playActions($box);
                  }finally{
                      $box->Close()
                          ->CleanUpToys();
                  }
        };
    }

    ...

    public function nextToy():ToyBox{ ... }
    public function addToy(string $toyName):ToyBox{ ... }    
}

function Usage():void{
        $refill = fn(ToyBox $box):ToyBox
           =>$box
             ->addToy("lego")
             ->addToy("mechano")
             ->addToy("laser"); };

        $play = fn(ToyBox $box):ToyBox{
          => $box
            ->nextToy()
            ->nextToy()
            ->nextToy(); };

        $playSession = ToyBox::play($refill, $play);       
        $playSession();

Enter fullscreen mode Exit fullscreen mode

The Scope element in Closures

Closures can refer to different scopes than those in which they were created thanks to the bind functions. The possibilities of closures become considerable and they could replace the use of Reflection since they can access the private state of objects: this, however, has limitations in the presence of static analysis infrastructures such as PHPStan and PSALM which highlight abuses of closures as in the following example.


class SimpleClass { 
    private int $privateData = 2; 
}

$simpleClosure = function():int {return $this->privateData;};
$instance = new SimpleClass();
$resultClosure = \Closure::bind(
                       $simpleClosure, 
                       $instance, 
                       SimpleClass::class);

echo $resultClosure==false?-1:$resultClosure();

...
//
//PSALM Output
//  
// ERROR: InvalidScope - Invalid reference to $this in a 
//          non-class context
// INFO: MixedInferredReturnType - Could not verify the  return 
// type 'int' for...
//

Enter fullscreen mode Exit fullscreen mode

It's however possible to find ways to overcome these problems and exploit appropriately the potential of the closures from the outside of a class. A possible trick is to define a bridge operation (a protected function, also static if needed) to use when the Closure is created, as follows:


class SimpleClass {
    private int $privateData = 2;
    protected final function data():int{ 
          return $this->privateData;
    }    
}

$instance = new SimpleClass();

// @phpstan-ignore return.unusedType
$accessTo = fn (string $bridgeOperation): ?\Closure
   =>
    \Closure::bind(
        function(SimpleClass $instance) use ($bridgeOperation): int{
            /** @var callable():int $operation */ 
            $operation = [$instance,$bridgeOperation];
            return $operation();
        },
        NULL,
        SimpleClass::class);

/** @var null|\Closure(SimpleClass):int $resultClosure **/
$resultClosure = $accessTo("privateData");

echo ($resultClosure===null?-1:$resultClosure($instance)); // 2

Enter fullscreen mode Exit fullscreen mode

This approach seems more verbose but we can make it reusable and it passes static analysis given also more strong typing. It's also an example of how we can approach the FP concept of Optics.

Conclusion

The importance of the exposed specifications is not only in having introduced types for the different elements but it's also in the new design possibilities. They have also brought PHP even closer to natively approaching Functional Programming: so it's important to study and experiment also with static analysis enabled! It is worth noticing that with PHP you can approach FP both in a more function-oriented way or in a more object-oriented way using generics and advanced techniques thanks also to PHPStan or PSALM.

To Go Furter

More elements need to be considered, here not exposed as for the design consequences of read-only constructs or the Generators, but also if not adopted, it's useful to consider these concepts to enrich our ability to design Clean Software!

Other resources allow the adoption of functional idioms/constructs and to study the concepts and approaches presented in this post: so... Happy Learn!

Top comments (0)