LoginSignup
4
0

More than 1 year has passed since last update.

Javascript 関数プログラミング【Haskell の State Monad を真似てみた】

Last updated at Posted at 2023-04-20

最近 Javascript の関数をラムダ式(アロー関数)を使って行う例が、ネット等に多く掲載されています。ラムダ計算は関数型プログラミングの基礎となっているそうです。それならば「ラムダ式を使う事で関数型プログラミングを Javascript でも体験出来るはずだ!」という事でチャレンジしてみました。

以下にソースコードを載せます。

/*
 *  State Monad Emuration program
 */
const lu = require("./Lists.js"  );       // List   操作ライブラリ
const ou = require("./Objects.js");       // Object 操作ライブラリ

const fst = lst => { return lst[ 0 ] };  //  タプルの初項を取得
const snd = lst => { return lst[ 1 ] };  //  タプルの次項を取得
const tpl = lst => { return [[],lst] };  //  [[value], [state]]

(fncP = listP => {                        //  配列出力関数
    if (listP.length === 0) return;
    console.log (lu.head (listP));
    return fncP (lu.tail (listP));
}) ((list1 => list2 => list3 => {         //  <= 処理本体
    (h = valB => tpC => {
        const ca = fst(tpC);              //  <= tuple 値
        const cs = snd(tpC);              //  <= tuple 環境
        if (cs.length === 0) return [valB, cs];
        const cx  = lu.head(cs);          //  <= list 先頭
        const cxs = lu.tail(cs);          //  <= list 残り
        return (cx === valB ? [lu.cons ( 0  ) (ca), cxs]
                            : [lu.cons (valB) (ca), cs ]);
        });
    (g = valA => lstB => tplC => {
        const ba = fst(tplC);             //  <= tuple 値
        const bs = snd(tplC);             //  <= tuple 環境
        if (lstB.length === 0) return [ba, bs];
        const bx  = lu.head(lstB);        //  <= list 先頭
        const bxs = lu.tail(lstB);        //  <= list 残り
        const rth = h (valA + bx) (tplC);
        return g (valA) (bxs) ([fst(rth), snd(rth)]);
    });
    (f = listA => listB => tuplC => {
        const aa = fst(tuplC);            //  <= tuple 値
        const as = snd(tuplC);            //  <= tuple 環境
        if (listA.length === 0) return [aa, as];
        const ax  = lu.head (listA);      //  <= list 先頭
        const axs = lu.tail (listA);      //  <= list 残り
        const rtg = g (ax) (listB) (tuplC);
        return f (axs) (listB) ([fst(rtg), snd(rtg)]);
    });
    //  evalState 相当の処理
    return lu.reve (fst (f (list1) (list2) (tpl(list3))));
                               //  state monad 化 ↑
}) ([ 1, 2, 3, 4, 5, 6, 7, 8, 9])
   ([ 1, 2, 3, 4, 5, 6, 7, 8, 9])
   ([ 3, 5, 7,11,13,17]));

// 処理結果
// [  2,  0,  4,  0,  6,  0,  8,  9, 10
// ,  3,  4,  5,  6,  7,  8,  9, 10,  0
// ,  4,  5,  6,  7,  8,  9, 10, 11, 12
// ,  5,  6,  7,  8,  9, 10, 11, 12,  0
// ,  6,  7,  8,  9, 10, 11, 12, 13, 14
// ,  7,  8,  9, 10, 11, 12, 13, 14, 15
// ,  8,  9, 10, 11, 12, 13, 14, 15, 16
// ,  9, 10, 11, 12, 13, 14, 15, 16,  0
// , 10, 11, 12, 13, 14, 15, 16, 17, 18 ]

サンプルにしたのは勉強の為に自作してみた Haskell の StateT Moand を使った IO Monad の持ち上げ確認プログラムです。3 つのリストの内一つを状態と見なして、他の2つのリストの積集合の要素合計と 3 つ目めの昇順に並んだリストが一致する場合はゼロとするリストを作成する物です。尚、3 つ目のリストは先頭がマッチするとリストから削除され二度とその値は使われない事とします。

