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

イージー・フォース (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を学び始めたばかりの初心者には、スタックを使うよりも変数を使う方がいいと思う。スタックを使おうとしすぎてトラブルになるより、変数を使おうとしすぎてトラブルになる方が、初心者にはずっと簡単だからね。

Hacker Newsで議論の続きを見る