あまり時間をさけていませんが、細々と Factor をいじっています。
Hello, World! の次の定番、Fizz Buzz をやってみました。
前回のおさらい
「Factor とは何?」という話は、前回の記事に書きましたのでこちらを参照していただけたら。
今回も Mac で Homebrew を使ってインストールした環境を利用しています。
ファイルを実行する場合は、ターミナルから次のように入力してください。
ここで fizz_buzz.factor は今回作成する Factor プログラムを保存したファイルです。
/Applications/factor/factor fizz_buzz.factor
ファイル名を指定せず実行すると REPL が起動します。
/Applications/factor/factor
REPL が起動すると次のようなプロンプトが表示されます。
IN: scratchpad
以下、このプロンプトから始まっているコードは REPL で入力していると思ってください。
if then FizzBuzz else if then Fizz else if then Buzz else number
もっとも愚直な if を使った FizzBuzz です。
「15 で割り切れば "FizzBuzz" 、そうでなく 3 で割り切れば "Fizz" 、そうでなく 5 で割り切れば "Buzz" 、そうでなければ数字」をそのままコードにします。
前回の記事でも書いたように、Factor は逆ポーランド記法なので if も後置になることに注意してください。
rem, zero?, t, f
割り切るか否かの判定には、余りを計算する rem とゼロを判定する zero? を利用します。
Factor の真偽値は t と f で表現されます。
これらを組み合わせた 3 rem zero? は、スタックにある値が 3 で割り切れれば t (true) を、割り切れなければ f (false) をスタックに積みます。
IN: scratchpad 10
--- Data stack:
10
IN: scratchpad 3 rem zero?
--- Data stack:
f
dup
このままだと元の値は「消費」されてしまい消えてしまいます。
判定後にも値を利用したいので、先に dup で値を複製します。
IN: scratchpad 10
--- Data stack:
10
IN: scratchpad dup 3 rem zero?
--- Data stack:
10
f
これらを踏まえて実装します。
実装
USING: io kernel math math.parser ranges sequences ;
IN: emattsan
: fizz_buzz ( n -- m )
dup 15 rem zero?
[
drop "FizzBuzz"
] [
dup 3 rem zero?
[
drop "Fizz"
] [
dup 5 rem zero?
[
drop "Buzz"
] [
number>string
] if
] if
] if ;
: main ( -- )
30 [1..b] [ fizz_buzz print ] each ;
MAIN: main
新しい言語要素が出てきたので個別に解説します。
USING:, IN:, MAIN:
USING: を使って追加で参照するボキャブラリ(モジュールやパッケージなどに相当するもの)を指定します。
また IN: は現在のプログラムのボキャブラリを定義します。
MAIN: はエントリポイントを指定します。
ここではエントリポイントを main という名前にしていますが、MAIN: で指定したものがエントリポイントになるので、名前は自由につけることができます。
[1..b], each
[1..b] は 1 から指定した値(ここでは 30 )までの範囲のシーケンスを作る関数です。
他の言語ではあまり見かけない見た目をしていますが [1..b] が関数の名前です。
Factor では語の区切りは空白文字なので、空白文字を含まない任意の文字列を名前として利用できるようです。
また作られるシーケンスも Factor で定義されるデータ型で、「1 から 30 までのシーケンス」という 1 つの値です。
そして each は、シーケンスが表現する値のひとつひとつに対して操作を適用する関数です。
つまり 30 [1..b] [ fizz_buzz print ] each は、「1 から 30 までの範囲のそれぞれの値に fizz_buzz print を適用」する動作になります。
drop
一度値を評価するとその値を「消費」してしまうので、値を評価して合致しなかったばあいに後続の判定に値を受け渡すために複製をしておきました。
しかし合致したばあいには今度はその値が不要になります。
スタックに残った不要な値を「捨てる」には drop を使います。
IN: scratchpad 42
--- Data stack:
42
IN: scratchpad 43
--- Data stack:
42
43
IN: scratchpad drop
--- Data stack:
42
number>string
"Fizz" や "Buzz" や "FizzBuzz" といった文字列に変換されなかった数値を number>string で文字列に変換します。
これもまた他の言語ではあまり見かけない見た目ですが、number から string へという操作がそのままが名前なった関数です。
IN: scratchpad 42
--- Data stack:
42
IN: scratchpad number>string
--- Data stack:
"42"
print
print は文字列を改行付きでコンソールに出力します。
他の言語では
入れ子がどんどん深くなっていますが、これは他の言語でも同じです。
Elixir でも同じことが起こります。
defmodule EMattsan do
def fizz_buzz(n) do
if rem(n, 15) == 0 do
"FizzBuzz"
else
if rem(n, 3) == 0 do
"Fizz"
else
if rem(n, 5) == 0 do
"Buzz"
else
Integer.to_string(n)
end
end
end
end
end
C++ でも状況は同じです。
const std::string fizz_buzz(int n) {
if (n % 15 == 0) {
return "FizzBuzz";
} else {
if (n % 3 == 0) {
return "Fizz";
} else {
if (n % 5 == 0) {
return "Buzz";
} else {
return (std::ostringstream() << n).str();
}
}
}
}
ただ C++ では単文の場合は { } が必要ないため else if と書くことができ、最初の if と同じ深さに書くのが一般的です。
const std::string fizz_buzz(int n) {
if (n % 15 == 0) {
return "FizzBuzz";
} else if (n % 3 == 0) {
return "Fizz";
} else if (n % 5 == 0) {
return "Buzz";
} else {
return (std::ostringstream() << n).str();
}
}
Ruby ではこのような書き方のために、言語のしくみとして elsif が用意されています。
def fizz_buzz(n)
if (n % 15).zero?
"FizzBuzz"
elsif (n % 3).zero?
"Fizz"
elsif (n % 5).zero?
"Buzz"
else
n.to_s
end
end
多くの言語で、一回の評価で複数の分岐が存在する場合のしくみが用意されています。
Factor でも case が用意されています。
case
case を使うことで複数の分岐を一度に定義することができます。
記号が多いので多少の読みにくさを感じますが、構造は難しくありません。
case はスタックから 2 つの値を取り出します。
1 つめの値は判定される値です。
2 つめの値は「『判定する値と評価する操作の 2 要素の配列』の配列」です。
判定する値が 1 つめの値と一致する操作を評価します。
次のコードは、判定される値が 1 であれば [ "One" ] が評価され "One" をスタックに積む、値が 2 であれば [ "Two" ] が評価され "Two" をスタックに積む、という動作をします。
IN: scratchpad 1 { { 1 [ "One" ] } { 2 [ "Two" ] } { 3 [ "Three" ] } } case
--- Data stack:
"One"
IN: scratchpad 2 { { 1 [ "One" ] } { 2 [ "Two" ] } { 3 [ "Three" ] } } case
--- Data stack:
"Two"
要は次のような構造です。
{
{ 値1 [ 操作1 ] }
{ 値2 [ 操作2 ] }
{ 値3 [ 操作3 ] }
...以下略
} case
ここで判定される値は 1 つの値になっている必要があるので、「3 で割り切れるか?」と「5 で割り切れるか?」が 1 つの値として判定できるように表現されていなければなりません。
bi
そのためにまず 1 つの値に対して 2 つの評価をする bi を利用します。
bi は、スタックから値を 1 つ取り出し、2 つの操作を適用してそれぞれスタックに積む、という動作をします。
例えば [ 2 * ] [ 3 * ] bi は 2 を掛ける操作と 3 を掛ける操作を適用してそれぞれの結果をスタックに積みます。
IN: scratchpad 10
--- Data stack:
10
IN: scratchpad [ 2 * ] [ 3 * ] bi
--- Data stack:
20
30
これを利用し [ 3 rem zero? ] [ 5 rem zero? ] bi とすれば、1 つの値に対して「3 で割り切れるか?」と「5 で割り切れるか?」を同時に評価してそれぞれの結果をスタックに積むことができます。
2array
このままでは 2 つの値なので、これを 1 つの値に変換します。
2array はスタックから値を 2 つ取り出し、その 2 つの要素からなる配列をスタックに積みます。
同じようなことを繰り返しますが 2array が関数の名前です。
IN: scratchpad 10
--- Data stack:
10
IN: scratchpad 20
--- Data stack:
10
20
IN: scratchpad 2array
--- Data stack:
{ 10 20 }
これらを組み合わせることで、 15 で割り切れれば { t t } 、3 で割り切れて 5 で割り切れなければ { t f } 、3 で割り切れず 5 で割り切れれば { f t } 、それ以外ではれば { f f } と判定することができるようになりました。
IN: scratchpad 10
--- Data stack:
10
IN: scratchpad [ 3 rem zero? ] [ 5 rem zero? ] bi 2array
--- Data stack:
{ f t }
実装
USING: arrays combinators io kernel math math.parser ranges
sequences ;
IN: emattsan
: fizz_buzz ( n -- m )
dup [ 3 rem zero? ] [ 5 rem zero? ] bi 2array
{
{ { t t } [ drop "FizzBuzz" ] }
{ { t f } [ drop "Fizz" ] }
{ { f t } [ drop "Buzz" ] }
{ { f f } [ number>string ] }
} case ;
: main ( -- )
30 [1..b] [ fizz_buzz print ] each ;
MAIN: main
他の言語では
Elixir の case/2 や Ruby の case 、C++ の switch など、複数の分岐を扱う構文も多くの言語で見られます。
defmodule FizzBuzz do
def fizz_buzz(n) do
case {rem(n, 3) == 0, rem(n, 5) == 0} do
{true, true} -> "FizzBuzz"
{true, false} -> "Fizz"
{false, true} -> "Buzz"
{false, false} -> Integer.to_string(n)
end
end
end
def fizz_buzz(n)
case [n.remainder(3).zero?, n.remainder(5).zero?]
when [true, true] then "FizzBuzz"
when [true, false] then "Fizz"
when [false, true] then "Buzz"
when [false, false] then n.to_s
end
end
const std::string fizz_buzz(int n) {
switch (((n % 3) ? 0b00 : 0b01) + ((n % 5) ? 0b00 : 0b10)) {
case 0b11: return "FizzBuzz";
case 0b10: return "Fizz";
case 0b01: return "Buzz";
default: return (std::ostringstream() << n).str();
};
}
C++ の switch は整数値しか受け付けないので、ここでは少々トリッキーなことをしていますが、方法としては同じです。
もっとも case が使う 2 つめの値は、文字通り「値」でしかないため、実際には次のようなテーブルで操作を選択して適用しているととらえるのが正確かもしれません。
defmodule FizzBuzz do
def fizz_buzz(n) do
table = %{
{true, true} => fn -> "FizzBuzz" end,
{true, false} => fn -> "Fizz" end,
{false, true} => fn -> "Buzz" end,
{false, false} => &Integer.to_string/1
}
f = table[{rem(n, 3) == 0, rem(n, 5) == 0}]
f.(n)
end
end
def fizz_buzz(n)
table = {
[true, true] => ->(_) { "FizzBuzz" },
[true, false] => ->(_) { "Fizz" },
[false, true] => ->(_) { "Buzz" },
[false, false] => ->(n) { n.to_s }
}
func = table[[n.remainder(3).zero?, n.remainder(5).zero?]]
func.(n)
end
ここまで書いたコードをふりかえってみると。
知っている言語で同じように実装できていますが、これは知っている言語のコードを Factor で焼き直しただけともいえます。
特に dup と drop の多用が気になります。
値を評価するには、その値をスタックから取り出さなければならず、評価した後にもその値を使いたいならばスタックに残しておく必要がある。このため値の複製と廃棄を繰り返しています。
自分が他の言語で知っている手法を適用した結果、その言語に適さない書き方をしている感じが漂っています。
C 言語のような書き方を Ruby でやってしまうとか、そんな感じ。
もし複製と廃棄の繰り返しが避けられないならば、dup や drop を書かなくてもよいようなしくみや操作が提供されていると予想でき、まだそれらの掘り起こしができていないのだろうと推測します。
この記事を書いている途中にも 1check という操作を見つけました。
これは、たとえば [ 5 rem zero? ] 1check と書くことで dup 5 rem zero? と同じ結果をえることができます。
IN: scratchpad 10
--- Data stack:
10
IN: scratchpad dup 5 rem zero?
--- Data stack:
10
t
IN: scratchpad 10
--- Data stack:
10
IN: scratchpad [ 5 rem zero? ] 1check
--- Data stack:
10
t
記述は長くなりますが、dup を使ったコードよりも操作の意図は見えやすくなる気がします。
他にも、複数の引数を受け取ったとき、Ruby や Elixir ではそれらの引数を自由に参照できますが、Factor ではスタックの先頭の値しか参照できないため、実装ではその順序を考慮しなければなりません。
Factor らしいコードを書くには、これらを飛び越える必要がありそうです。
いつか読むはきっと読まない:概念を覆す
IC について学び、以前仕事でも関わったことがあります。
なので電子レベルで IC がどのように動作しているかは知ってはいます。
とはいえその原理を理解しているかと、怪しい限り。