見出し画像

Retro-gaming and so on

モジュールの話

星田さんの記事を見て笑ってた(笑)。

(モジュールの話を)最初見たときには「なんか面倒くさそうだなw」という印象で深入りする気は無かった

わははははwwww
すんげぇ良く分かる(笑)。
実は僕も元々すげぇ嫌だったのは間違いない。

実際問題、プログラミング入門書関係でも「モジュール」と言うのを扱ってるヤツもあれば扱ってないヤツもある。
書き捨てのスクリプト程度を作るには実際問題必要がない。単にテキストエディタで開いた1ページに全部詰め込んで書けば済む話だ。
しかし、そこそこ大規模なアプリケーションを書こう、とすればプログラム全体像を1枚のテキストファイルで把握する、っつーのは難しくなってくる。アドベンチャーゲームなんかもそこそこ規模がデカいアプリケーションに含まれるだろう。

モジュール、でプログラムを別々のファイルとして分けて書き、そして1つにまとめる、ってのは「なんとなく」分かるだろう。
かと言って、実際問題、じゃあ「どうやって分割していくのか」と言うノウハウがあるのか、とか訊かれれば実は僕も聞いた事が無い。
うん、無いんだ。
例えば、これは必ずしもモジュールではないが、例として、xnp2と言うソフトウェアを見てみよう。これはLinux用のNEC PC-9801用エミュレータなんだけど、Windows版np2とは違い、xnp2はありがたい事にソースコードで配布されている・・・つまり、自分でCコンパイラを使ってソフトウェアを「作らないと」ならない形式だ。
これのソースコードのパッケージは次のような構成になっている。


何か規則性があるのか・・・?少なくとも、フォルダはソフトウェアの「機能別」に分かれてる、としかいいようがない(例えばサウンド、とかCPU別のソースコード、とか、名前から推測する事が出来る)。
同じような、往年のPCエミュレータ、linapple 2を見てみよう。これはLinux用Apple IIエミュレータだが、これもソースコードでの配布になっていて、自分でソフトウェアを「作らないと」ならない。
これはこういう構造になっている。


アイコン用画像情報やフォントが入っていて、本体のソースコードはsrcと名付けられたフォルダに入っているようだ。細かく見なくても、上のPC-9801用エミュレータと全然構成が違う事が分かる筈だ。
ちなみに、srcフォルダの中身は以下のようになっていて、C++で書かれてるのが分かる。


いずれにせよ、どうファイルを分けるのか、どうフォルダに纏めるのかってぇのは「好きにやって良い」と言う事だが、同時にノウハウが無い、ってのはなんとも気持ちが悪い。

ところで、一体プログラミングに於いて「ファイルを分ける」と言うのはいつ頃から始まったんだろうか。
原点は分からない。いずれにせよ、かつての人々は一枚の巨大なテキストファイルとしてソフトウェアをプログラミングする、ってのが元々のスタイルだった、とは言えるだろう。
表面的に見てみると、Pascalが登場した1970年からC言語が登場した1975年の間に、ある程度大きな意識変革が最先端のコンピュータサイエンスの人たちに起きた、ってのは想像が出来る。
問題は、大きな1つのファイルを「コンパイル」すると、使えるソフトウェアに「なる」まで時間がかかる、って事だ(※1)。
これが仮に書き上がった「小さなファイル」を順次コンパイルしていくのなら、当然個別には時間がかからない。
そこで、複数の小さなファイルをコンパイルしておいて、それらを「統合」する事がこの時期に強く望まれたんだろう。その方がデバッグの時間もかからんし、小さいファイル毎にデバッグ済み->小さい部分を完成部品にする->最終的に部品同士を組み合わせてソフトウェアを作り上げる、方がラクだ、と言う事だ。これを分割コンパイルと言う。
1970年に登場したPascalは元々教育用言語目的として作られたんで、ぶっちゃけ、現実的なエンジニアリングに関しては全く考えてなかった。従って、分割コンパイル技法なんて導入されなかった。つまり、往年の(1970年以前の)フツーのプログラミング形式に則って、ソフトウェアを作るとしたら「一枚の巨大なテキストファイル」にならざるを得なかったんだ。
一方、その5年後に登場したC言語は。この「分割コンパイル」と言う技法が導入されていた。それどころか、C言語そのものが「バラバラな部品の塊」として提供されていた。C言語のコアだけでは「何も出来ない」ようにデザインされていて(※2)、そこに「バラバラになった部品」を読み込んで、やっと目的のプログラムを書けるようになる。つまり、まるで「プログラムをファイル別に記述する」方法論を自身に適用したようなプログラミング言語になっていたわけだ。
いずれにせよ、コンピュータサイエンス及び「エンジニアリング」の要求がこの5年前後で格段に進んだのだろう。
しかし、このパラダイム変換時はぶっちゃけ、パソコン以前のミニコン、なんかの時代なんで、あくまでまだ「最先端」の話だったわけだ。
C言語の登場に前後してパソコンがまずは自作キットの形式で発売されるわけだが、圧倒的に使われていたのは、高度な機能に欠けているBASICだった。そこではユーザーは、パソコンのメモリがミニコンより圧倒的に小さかった事もあり、1プログラム = 1ファイルの世界に住まざるを得なかったんだ。

