PHPでC#のLinqライクなクラスの実装


はじめに

PHPで関数型プログラミングの側面について勉強を行いました。

PHPはマルチパラダイムとまでは言いませんが、オブジェクト指向プログラミング関数型プログラミング が扱えます。メソッドチェーンについて少し触れたことがあったので、勉強がてら作成してみました。
今回作成したLcq(List control query)というクラスはC#で実装されているLinqという機能を模して作っています。Linqは非常に便利なので、C#erは覚えたほうがいいです。
PHPでもLinqを模したライブラリとしてGinqというのがあります。今回は勉強のために車輪の再発明を行いました。

関数型プログラミングについて

これに関しては長くなるのでここでは深く触れません。
関数型プログラミングとは、「データに何らかの処理を加えていく」の連続で組み立てていくものです。プログラムの関数と言うより数学の関数をイメージするといいかもしれません。
データ ⇒ f(データ) ⇒ データ´ ⇒ g(データ´) ⇒ データ´´

身近な関数型プログラミング言語としてSQLがあります。
以下の画像が関数型プログラミングの概要図になります。

(この画像は俺が後輩に対してUniRxを教える際に使った資料の一部を切り取った物です。SlideShareに載せてるので、興味があればご一読くださいな。https://www.slideshare.net/YuujirouItou/rx-class)

関数型プログラミングについて深く知りたい方は以下をご覧ください。
https://qiita.com/stkdev/items/5c021d4e5d54d56b927c

PHPの関数型プログラミングの仕様

PHPの関数型の特徴として、クロージャが使える事があげられます。
以下を参考にしてください。
https://www.sejuku.net/blog/80737

ただC#のようにクロージャに対して引数、返り値共に指定できないので、少し不便に感じました。

Lcqの実装

お待たせしました。本編でございます。
今回実装するLinqライクなクラスにはSelect操作とWhere操作しか実装していません。
PHPDocを使ってふんだんにコメントを書いているので、ここでの説明はある程度省略します。


<?php

/**
* PHP上でC#のLinqを行うための模倣クラス
* 現在はSelectとWhereのみ実装済
* (List control queryの略)
*/
class Lcq{
   //----Fields----
   private $arr;


   //----Methods----
   /**
    * コンストラクタ
    *
    * @param Array $arr
    */
   public function __construct($arr){
       //値の検証
       if(!is_array($arr)){
           throw new Exception('配列ではない引数の代入');
       }

       $this->arr = $arr;
   }

   //--Controllers--
   /**
    * 射影・選択を行うためのメソッド
    * 引数には射影用の関数を代入する事。
    * 関数の動作が不可能な場合は例外を出力。
    *
    * @param function $func
    * @return Lcq
    */
   public function select($func){
       //--Validate--
       if(!is_callable($func)){
           throw new Exception("Selectの引数が関数ではありません");
       }

       $result = [];
       //全要素に対して射影を行うループ処理
       for($i = 0; $i < count($this->arr); $i++){
           try{
               //引数に渡された関数にて選択・射影処理を行い、結果を格納。
               $tmp = $func($this->arr[$i]);
               array_push($result, $tmp);
           } catch (Exception $e){
               throw new Exception($e);
           }
       }

       //保持変数を入れ替え
       $this->arr = $result;
       return $this;
   }

   /**
    * SQLのWhereを行うためのメソッド
    * 引数には条件指定用の関数を代入する事。
    * 関数の動作が不可能な場合は例外を出力。
    *
    * @param function $func
    * @return Lcq
    */
   public function where($func){
       //--Validate--
       if(!is_callable($func)){
           throw new Exception("Selectの引数が関数ではありません");
       }

       $result = [];
       //全要素に対して条件判定処理を行うループ処理
       for($i = 0; $i < count($this->arr); $i++){
           try{
               //渡された関数で条件判定
               $compareResult = $func($this->arr[$i]);
               //bool値がtrueな場合、配列に追加する。bool値でなければ例外を返す
               if(!is_bool($compareResult)) throw new Exception("Whereに渡された関数の戻り値がbool値ではありません");
               if($compareResult) array_push($result, $this->arr[$i]);
           } catch (Exception $e){
               //例外が出た場合は何もせずに自身を返却
               print "Whereが成功しませんでした";
               return $this;
           }
       }

       //保持変数を入れ替え
       $this->arr = $result;
       return $this;
   }

   //--validate--
   /**
    * 配列の検証を行う関数
    *
    * @return Exception|void
    */
   private function arrayValidate(){
       if(!is_array($this->arr) || count($this->arr) <= 0){
           throw new Exception("配列の長さが0又は配列ではない値の保持");
       }
   }

   //--Getter--
   /**
    * 現在操作中の配列を返却する関数
    *
    * @return Array
    */
   public function toArray(){
       $this->arrayValidate();

       return $this->arr;
   }

   //--Creater--
   /**
    * 配列をLcqオブジェクトに変換する静的関数。
    * 配列を渡さない場合、例外を出力する
    *
    * @param Array $arr
    * @return Lcq
    */
   public static function From($arr){
       if(!is_array($arr) || count($arr) <= 0){
           throw new Exception("配列の長さが0又は配列ではない値の保持");
       }

       return new Lcq($arr);
   }
}

//配列からLCQオブジェクトを作成する
$arr = [2, 3, 5, 6, 3, 2];
$lcq = Lcq::From($arr);

/*
*メソッドチェーンにてコレクションを操作する
*即時関数にて比較用関数を渡している
*/
$result = $lcq
       ->select(function($x){return $x + 2;})  //x + 2で射影
       ->where(function($x){return $x >= 5;})  //x が 5以上の場合条件クリア
       ->toArray();

//操作した配列の内容を出力する
print "-------------------before-------------------" . PHP_EOL;
print_r($arr);
print "-------------------after--------------------" . PHP_EOL;
print_r($result);

以下が出力内容です。

-------------------before-------------------
Array
(
    [0] => 2
    [1] => 3
    [2] => 5
    [3] => 6
    [4] => 3
    [5] => 2
)
-------------------after--------------------
Array
(
    [0] => 5
    [1] => 7
    [2] => 8
    [3] => 5
)

おわりに

PHPは動的型付けなので、型宣言が無いですよね。
今回のLcqの開発で分かった型宣言が無いメリットデメリットがいくつかありました。
メリット
 ・勝手にGenericsとして実装されるため、ユーザが型を指定しなくてよい
 ・配列がほぼ可変長なので、新しく作る必要がほぼない
デメリット
 ・値が入っているかや、関数であるか証明するという検証が大変
 ・クロージャに対して返り値の型や引数の数等指定できないので、プログラムの動作に保証がほとんど無い

短い時間のみでの作成のため、設計もガバガバで動作も確証できません・・・。
配列の置き換え部分などが雑なので、メモリ使用量も多いと思います。
いい改善案などあったら仰っていただけますと幸いです。