世界を動かす技術を、日本語で。

イージー・フォース (2015)

概要

  • Forth はスタック指向のプログラミング言語で、独特な構文と操作性を持つ
  • このガイドでは Forth の基本から応用までを、JavaScript実装を使って解説
  • スタック操作ワード定義出力条件分岐ループ など主要機能を紹介
  • 例題 を通じてForthの考え方と使い方を体験できる構成
  • 他言語経験者向けに、 Forth の特徴的な思考法を学ぶための入門書

Forth入門:概要と特徴

  • Forth は1970年代に開発された スタック指向言語
  • 関数型オブジェクト指向 ではなく、 型チェック や複雑な構文もほぼ無し
  • 現代でも 一部の組込み機器や特殊用途で使用例あり
  • 新しい言語を学ぶことで 問題解決の視野拡大 に役立つ
  • 本書は JavaScript実装 の簡易Forthインタプリタを利用し、例題を試しながら学習

数値操作とスタックの基本

  • Forthでは 全ての操作がスタック を中心に行われる
  • 数値を入力すると スタックにプッシュ される
  • + 演算子はスタック上部2つの値を 加算 し、結果を再びスタックへ
  • 例:
    • 1 2 3 → スタック:1 2 3(上がTop)
    • + → 1 5(2+3=5)
    • さらに+ → 6(1+5=6)
  • オペレータが 後置 される 逆ポーランド記法 を採用
  • 括弧や優先順位を気にせず、 記述順がそのまま計算順序

スタック効果とワード定義

  • 各ワード(命令)は スタックの状態 を変化させる
  • スタック効果は( 入力 -- 出力 )で表現
    • 例:+ ( n1 n2 -- sum )
  • 新しいワードは: 名前 ... ;で定義
    • 例:: foo 100 + ;はスタックトップに100を加算
  • 未定義ワードや数値以外はエラーとして表示

スタック操作ワード

  • dup ( n -- n n ) :スタックトップを複製
  • drop ( n -- ) :スタックトップを削除
  • swap ( n1 n2 -- n2 n1 ) :トップ2つの値を交換
  • over ( n1 n2 -- n1 n2 n1 ) :2番目の値をトップに複製
  • rot ( n1 n2 n3 -- n2 n3 n1 ) :上位3つの値をローテーション

出力関連ワード

  • . ( n -- ) :スタックトップの数値を出力
  • emit ( c -- ) :スタックトップをASCII文字として出力
  • cr ( -- ) :改行出力
  • ." ( -- ) :文字列出力(定義内で使用)

条件分岐とループ

  • Forthにはboolean型がなく、0がfalse、0以外(主に-1)がtrue
  • =, <, > で比較、 and, or, invert で論理演算
  • if then :条件分岐(定義内のみ利用可)
    • 例:: buzz? 5 mod 0 = if ." Buzz" then ;
  • if else then :if/else文
  • do loop :C言語のfor文に近いループ
    • 例:: loop-test 10 0 do i . loop ;(0〜9を出力)

Fizz Buzzの例

  • Fizz Buzz もシンプルに実装可能
    • 3の倍数でFizz、5の倍数でBuzz、両方でFizzBuzz
    • doループと条件分岐を組み合わせて実現

(続きが必要な場合はお知らせください)

Hackerたちの意見

これ、前にも何回かここに出てきたよね(例): https://news.ycombinator.com/item?id=10634918 でも、Forthに対するみんなの反応を聞くのはいつも興味深いし、たまにこのスレッドで面白い話が出てくるから、文句はないよ。

そうだね。もう一度言うけど、実際にForthで役に立つコードを書いてる人はいないってことにも気づくよね。

これは素晴らしいリソースだし、ブラウザで動かすのは素早いフィードバックが得られていいね。シェアしてくれてありがとう!1ヶ月くらい前からForthを学び始めたんだけど、Andreas Wagnerのこの動画[1]は見るのが楽しかったよ。もしOPの本を読んでForthを実際に見てみたいと思ったら、この動画をおすすめするよ。 [1]: https://youtu.be/mvrE2ZGe-rs