map メソッドを使って最初に 2 つのリストを生成し、次に生成したリストと最後のリストとマッチングをするか、3 重ループ構造にするかのいずれかが考えられます。データ量によっては3重ループは結構重い事と、他の 2 つのリストと異なり 3 つ目のリストは他で生成したリストに対して全照合する訳では無いので、3 つ目のリスとを何度もこの場合だと list1.length × list2.length 回程 list3 をスキャンしてしまい兼ねません。そこで先頭から照合した要素を無す事で、list3 のスキャンをトータル 1 回で済済ませてしまおうという発想から上記の仕様ができました。(効率とかは言いっ子無しでお願いします。)

マッチした所の値が 1 回だけ 0 に成っています。見て分かる通り繰り返しはでラベルを使って再帰をしています。かなり変わった見慣れないコードだと思いますが、チャンと Node.js で動作します。list3 のリストが State Monad の環境に相当する値の初期値になります 。関数 f → 関数 g → 関数 h と 3 階層を跨いで値を変化させつつ受け渡しています。

書式の特徴としては、カリー化している事と自由変数を全て const で定義して書換を行っていない事が挙げられます。補助用の関数の内部ではディープコピーをしたのちに書換 (slice) を行ったりしてますが、外部の値を変更することは行っていません。もっとコードの集約はできると思うのですが、逆に読み辛い上に理解し辛くなるので一度自由変数に束縛するコードに留めてます。

以下に元となった Haskell のコードを添付します。

import Control.Monad.State
import qualified TestNumLists as L

f :: [Int] -> [Int] -> StateT [Int] IO [Int]
f arX arY = do  -- do は無くても良い
    case arX of [    ] -> return []
                (x:xs) -> do  -- do は絶対必要
                    ry  <- g x  arY
                    rys <- f xs arY
                    return $ ry ++ rys

    where

    g :: Int -> [Int] -> StateT [Int] IO [Int]
    g x arY = do  -- do は無くても良い
        case arY of [    ] -> return []
                    (y:ys) -> do  -- do は絶対必要
                        rz  <- h $ x + y
--                        lift $ print rz
                        rzs <- g x ys
                        return $ rz : rzs

    h :: Int -> StateT [Int] IO Int
    h xy = do  -- do は絶対必要
        arZ <- get;
        case arZ of [    ] -> return xy
                    (z:zs) -> do  -- do は絶対必要
--                        lift $ print z
                        put    $ if xy == z then zs else (z:zs)
                        return $ if xy == z then 0  else xy

main :: IO ()
main = do  -- do は絶対必要
    x <- runStateT (f L.list1 L.list2) L.list3
    putStrLn $ show x

-- λ> main
-- ([ 2, 0, 4, 0, 6, 0, 8, 9,10
--  , 3, 4, 5, 6, 7, 8, 9,10, 0
--  , 4, 5, 6, 7, 8, 9,10,11,12
--  , 5, 6, 7, 8, 9,10,11,12, 0
--  , 6, 7, 8, 9,10,11,12,13,14
--  , 7, 8, 9,10,11,12,13,14,15
--  , 8, 9,10,11,12,13,14,15,16
--  , 9,10,11,12,13,14,15,16, 0
--  ,10,11,12,13,14,15,16,17,18],[])

Javascript に比べるとコードがスッキリしています。これは引数がタプルである必要が無い(括弧がいらない)ことと、ポイントフリースタイルにより State 値が暗黙の裡に受け渡しが出来ているからです。因みにコメントアウトしている lift $ print 文ですが、これが本来試したかったコードです。State Monad 環境下に IO Monad をリフトアップする処理で他の Monad の糖衣構文 (do) の中でも IO Monad を使う事ができます。結果は repl 上の出力を採取・編集しました。

Javascript でラムダ計算を使って純粋関数型言語の様な処理が出来たので嬉しくなり投稿してしまいました。Javascript で State を引き摺り回すのは結構混乱しましたが、意外にもラムダ計算式で色々な事が出来る事が分かり感心しました。皆さんも色々挑戦してみられては如何でしょうか。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0