実際この時代のちょっと後からPascalのパソコンへの移植から始まり、80年代後半へと向けてC言語もPCに入ってくる。
ただ、例えばこの時代から以降暫く経ってからのゲームのソースとか見ても、今の時代みたいに「別々のファイルにキチンと分けて書かれている」って例が、確認出来る分では意外と少なかったりする。やっぱり「巨大な一枚のファイル」だったりして、今のソフトウェアエンジニアリングでの「推奨手法」たぁかなり違った様相だったんだ。
ここで例えば、GNU Emacsの付属ソフトであるアドベンチャーゲーム、dunnetのソースコードを紹介しよう。これはオリジナルはさておき、1992年に書かれている。つまり今から30年前なんだけど、1992年にして「巨大な(3162行に及ぶ)」1枚のテキストファイルだ。
もちろん、GNU Emacsはテキストエディタなんで、本格的なプログラムには用いない、とか、Emacs Lispは所詮スクリプト言語処理系だ、とか入出力は全部Emacsに頼ってるから、とか色々と言い訳は可能だ(※3)。
しかし、気をつけて欲しいのは、これを書いたのが「往年のハッカーだ」って事だ。ハッカーなのにそれでも一枚のテキストファイルで仕上げてるし、メッセージは分離してないで関数本体に埋め込んでるし(※4)、とか、色々な意味で1992年の状況なんだ。
分かる?MITだぞ?1992年と完全にシンクロナイズしてないにせよ、かつてはこういう状況だった、と言う状況証拠ではあるんだ。

と言うわけで、総体的に見ると、プログラムを書いたファイルを小分けにしてモジュールとして落とし込む、ってのはプログラミングの歴史に於いても標準的には古い技法でもない、って事が分かるだろう。
実際、C言語的な、まだ単純なアイディアだった頃はさておき、多人数で大規模なプログラムを書かなアカン、って場合は色々な問題を解決しておかないとならない。大きな問題は名前空間の問題だ。
例えばH氏とI氏とC氏が何かのプログラムを書く、ってプロジェクトに雇われてたとしよう。
H氏もコツコツ作ったユーティリティがあるし、I氏もそうだし、C氏もそうだったとする。
それで3人が仕様に則って、部分的に別々に作ったプログラムを合わせてさぁ本番、ってなった際、実は3人共addって名前で機能的に全く別の関数を書いてたんで、関数名が衝突してバグっちまった・・・ってのが良く出てきた問題だったわけ。
さて、ここで、3人が別々に作った関数をどうやって「違う」モノとして判別すべきか・・・これはなかなかメンド臭い問題だ(※5)。
で、ある種、1975年のC言語の登場から、1996年辺りまでこの辺の問題っつーのは、研究対象ではあったんだけど、ほったらかしにされてたんだよな(笑)。
ある意味、「強制的にこのテの問題に解を与えた」のがご存知Java、だと思う。Javaが最初の実装例、ってワケじゃないんだけど、Javaが強制的に人々に知らしめた、と言うべきか。
いずれにせよ、この辺ではじめて「モジュール」って概念が一般に広まったんじゃないか、って思う。なんせ、Javaは言語設計上、1クラス1ファイル、ってのが基礎なんで、「強制的に」ファイルを分割して書かざるを得ない。従って、自然とモジュールを組んでプログラミングせなアカンわけ。
あんまJavaは詳しくないんだけど(勉強はしたが・笑)、割に「強制」ってのがJavaのキーワードだな、とは思う。オブジェクト指向を「強制」するし、ファイル分割でさえ「強制」する。
結局、Javaってのは、「個人プログラミング」には向いていない。これは別に悪口じゃない。そうじゃなくって最初っから「会社で使われて多人数での開発に使われる」前提でデザインされている言語なんだろう。そこではこういう「強制」は話し合うまでもなく自然と「必要になる」部分だ。この辺はある意味、旧Sun Microsystemsの慧眼だと思う。それはJavaを個人的に好きである、とか好きじゃない、とかと関係ない部分だ。Javaを個人で使え、って言われりゃNo, thank youなんだけど、会社で使うとすればベストだとは言えるだろう。何故ならJavaに従う以上「問題は問題にならない」からだ。少なくとも問題は先送りできる。
なお、(登場自体はJavaより先な)Python的な「モジュール」のシステムは、「何でもある」筈のANSI Common Lispでも仕様上制定されてない(※6)。これはANSI Common Lispの制定時(1994年)だと、こういうファイル分割に対してのオーソドックスなセオリーがまだ分からなかったから、だ(現時点でも実は決定打はない)。
代わりにANSI Common Lispが「決めた」のは、名前空間の分割法だけ、だ。これをpackageと呼んでいる。

