見出し画像

Retro-gaming and so on

JavaScriptと関数型プログラミング

さて、前回JavaScriptの話をチラッと書いた。
JavaScriptは「プログラミング言語として」見ると、基本設計は相当良く、構文の見た目に反し、事実上Lisp語族、特にSchemeに深い影響を受けた言語だ。
つまり、表向きはいわゆる「オブジェクト指向言語」なんだけど(※1)、設計の根幹はむしろ関数型言語の影響がデカイ。
ところが、基本設計はさておき、「入出力がない」JavaScriptは当然ながら「フツーのプログラミング学習」を行いづらい言語、となっている。どうしてもHTMLやらCSSやら塗れの「ゴチャゴチャしてる」コードを書かざるを得なく、初心者向けとしては極めて学習効率が悪い言語、となってるんだ。
これも何度も言ってるし、JavaScriptに限った話じゃないんだけど、「Webブラウザで簡単にプログラミングが出来る!インストール不要!」なんつー文言は100%クソだ。むしろ、余計面倒な事になる、ってのが本当のトコだ。

繰り返すが、プログラミング初心者にJavaScriptは向かない。
余談だけど、Webを見てると、プログラミング経験が無いにも関わらず「フリーランスエンジニアになりたい」とか言っていきなりJavaScriptを始める奴が結構多いんだよ(※2)。でも「JavaScriptとはどういう言語なのか」良く知らず(ロクな解説がねぇから・笑)、HTML/CSSに塗れた練習問題ばっかやってるうちに嫌気が差して結果いつの間にかフェードアウトする(笑)。
そういう例を何件か知っている。

僕もあんまりJavaScriptはマジメに勉強した事がない。もう参考書の例題がHTML塗れ、ってのが耐えられないのね(笑)。いっつも言ってるけど「関数を書く際出力を混ぜるな」ってのがプログラミングの原則なんだけど、少なくとも初心者用のJavaScriptの教科書はそれに違反している。しょーがないんだ、何せ入出力がないJavaScript「だけ」じゃ計算結果を見る事さえ出来ない。
もう一つ最悪なのが、やっぱC言語脳の存在なんだよ(笑)。Pythonがポピュラーになるはるか前に「それ」は起こってた。Pythonと違ってJavaScriptの構文スタイルはC言語のそれを模している。結果、C言語脳が集まりすぎ(それがそういう構文設計にした目的だったんだが・笑)、「JavaScript界隈のC言語脳による汚染」はPythonより遥かに早かったんだ。
C言語脳は「JavaScriptとはどういう設計に基づく言語なのか」全く調べないし考えようともしない。「C言語に構文が似てる!」ってだけで例によって「C言語的な」書き方しかせず、結果そういう「汚い」コーディングスタイルが蔓延する。
JavaScriptは「C言語脳が集まるとロクな結果にならない」と言う最初の例じゃなかろうか(※3)。
そしてこの文書が発表されるまで、C言語脳の存在が邪魔になって、長い間誰もJavaScriptの「正体」に気付かされなかったんだ。

C言語脳の侵食はさておいても、「HTMLやCSS塗れで書かなきゃなんない」JavaScriptの学習は洒落になんないくらい面倒臭い。
繰り返すが、確かにJavaScriptは「ブラウザで実行する」モデルになっているので、ある意味これは仕方がない、と言えば仕方がないんだ。

余談だけど、「JavaScriptはブラウザで実行するモデルだ」と言う事は、言い換えるとJavaScriptが実行される度に「貴方のパソコンのリソースが使われる」と言う事を意味する。
前回、こういう記述を紹介したんだけど。

> 今はJavascriptがWebアプリ開発言語としてなぜか脚光を浴びており、プログラミングスクール詐欺師共の格好のネタになっている。

「プログラミングスクール詐欺師共の格好のネタ」かどうか、ってのとそのプログラミング言語の性能は関係がない。そしてJavaScriptは必ずしもWebアプリ開発言語を意味しない。
と言う前提に立っても、恐らく一番メーワクなJavaScriptの使い方は何と言っても広告だろう(笑)。上の記述の通り、貴方が喜ばない(JavaScriptで書かれた)広告がある度、実は貴方のパソコンのリソースが使われてる、とすればこれは腹に据えかねる、ってのは分かるわけだ(笑)。
例えば、今はそうでもないけど、下手すればNTT docomoがgoo blogの管理画面に「余計な広告を入れる」とか「余計なサービスを付け足す」度に貴方のPCのリソースが使われる、って事はありえるわけだ。