自動スクロールのせいで、Safariではページがほとんど使えないね。

Firefoxでも同じだね。

jsを無効にすれば、FFで普通に使えるよ。

今日はHNでForthが見られて嬉しい!小さな実験的プロジェクトで遊ぶのが好きな人には、ForthとTixyにインスパイアされたミニマルでエソテリックなキャンバスカラーリング言語を作ったことがあるよ: https://susam.net/fxyt.html

すごくクールだね!もしデモプログラムのライブラリがあって、みんながそれをハックしてツールの力を実感できたら、さらに良くなると思う。編集:GitHubでそれを見たよ。でも、メインインターフェースに組み込んでいるツールも見たことがある。それは改善点

これ楽しい!円を描こうと思ったけど、sin/sqrtが足りなかったんだ。そこでルックアップテーブルを使おうと思ったけど、行き詰まっちゃった。円を描くためのアドバイスとかある?デモ#4を見てるんだけど(https://susam.net/fxyt.html#XYpTN1srN255pTN1sqD)、円形の形がどこから来てるのか知りたいんだ。Forth Haikuって見たことある?

Forthが他の多くの言語と違うのは、スタックの使い方だね。Forthでは、全てがスタックを中心に回っている。まあ、ほとんどの言語がそうなんだけど。主な違いは、プログラマーがそれにアクセスするのがメソッド呼び出しの定義みたいなもので制約されていないことだね。

Forthはほとんどの言語とは違って、2つのスタックを持ってるんだ。聞こえは些細だけど、いろんなことが変わるよ。これによって、よりスリムな呼び出し規約が可能になる。単一のスタックだと、関数呼び出しは引数を関数の戻りアドレスの上に「押し込まなきゃ」いけないけど、Forthは引数を「滑らせる」から、関数呼び出しがかなり軽くなるんだ。

ほとんどの言語には明示的なスタックがなく、暗黙のスタックもサブルーチン呼び出しのためだけに使われる。サブルーチン呼び出しをしていないなら、コンパイルされたコードはスタックにアクセスしないかもしれない。例えば、OpenBSDのstrlcpy関数を軽く編集したものを見てみよう:

size_t strlcpy (char *dst, const char *src, size_t siz) {
    register char *d = dst;
    register const char *s = src;
    register size_t n = siz;
    if (n != 0 && --n != 0) {
        do {
            if ((*d++ = *s++) == 0) break;
        } while (--n != 0);
    }
    if (n == 0) {
        if (siz != 0) *d = '\0';
        while (*s++) ;
    }
    return(s - src - 1);
}

GCC 12.2.0はこれを以下の18のARM命令にコンパイルする:

.text
.align 2
.global strlcpy
.syntax unified
.arm
.type strlcpy, %function
strlcpy:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
mov r3, r1
cmp r2, #0
beq .L6
.L14:
subs r2, r2, #1
beq .L3
ldrb ip, [r3], #1 @ zero_extendqisi2
strb ip, [r0], #1
cmp ip, #0
bne .L14
.L4:
sub r0, r3, r1
sub r0, r0, #1
bx lr
.L3:
mov r2, #0
strb r2, [r0]
.L6:
ldrb r2, [r3], #1 @ zero_extendqisi2
cmp r2, #0
bne .L6
b .L4
.size strlcpy, .-strlcpy