「名前空間分割の方法だけは提供するんで、あとは勝手に好みでやってくれや。」

と言うのがANSI Common Lispの「方針」だった。
後に、仕様外のデファクト・スタンダードになるASDFと言うモノが定義されたが、最初の版が登場したのは2001年頃になる。
そんな中で、RacketもGaucheも2000年以降の「モジュールシステム」を積極的に導入してきた、ってのはなかなか先進的なアイディアだったんじゃないか。
繰り返すが、プログラミングの歴史で見てみると、「ファイル分割によるプログラミング技法」ってのは登場から考えると割に新しいアイディアに近いんだ。
言い換えると「古くて新しい」と言う事になる(※7)。

さて、と、こうやって歴史を絡めて「モジュール」と言う技法を概論で説明しておいて、だ。
星田さんの疑問にちと答えていってみよう。


 生来のメンドクサがりなんで、SRFI的にファイルに複数の関数を入れておいて一発でまるごと使えるようにしたいなぁ・・とw。ドキュメントを検索すると、なんとなく(all-defined-out)がそれっぽい気がするんだけど・・駄目でした。英語力ホント大事だな〜

まず、良く言われるんだけど、プログラミングに重要な才能はどうも「メンドクサがり」らしい。ハッカーになればなるほど「メンドクサイは悪」と考える傾向がある事だ。
よってそういう意味でも星田さんはハッカーになる才能があるんだろう。
そしてハッカーは「全身全霊でメンドクサイ事を回避する為に死力を尽くす」らしい(笑)。言い換えると「メンドクサイ事を回避する為にメンドクサイ事をする事を厭わない」と(笑)。文字通りメンドクサイ連中だ(笑)。
ついでに言うと、英語力は充分だ。
実の事言うと、星田さんの記事を見て「あれ?」ってんでRacketでの例示そのもので試験してみたんだが、Racketのドキュメントの通り動かない
つまり、ドキュメントと実行結果が乖離してるトコを見ても、恐らく何らかのバグだろう。
と言うのも、まずは、マクロmoduleを利用して・・・ってのは僕も昔やってたんだけど、ぶっちゃけPLT Scheme時代の遺産だと思う。現在ベースシステムがR6RS実装のChez Schemeになった事もあり、多分どっかの時点でこのmoduleが上手く動かない、ってバグが入ったんじゃないか。
つまり、星田さんが読んだ英語は正しいんだ。英語力の問題じゃなくって実装側が恐らく何らかのポカをやってる。あるいは、実装が変更になったトコにマニュアルの改訂が追っついてない、とか。
Racketのユーザーコミュニティに何か上がってないか、って調べてみたんだけど、誰も何も言ってない。
っつー事は、古いスタイルのmoduleは現在誰も使ってなくって、単にprovideで指定してるユーザーが殆どで、マニュアルの記述がRacketの動作に合ってない、って誰も気づいてない、って事だろう。
なお、all-defined-outは星田さんの読み通り、の機能だ。
従って、ファイル名をutil.rktと名付けて、