こういうヤツだ。NTT docomoの発想が不思議なのは、このテの広告を貼るならブログの方じゃないか、と思うんだが、何故かブログの管理者にばっか広告を出す、と言うワケの分からん事をやっている。

何度か言ってるけど、21世紀の「フツーのプログラミング」ではさして書いたプログラムの「実行速度」は重要ではなくなった。しかし例外がある。Webブラウザだ。
元々、インターネット自体は研究所同士を繋げてたわけで、テキストだけの世界だったんだけど、今だと写真や動画があり、現在一番「実行速度」が重要視されるクリティカルな世界になったわけだ。
そしてJavaScriptは貴方のPCのリソースを消費する。JavaScriptを利用した広告が実行される度に貴方のパソコンのCPUやメモリが利用される。そしてWeb Assembly(WASM)はそういった「Webブラウザの(貴方のPCが利用される)ハードスケジュール」を緩和する為に作られたわけだよ。つまり、そういう「速度が速い」状況にしておいてJavaScriptで書かれた広告がコンパイルされて速い速度で実行されるわけだ(笑・※4)。
何やってんだろね(笑)。
もうそう言ったクソ状況を避ける為には、JavaScriptがねぇ、端末上のWebブラウザを使うしかねぇんじゃねぇの?とかちと思う(笑)。


Linuxの端末上で使えるWebブラウザ、w3m。余計な事は一切せず、粛々とテキスト情報を表示する。

さて、JavaScriptの学習の話へと戻ろう。
もし、JavaScript「だけ」を学習したいのなら、スタンドアローンのJavaScript処理系があるのかないのか、って話になる。何故なら、JavaScriptのスタンドアローン処理系さえあれば、HTMLやCSSに邪魔されずにJavaScript「だけ」に集中可能だから、だ。
一番導入が簡単なJavaScriptのスタンドアローン処理系にRhinoと言う処理系があった。RhinoはJavaで書かれたJavaScript処理系だ(※5)。僕はそれを愛用してたんだけど、残念ながら最新のECMAScriptの仕様に則っていず、キャッチアップしてないんで捨てざるを得なくなった。
JavaScriptエンジンと言われるブツは色々とあるが、結果、開発が活発でスタンドアローンとしても使える、となると、実際ブラウザのJavaScriptエンジンとして使われているMozillaのSpiderMonkeyとGoogleのV8くらいしか選択肢が無くなってしまう。
ただし、この2つは、Debian系Linuxみたいな環境だとダウンロード/インストールは簡単なんだけど、Windowsだと途端に敷居が高くなる。基本的にこれらはソースコードをダウンロードしてきて自分でビルド(実行ファイルを作る事)せなアカンからだ。
Web上で検索してみると、珍しく役立つ記事がQiitaにあった(笑)。WindowsでJavaScriptをWindowsの端末(DOS窓)で実行してみたい人はその記事に従ってSpiderMonkeyを入手してみて欲しい。それを使えば「HTML/CSS塗れ」のJavaScriptコードを書く必要はなくなる。
ここではSpiderMonkeyを使用する、と言う前提で話を進めよう。

JavaScriptは実はPythonよりよっぽど「関数型プログラミング」っぽいプログラミングをするのに望ましいプログラミング言語だ。
丁度Web検索をすると、適した題材があったんで紹介しよう。




正直な話、極めて単純な問題で、これを解いたから、と言って「スキルアップ」するとは思えないんだけど、「JavaScriptでの関数型プログラミング」っぽいプログラミング法を紹介するには単純だから故良い題材なんじゃないか、って思う。
なお、Lispで解いたらどうなるか。JavaScriptが「Schemeに強く影響を受けた言語」だとすると、ロジック的にはSchemeにほぼ準じたプログラムが書ける筈だ、と言う前提で考えてみよう。
いつもだったらここでRacketを使うトコだけど、RacketはSchemeである事を辞めてるんで、ちと題意的には相応しくない。良くってRacketはScheme方言だ。
そんなワケで、まずは日本で一番人気のLisp処理系であるGaucheでコードを書いてみた(※6)。
Scheme3つともそれぞれコードが若干違うが(※9)、基本的な発想は次のようなプロセスになる。

  1. 引数として受け取ったnumが26だろうが342だろうが、一の位の数値は無視して1にする(結果26は21になり、342は341になる)。
  2. iota(Pythonで言うrange)を使って、1. で得た数値を始点とし、1づつ増やした要素を持つ、長さ9のリストを作る。
  3. 2. で作成したリストから引数のmaxより大きい値(要素×要素の一の位)を算出する要素を削除する。
  4. 3. で得られたリストの長さが答えになる。