ARMアセンブリに詳しくないなら、この関数全体がスタックを全く使っていないことを教えておくよ。これは、strlcpyが他の関数を呼び出さないから可能なんだ(いわゆる「リーフサブルーチン」、または「リーフ関数」とも呼ばれる)し、ARMはほとんどのRISCと同様に、サブルーチンの戻りアドレスをスタックではなくレジスタ(lr)に置くからなんだ。呼び出し規約でも引数や戻り値をレジスタに置くから、関数はメモリとレジスタの間でデータを移動させたり、ループカウンタを減らしたり、ポインタを増やしたりすることができるんだ。FORTRAN 77までのFORTRANは再帰をサポートしていなかったから、スタックなしで実装できたんだ。それに対して、Forthではレジスタの代わりにオペランドスタックを使う。ループカウンタにはリターンスタックを使うこともある。時にはオペランドスタックを変数の代わりに使うこともできるけど、Forthを学び始めたばかりの初心者には、スタックを使うよりも変数を使う方がいいと思う。スタックを使おうとしすぎてトラブルになるより、変数を使おうとしすぎてトラブルになる方が、初心者にはずっと簡単だからね。

Forthはすごく楽しいし、新しい人がそれを発見するのを見るのはいつもワクワクするけど、3つの大きな問題があるんだ。まず一つ目は技術的な問題。Forthの強みは、制限された環境での自己ホスティングの開発ツールなんだ。例えば、RAMが256KiB以下、SSDなし、1MIPS未満、ハードディスクが10MB以下、あるいはフロッピーだけの環境ね。そんな環境では、メカニズムをあまり重複させる余裕がないから、プログラマーはそれに適応しなきゃいけない。だから、結局はかなり異なる目的に対して同じメカニズムを使うことになって、妥協が伴うんだ。でも結果は驚くべきもので、64KiBのRAMとCP/Mを搭載した8080でF83を動かせば、仮想メモリやマルチスレッド、ちょっと使いにくいWYSIWYGの画面エディタ、再帰と構造化制御フローを持つ言語のコンパイラ、アセンブラ、アプリケーション用のCLIとスクリプト言語が使えた。そんな環境は今ほとんど存在しないけど、例えばMSP430をプログラミングする場合(https://www.digikey.com/en/products/detail/texas-instruments...を参考にしてみて)、RAMはたったの2KiBしかないし、Mecrisp-Stellarisを使うことができる。あのチップのリソースはかなり限られてるんだ。お金の経済では、リソースをお金で測るから、限られたリソースのチップを使う理由はお金を節約するためか、もっと少ないお金で済ませるためなんだ。そのチップは7.40ドル。5.59ドルで、https://www.digikey.com/en/products/detail/stmicroelectronic...のような、100MHz、512MiBのフラッシュ、256KiBのRAM、50のGPIO、CANバス、LINバス、SD/MMCなどが手に入る。さらに、https://www.st.com/content/ccc/resource/technical/document/d...の表33によると、通常は25°で1.7Vのスタンバイモードで1.8μAを使うんだ。これはMSP430の0.1μAよりも多いけど、用途によってはまだ十分に低い数値だよ。(220mAhのCR2032コイン電池は理論的には1.8μAを13年間供給できるけど、実際の寿命は約10年だから、STM32はバッテリーの自己放電電流よりも少ない電力を使うんだ。)つまり、そんな小さなコンピュータのニッチは小さくて急速に縮小しているんだ。それに、マイクロコントローラが2KiBのRAMしか持っていないとしても、それをプログラムするために使うキーボードや画面は、ほぼ確実にRAMが百万倍、CPUが千倍速いコンピュータに接続されているんだ。だから、CやC++、Rustでプログラムして、速いコンピュータで遅くて膨れたコンパイラを動かして、マイクロコントローラ用にもっと効率的なコードを生成することができる。ターゲットデバイス自体でコードをビルドしなきゃならないケースはほとんどないんだ。Forthは簡単なことを簡単に、難しいことを可能にするために設計されたんだ。二つ目の問題は社会的なもの。最初の問題の結果、Forthを使っていた人たちはほとんどが他の場所に移ってしまった。今のForthコミュニティは、人工的な挑戦を求めているForth初心者がほとんどなんだ。難しいことを可能にする代わりに、簡単なことを難しくしたいって感じ。昔からForthを使っている人も少し残っているけど、彼らはForthが本当に難しいことを可能にしていた頃から使っているから、今のForthユーザーとは違うんだ。ほとんどの人はForthで実際のアプリケーションを書いたことがないし、Forthがなければ書けなかったようなものを書く宗教的な転換体験もしていない。三つ目の問題も社会的なもので、二つ目の問題の結果、今のForthのチュートリアルのほとんどはForthをよく知らない人が書いているんだ。このチュートリアルをざっと見たけど、まさにその例のように思える。例えば、即時語について全く説明していないし、いつ即時語を使わないべきかも触れていない。(もしForthで何かを書く方がCよりも簡単なことがあるとしたら、それは即時語を定義できるからで、Cのプリプロセッサでは届かない方法でアプリケーションのDSLに言語を拡張できるんだ。)それに、文字列処理については全く触れていないし、文字列処理はForth初心者がForthを使い始めたときに最もつまずくことの一つだから、ワードタイプについても言及していない。だから、著者がForthを学び続けて、チュートリアルをもっと多くの側面をカバーするように拡張してくれることを願っているよ。

ファイルから行を読み込んで、メモリ内で文字列を処理するのが、ある年のアドベントオブコードの3日目で使うのをやめた理由なんだ。良い解決策が見つからなくて、パッドの使い方に大きく逸脱しなきゃいけなかった。ファイルから完全な行を読み込むなんて、そんな簡単なことが、全くできなくなったんだ。もちろん、「ズル」をして入力をプログラムに直接入れることもできたけど、Forthを学びたかったから、これができるべきだと思ったんだ... 後でGForth 1.0がもっと文字列処理のワードを持っていると読んだけど、その頃には簡単な解決策を見つける希望をすでに失っていた。誤解しないでほしいけど、学んだ少しのForthはすごく興味深かったし、もっと進めたかったんだ。多分、スタックシステムがマルチコアや永続データ構造を扱えるとは思えなかったから、希望を失ったのかも。そういうのは他のニッチな言語で使うようになったことだし。あと、いくつかのプロジェクトやライブラリは一人でやっているものが多くて、メンテナンスが止まっているものもある。基本的に古びていて、初心者が長い間理解できるよりもずっと理解がある人たちが作ったものなんだ。Forthを本当に学ぶには、よく勧められる本の一冊を読んで、ファイルを行ごとに読むような簡単なことを学ぶまで、たくさんの忍耐が必要だと思う。

よく言ったね。Forthは素晴らしいし、学ぶ価値があると思うけど、ワークステーションレベルのアプリケーションをForthでプログラムする人はほとんどいないし、君が言うように、組み込み環境でもリソースのレベルが上がってきているから、もうForthを選ぶ理由はほとんどないんだ。それがちょっと悲しいな。Forthは本当に素晴らしいから。

F83の説明は面白そうだね。実際に見る方法や、自分で使う方法はある?

ほとんどの人はForthで本格的なアプリケーションを書いたことがないし、Forthがなければ書けなかったようなものを書くための宗教的な転換体験もしてないよね。誰かが「Fmacs」みたいな大きなシステムのForthのソースコードをアップロードしてくれたら面白いかも。これは、主にForthで書かれたEmacs風のエディタで、埋め込まれた言語としてELISPの代わりにForthを使ってるんだ。そうすれば、速度や可読性(今も毎日大事だよね)、RAMやディスクのメモリ要件などを比較するのが面白くなると思う。(昔はすごく重要だったけど、今はあんまり重要じゃないかも。)ちょっとForthベースのオペレーティングシステムのソースコードを見てみたけど、もちろんあんまり理解できなかった。コードを見ても何が起こってるかは分からないから、スタックで何が起こってるかを想像しないといけないんだよね。

Forthは思考を広げるために学ぶ価値があると思う。自分のインタープリタを作ったり、自分の言語をデザインするための良い出発点にもなるしね。

まだ見たことない人のために言うと、Jones Forthは素晴らしい実装で、リテラルプログラミングスタイルで書かれてるんだ。アセンブリを知らなくても、読む価値は十分にあるよ。[1] https://github.com/nornagon/jonesforth/blob/master/jonesfort...