#lang racket

(provide (all-defined-out))

(define (wait)
 (sleep 1))

(define (saikoro)
 (random 1 7))

(define enumerate
 (case-lambda
  ((seq) (enumerate seq 0))
  ((seq start) (map (lambda (x y)
           (cons x y))
          (range start (+ (length seq) start))
          seq))))

としておけば当面は問題がないだろう。マクロmoduleは使う必要がない。
また、このutil.rktファイルがutilフォルダ内にある場合、呼び出す方は

(require "util/util.rkt")

と記述する。パス表記だが、ゲームプログラムからの相対パス記述になる。

例えば現在の作業フォルダがあって、ナウシカのゲームnausicaa.rktとユーティリティが入ってるutilフォルダが図のように同レベルにあった場合、呼び出しは上のように(require "util/util.rkt")となる。
このように、相対パスは常に「自分がいる階層から見てどこにあるか」を記述し、utilフォルダとnausicaa.rktが同階層にいるかどうかで記述の複雑さが代わる。
 仮に上階層に(つまりゲームプログラムが入ってるフォルダと同階層)にutilフォルダがあるとすると、UNIX的書法では(require "../util/util.rkt")と記述するが、開発時にはそんなにねぇケースだろう。

※1: 今はパソコンが速いんで、コンパイルなんて割に一瞬で終わる作業じゃねーの?とか思うんだが、当時はコンピュータが遅かったんで、コンパイルは今より遥かに時間がかかった。
この「コンパイル作業」と言うのが開発上時間が特に取られてた部分だ。なんせ、マルチタスクででもなければ「コンパイル作業中には他の作業が一切出来ない」。
つまり、インタプリタ vs. コンパイラと言うのは開発環境的に見ると、実行自体が遅くても「作った結果がすぐ見れる」方式と、実行はすぐ出来ないけど「作ったモノが速く動く」方式との対立である。
今はインタプリタとコンパイラの境界線はどんどんと曖昧になってるが、言い換えるとこれは「速いコンピュータの恩恵」で色々なアイディアが試せるようになった結果だ。しかしながら、昔日ではまさしく、「アチラを立てればコチラが立たず」と言ったようなクリティカルな問題だったんだ。
なお、今でも主にUNIX系OSで見られる「ソースコードからソフトウェアを作り上げる(実行形式を作る)」事を「ビルドする」と呼ぶ。
また、「ビルドする」際に指針とするスクリプトファイルにmakeファイル、等がある。

※2: 厳密に言うと、「何も出来ない」わけではない。「何も出力されない」だ。
C言語は設計上、入出力までもが本体と切り離されて「ライブラリ」としてまとめられている。
プログラミング入門者がC言語を最初に学ばされて謎に思う、ファイル冒頭の

#include <stdio.h>

は、入出力用のライブラリを読み込め、と言う意味で、仕様上はこれがなければC言語は入出力さえ「出来ない」プログラミング言語だ(ちなみに、stdio、と言うのは標準入出力、つまりStandard Input and Outputの酷い略称だ)。
かなり酷い設計に見えるが、実は、標準入出力、の部分はハードウェアメーカーによって「一番違いが大きい」部分で、結果ここを切り離して、「ハードウェア毎に勝手に設定してくれ」と言う丸投げ対応、ってのがC言語の設計のキモとなっている。
要するに、「C言語として扱った際」の入出力のフォーマットだけ提示しておくんであとは好きにしろ、と言うのがC言語設計者側の意図で、結果、ハードウェアから提供される直のブツか、あるいはOSのAPIから提供されるブツを「ラッピング」するだけで良くなる、と言う設計になっている。

※3: ちなみに、今作られてるEmacsの拡張プログラムは、むしろ現代のソフトウェア技法に則っていて、単一のファイルより複数のファイルを詰め込んだフォルダ、と言うような構成になってたりする。

※4: これを鑑みると、これより約10年以上前に書かれた「Wizardry」がメッセージ分離方式で書かれてた、と言うのは驚くべき事だろう。今では当たり前のテクニックを自慢気に話すロバート・ウッドヘッドってどうよ?って思う人もいるだろうが、実際問題、プログラムに文字列を引き連れたprintを埋め込む人、ってのは2000年を超えてからも存在し、結果、ソフトウェアのローカライズを難しくしてた、ってのは事実なんだ。
これで分かるのは、優れた方式でも、一般に広がって「常識化」するには、思ったより物凄く時間がかかる、と言う事だ。