これだけ、だ。
4.がトリッキーと言えばトリッキーに見えるだろうが、3.で得られたリストの長さが問題の要求である一の位と一致する、ってのは良く考えてみれば分かるだろう。26を与えられた時は21、22、23、24、25、26、27の7つの要素を持つリストになり、342を与えられた時には341、342、343と言う3つの要素を持つリストが得られる。
また、一の位が0のケースは考えなくっていい。問題の要請ではx × 0を計算する事になるが、答えは当然0で、maxより小さくなるだろう事は明らかだろうし、maxが0ならそのまま「リストの長さが0」なので必然的に0となる。
このように、関数型プログラミングの大きな特徴の一つに、単一の値よりリストを基準とした演算を多用する、と言う事が挙げられ、結果、単一のデータよりコンパウンドデータ(合成データ)の処理が得意だ、と言う事が挙げられるだろう。
なお、SRFI-1の関数を使ってるのがインチキに見えるかもしれないが、構わない、と思ってる。SRFIは「外部ライブラリ」の類ではなく、「実装要求」であり、Scheme処理系の各実装者が好みで自分の実装に組み入れる性質のモノだが、SRFI-1はほぼ全ての実装に入ってる、と言って過言ではない。特にiotaナシでこのプログラムを書こうとすれば極めて面倒臭いんだ(※10)。

