エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Factor へ、パラダイムを飛び移る

あまり時間をさけていませんが、細々と 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 の真偽値は tf で表現されます。

これらを組み合わせた 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

case FizzBuzz Fizz Buzz

多くの言語で、一回の評価で複数の分岐が存在する場合のしくみが用意されています。

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/2RubycaseC++switch など、複数の分岐を扱う構文も多くの言語で見られます。

# Elixir
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
# Ruby
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
// C++
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 つめの値は、文字通り「値」でしかないため、実際には次のようなテーブルで操作を選択して適用しているととらえるのが正確かもしれません。

# Elixir
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
# Ruby
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 で焼き直しただけともいえます。

特に dupdrop の多用が気になります。 値を評価するには、その値をスタックから取り出さなければならず、評価した後にもその値を使いたいならばスタックに残しておく必要がある。このため値の複製と廃棄を繰り返しています。

自分が他の言語で知っている手法を適用した結果、その言語に適さない書き方をしている感じが漂っています。 C 言語のような書き方を Ruby でやってしまうとか、そんな感じ。

もし複製と廃棄の繰り返しが避けられないならば、dupdrop を書かなくてもよいようなしくみや操作が提供されていると予想でき、まだそれらの掘り起こしができていないのだろうと推測します。

この記事を書いている途中にも 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 がどのように動作しているかは知ってはいます。 とはいえその原理を理解しているかと、怪しい限り。