※5: ちなみにPythonでは1ファイル = 1名前空間、と言う単純なルールを採用してる。と言うか、Pythonは割に全てに於いて、単純なルールを採用する。
例えばPythonで、

import functools

と書いた時、別ファイルfunctoolsで定義された関数等を全部import出来るわけだが、これに含まれるreduceと言う関数を使う際には、

functools.reduce(某)

と書かないとならない。これはfunctoolsと言う名前空間にあるreduceと言う意味で、言い換えると「reducefunctoolsと言う名前空間に属してる」と明示しないと使えない、と言う意味になる。
このように、Pythonでは1ファイル1名前空間、と言うのが徹底された原則で、自作のfoo.pyに定義されたbarと言う関数をbaz.pyと言う自作ファイルへ呼び出して使うには、foo.bar(某)と書かないとならないわけ。
このお陰で、複数のファイルにbarと言う別の機能での同名関数があった場合でも衝突しない、ってのがPythonのやり方だ。

※6: 厳密に言うと「ある」。
ANSI Common Lispでもrequireprovideと言うシステムはあるが、第一に実装依存だと言うこと(となるよう仕様で定義してる・笑)。第二に、ANSI Common Lispはコンパイラ指向の為、モジュールによるライブラリ化にC言語系のツールであるmakeに似たようなシステムがほしかったけど、古来のrequireprovideだと「力が足りない」と言う側面があったらしい。
加えると、後発のRacketやGaucheのような「使いたい関数だけ☓☓する」と言うような拡張機能もなく、どっちかっつーと古い、Emacs Lispに搭載されているrequireprovideに近い程、だ(Emacs Lispと違って、ANSI Common Lispは名前空間を簡単に分ける能力があるので、名前の衝突の回避に関しては格段に優れてはいるが)。
そして、「どういった関数を外部ファイルに使わせるか」と言うのも、ANSI Common Lispでは名前空間の問題として捉えていて、機能はdefpackageの方に集中している。
結果、「現代的なモジュールの仕組みがある」と言う意味では機能的にはenoughなんだけど、使うのがちと面倒、と思えるような「バラバラの材料として提供されている」と見る事が出来るわけだ。
どっちにしても、我々がPythonに見るようなシステムには、簡便性で見ると今一歩及ばない。
この辺の所々の問題を解決するように作られたのがASDFと言う事だ。
なお、古来のファイル分割の例が、それこそ実用Common Lisp(PAIP)のコードの例で、当然教科書だから、って事もあるんだけど、「前に作ったプログラムを利用して新しいプログラムを作っていく」と言うような流れが見て取れる。
1990年代ではある意味、それで充分ではあったんだ。

※7: 要するに、超古典的な方法論だと、ファイルを分割して、主プログラムの方から単純にサブにあたるファイルを"load"すりゃ一見問題が解決しそうな話ではある。
あるが、例えばそのサブファイルで「提供したい」関数aが、同じファイル内に記述されてる関数bを利用してます、って場合どうなるだろう。プログラムを書く側から言うと関数bの存在は隠蔽したい。そして、ここで良くありがちなaddって関数名をbが採用してた場合、そしてそれがプログラミング言語本体に含まれてた場合、明らかに衝突が起きる。
もちろん「ありがちなaddなんて名前を付けなきゃエエやん」ってのも正解ではあるんだけれど、プログラマはメンド臭がり屋だ。あるファイル内だけで通用すればいいだけ、の名前を熟考したい、なんて考える奴は稀だろう。
結果、本文にも書いたが、「名前空間」の問題が出てくるわけだ。
言い換えると、この辺の「別ファイルで使わせたい関数」「別ファイルにはロードさせたくない関数」なんかが1ファイル内で混在してる事が良くある。
結果、「都合の良いブツだけはロード出来て、そうじゃないヤツはロード出来ない」、プログラミング専用の特殊なロードの仕組みが欲しい、と言う話になってくるわけだ。
ザックリ言っちゃえば、これが「モジュール」と言う仕組みで、一般的な枠組み、と言うモノは存在しないが、各プログラミング言語、及び実装毎である程度似通ったシステムを提供してるのが今日だと言えよう。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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