↑間違い。 「イ」しか合ってない(謎

しかし、ロジックをJavaScriptへ移す際、実はJavaScriptの仕様にはSchemeのiotaにあたる関数がない、んだ。
よってここが最大の難関になる。

JavaScriptのiotaの代替としての雛形は次のような計算式が考えられる。

[...Array(n).keys()]

恐らくJavaScriptの記述で「お?」とか思うのはこれだろう。
冒頭の...と言うのはスプレッド構文と呼ばれるものだ。これは平たく言うと、Pythonのアンパック(*)に近い。配列内で配列をバラバラにする効果がある。
順番に見てみよう。
Arrayコンストラクタは与えられた引数(整数)分の長さを持つ空の配列を生成する。

js> Array(9);
[, , , , , , , , ,]

結果、書式[Array(n)]は「配列の配列」を生成する。

js> [Array(9)];
[[, , , , , , , , ,]]

しかし、スプレッド構文を使えば、内側の配列はアンパックされて、全体では(void 0)を要素とする単なる配列と化す。

js> [...Array(9)];
[(void 0), (void 0), (void 0), (void 0), (void 0), (void 0), (void 0), (void 0), (void 0)]

この基本動作にいたずらを仕掛けるのがArray.prototype.keys()だ。Array(9)の情報をベースとしてそのインデックスのキーを含む、新しい配列イテレーターオブジェクトを返し、外側の配列内で実体化する。

js> [...Array(9).keys()];
[0, 1, 2, 3, 4, 5, 6, 7, 8]

結局このプロセスは、Racketで説明するのは心苦しいが、最初にリストを生成して、

> (make-list 9 null)
'(() () () () () () () () ())

そのリストに対してindexes-ofを適用するに等しい。

> (indexes-of (make-list 9 null) null)
'(0 1 2 3 4 5 6 7 8)

しかし、確かにインデックスを利用すると0から始まる配列なりリストは生成可能だが、上の手順を見ると特定の数からスタートした整数列を作らなければならない。
Racketなら上で得た整数列(リスト)に対してmapするだろう。

;; 与えられた数が26だった場合
> (let ((base (add1 (* (quotient 26 10) 10))))
  (map (lambda (x)
     (+ x base)) (indexes-of (make-list 9 null) null)))
'(21 22 23 24 25 26 27 28 29)

同様に、JavaScriptにもArray.prototype.map()がある。

// 与えられた数が26だった場合
js> const base = 1 + Math.floor(26 / 10) * 10;
js> [...Array(9).keys()].map((x) => x + base);
[21, 22, 23, 24, 25, 26, 27, 28, 29]

さて、これは「関数型プログラミング風」だけど、実際はオブジェクト指向プログラミングだ。Rubyでお馴染みのメソッドチェーンが行われている。
関数型プログラミングだとデータに対して関数を連続適用していって欲しい結果を入手する。一方、メソッドチェーンの場合は同様に、データに対してメソッドを連鎖していって欲しい結果を入手するわけだ。
そしてJavaScriptのラムダ式をアロー関数式と言う。この部分だな。

(x) => x + base

JavaScriptのアロー関数式はPythonのラムダ式より強力で、ブレース({})を利用すれば複文を取る事が可能だ。

(x) => {あれをしろ; これをしろ; ...}

従って、ここまでの知識/テクニックだけで、簡単に関数型プログラミングっぽいFizzBuzzさえ書いちゃう事が出来る(※11)。
さて、本題に戻るが、いずれにせよ上の計算で得られた長さ9の配列から条件に合う要素だけ残すようにフィルタリングすれば、題意を満たせる
JavaScriptでのフィルタリングはArray.prototype.filter()で、挙動自体はSchemeのSRFI-1のremoveとは逆だ。そこさえ気をつければコードを書き終える事は大して難しくもない。



なお、JavaScriptの変数宣言はvarletconstの三つがあるが、基本的に関数型プログラミング「っぽく」プログラミングしたければ、const一本槍で問題はない。何故なら関数型プログラミングは原則、再代入しないからだ。
また、この三つでは、ECMAScriptの現行仕様上、varの使用率が一番低い。

あるいはもっと関数型プログラミングに寄せてみようか。
いつぞや見た余再帰を用いてみる。まずはGaucheでunfoldを利用したコードを見てみよう(※12)。
そもそも、unfoldの定義を考えてみると、関数Funcの引数に与えられるnumそのものがseedになり、そこからリストを生成してる。
そしてunfoldに与える三つのラムダ式はそれぞれ次を意味している。

  1. 停止条件: xとxの一の位を掛けた数がmaxを超えた時に計算は終了する。
  2. 生成するリストの要素: これは3.が生成する「次点の数」そのものとなる。
  3. 更新: xは1づつ増えていくので、x + 1。
結果、例えば引数に26と200が与えられると、

gosh> (unfold (lambda (x) (> (* x (modulo x 10)) 200)) (lambda (x) x) (lambda (x) (+ x 1)) 26)
(26 27)

と2つの要素を持ったリストが返ってくる。
そして、リストの最後の要素の一の位の数値を返せばいいわけだ。

さすがのJavaScriptもunfoldは提供してないので、unfoldを自分で実装せなアカン。そこでSRFI-1に書かれている定義を利用して作ってみるわけだが、生憎JavaScriptはやっぱPythonと同様に再帰が苦手だ(※13)。
要するに、whileなんかの反復構文を使わなければならないが、実は実装そのものはそんなに難しくない。ライブラリ化しといてもいいだろう。
結果、お題はこんな風になるだろう(※14)。

さて、どうだったろうか。
JavaScriptはHTMLやCSSに邪魔されなければ、割に落ち着いて「フツーのプログラミング」が出来るし、関数型プログラミング(っぽいオブジェクト指向プログラミング)の優秀さはPython以上だ。
是非ともSpiderMonkey(あるいはGoogle V8)を入手して、JavaScriptプログラミングを楽しんで欲しい。

 
 
※1: JavaScriptのオブジェクト指向は登場時、プロトタイプベースと言うあまり聞き慣れない設計を持ち込んだ。結果、「JavaScriptのオブジェクト指向はオブジェクト指向ではない」と言うような論争まで発展したんだ(もちろん、本当は当然「オブジェクト指向」だ)。
JavaScriptに「何かJavaに比べると劣った言語だ」と言うような間違った印象を与えた一つの原因が、JavaScriptのオブジェクト指向は「クラスベース」じゃなかったから、だ。あまり「オブジェクト指向の理論的なバックグラウンドを知らない」プログラマが「理論をよく知らない」状態で、「Javaのオブジェクト指向が正しい」と言う根拠が無い「思い込み」でJavaScriptを批判したわけだ。
ただし、現在のJavaScript(ECMAScript)の仕様ではプロトタイプベースを基礎としつつもクラスも持つように改訂されている。
言わばJavaScriptは珍しくも「2つのオブジェクト指向を」同時に内包してるようなプログラミング言語となっている。

※2: そもそもピーター・ノーヴィグじゃないが、どうやら一部の人達は「コンピュータというものが、他のどんなものより、学ぶのがどういうわけか信じられないくらい易しい」と考えてる模様だ。

※3: 何度も繰り返すが「真のC言語エキスパート」の数は「C言語脳」の存在数より遥かに小さい。この2グループには雲泥の差があるんだ。
そして悲しい事に、「ある言語が人気が出る」と言う場合、今のトコBASICとPerlを例外とすれば「C言語脳が雲霞の如く」集まる現象のようで、それが起きたJavaScriptとPythonはロクでもない状態に追いやられたように見える。
言い換えると、一見「Pythonの後塵を拝してるように見える」Rubyコミュニティの方が、C言語脳を遠ざけてる、と言う一点に於いて、結果健全なままでいる、と見ることが出来る。
「C言語脳が集まる = 言語の人気が出る」と言う構図があるのなら、ヘタに言語の人気なんて出ない方がいいのかもしんない。

※4: 日本のWebページは古臭い、とデザイン上批判対称になる事が多いが、実のトコ、日本のWebページは一般にはそこまでJavaScript塗れになってないからだ、って事は言える。
欧米のWebページだとJavaScript比率が日本より高く「カッコいいWebページの演出が成される」度に見ている人のPCのリソースが使われる、って事になっている。
結局、欧米のページのJavaScript比率が高すぎて、Web Assemblyみたいな低レベル言語を作らないとどーにもなんない、ってトコまで追い込まれたんだろう。
そして欧米のページでは粛々と広告がコンパイルされる(笑)。

※5: 過去、Rhinoの一番簡単な導入方法はJavaの開発環境をダウンロード/インストールする事だった。Java SDKにRhinoが付録として付いていて、文字通り「Javaのスクリプト言語」になっていたが、最近はそれを止めてしまって、WindowsユーザーへのJavaScript単体処理系インストールの利便性が失われてしまった。

※6: 結果、現在の日本のLisp界隈に於いてはANSI Common LispよりSchemeの方が人気が高い。Gaucheはプログラミング界の王羲之、川合史朗氏作成のR7RS準拠のScheme処理系だ。
また、川合史朗氏が「滅多にこの世に存在しない」真のCエキスパートの一人だ。
なお、GaucheはUNIX生まれで、長い間公式ではWindowsをサポートしてなかったが、最近では公式にWindows版をサポートしだしたらしい。

※7: GNUの公式スクリプト言語。言わばGNUが提供する(予定の)UNIX互換OSでのVBScriptみたいなモノ。古いがSchemeのデファクトスタンダードであるR5RS準拠。
ただし、GNUが提供するUNIX互換OSはいまだ完成してなく、いわゆるLinux系OS(GNU/Linux)だとデフォルトでは搭載してなかったりする(意味ねぇじゃん・笑)。
Linux用VBScriptが無いように、Windows版Guileは存在しない。

※8: Checken Schemeも古いがデファクトスタンダードであるR5RS準拠のScheme処理系。特徴はC言語のソースコードへのコンパイルを行い、なおかつ実行形式を生成する多段コンパイラである事、だ。
ただし、マニュアルには「Cのソースコードが簡単に生成可能」なように記述されてるが、そんなに上手く行かない(笑)。外部ライブラリを使うと途端にダメダメだ(笑)。
なお、基本的にバイナリでの提供はしていなく、Windowsでも使えるが自分でソースコードからビルドしないとならない
結果、やっぱりDebian系Linuxのように「リポジトリからバイナリで提供してる」OSじゃないと色々と面倒臭い。

※9: R5RSの頃はライブラリのインポート機能でさえ定義されてなく、結果、デファクトスタンダードと言えど、処理系同士でコードを移す事が出来る「ポータブルなコードを書く」と言うのはSchemeでは極めて難しかった(シェバングも想定されてないし例外処理もないし、端末で動かす為のエントリポイントの前提も無い)。
そのうち、デカいR6RSになり、再び小さくなったR7RS-smallになり、この時期になって初めてSchemeで「ポータブル」なコードを書けるようになった、と言っていい。
よって、この3つの処理系は、どれも知名度が高くユーザー数は多いが、やはり日本で使うScheme処理系、となるとGauche一択になるだろう。
なお、RacketはR6RSの頃にはSchemeだったが、その後離反して独自路線に進む事となり、Gauche等のR7RS処理系とは互換性はない、って考えていい(RacketはPascalに対するDelphiっぽいニュアンス)。

※10: 実際、「なんでもある筈の」ANSI Common LispにはPythonのrangeにあたる関数はビルトインでは何故か無く、loop等を使って自作するしかないのでこの辺は利便性が落ちる。

※11: JavaScriptには三項演算子があるが、条件分岐自体は文だ。従って、アロー関数式の本体に直接「文」は埋め込めないので、ブレース(波括弧)が形成するブロックの力を借りないとならない。


※13: ECMAScriptの仕様だと、末尾再帰最適化を要求するようになった、と言った話もあったが詳しくは知らん(笑)。実装がまだまだ追いついてない、とか言われてたが現時点だとどうなんだろうか。

※14: Array.prototype.push()とか破壊的操作を用いてるが、一方、unfold内で生成した配列に対しての操作なので外部には影響はない。よってこのunfoldを使う事自体は関数型プログラミングを逸脱しない。
また、JavaScriptで任意のインデックスの配列要素を返す際にはArray.prototype.at()を用いる。Pythonと同様に、インデックスが負の数の時は配列の最後から要素を返していく。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事