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

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

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

プログラミング言語 Factor のプログラミングで仕事の疲れを癒す

この数ヶ月は、仕事でも私的なことでも負担の大きな時期で、気分の低空飛行もまた致し方なしという感想。

ようやく気持ちも多少は上向いてきたので、そろそ好きなことをして諸々の疲れを癒す時期かと。

そんなわけで、この数日 Factor をいじっています。

Factor programming language

factorcode.org

ja.wikipedia.org

なぜ Factor かと問われれば、ただの偶然でしかないのですが。 しかし、異なるパラダイムの言語のコードを書くのは、頭の使い慣れていない部分を使うことにが多く、プログラミングを再び新鮮な経験に変えてくれます。

インストール (macOS)

Mac のばあいは Homebrew を使うのが簡単です。

$ brew install factor

インストールするとアプリケーションに Factor が追加されます。

起動するとウィンドウが開きプロンプトが表示されます。 ここで Factor のコードを試すことができます。

アプリケーションをインストールすると、コマンドラインツールも同時にインストールされます。

Factor がインストールされたディレクト/Applications/factor/ の中に factor という名前で格納されています。

$ /Applications/factor/factor
Factor 0.100 x86.64 (2281, heads/master-80a4633f05, Sep 11 2024 14:22:41)
[Clang (GCC Homebrew Clang 18.1.8)] on macos
IN: scratchpad

パスが通っていないと思いますし、因数分解を実行する factor がインストールされていることもあると思いますので、フルパスで指定して実行してください。

Hello world

正確な話を省略すると。 Factor はスタックを使った逆ポーランド記法構文なので、値に対して操作が後ろに置かれます。

"Hello world" print
# コード 動作
1 "Hello world" 文字列をスタックに push する
2 print スタックから値を pop しコンソールに出力する
1 2 + 3 * .
# コード 動作
1 1 値をスタックに push する
2 2 値をスタックに push する
3 + 値を 2 つスタックから pop する、2 つの値を加算する、結果の値をスタックに push する
4 3 値をスタックに push する
5 * 値を 2 つスタックから pop する、2 つの値を乗算する、結果の値をスタックに push する
6 . 値をスタックから pop する、整形してコンソールに出力する(いわゆる pretty print)

条件分岐

制御構造も同じように組み立てます。

と、その前に。 quotation にも触れておきます。

式そのものを値として扱うために quotation というしくみがあります。 Lispquote と同様のしくみです。

Factor では [] で囲みます。

[ 1 2 + ]

値を . で表示すると、そのままの内容が表示されます。

[ 1 2 + ] .
[ 1 2 + ]

評価には call を利用します。

[ 1 2 + ] call .
3

これを踏まえて。

Factor の if は 3 つの値をスタックから pop し、1 つ目の値が真であれば 2 つ目の値を評価し、1 つ目の値が偽であれば 3 つ目の値を評価する、という動作になります。 見てきたように、操作は値の後ろに置かれるため、if は最後に配置されます。

1 2 = [ "EQ" ] [ "NE" ] if print
# コード 動作
1 1 値をスタックに push する
2 2 値をスタックに push する
3 = 値を 2 つスタックから pop する、2 つの値を比較する
等しい場合は t (真)を異なる場合は f (偽)をスタックに push する
4 [ "EQ" ] 値をスタックに push する
5 [ "NE" ] 値をスタックに push する
6 if 値を 3 つスタックから pop する
1 つ目の値が t であれば 2 つ目の値を f であれば 3 つ目の値を評価する
7 print 値をスタックから pop する、その値をコンソールに表示する

ここで [ "EQ" ][ "NE" ] は評価されると "EQ""NE" となり、つまり「文字列 "EQ" をスタックに push する」もしくは「文字列 "NE" をスタックに push する」という動作になります。

関数の呼び出しもスタックを利用します。

そもそもたとえば + という関数(Factor ではこれらを word と呼ぶようです)は、スタックから 2 つの値を取得してその和を push するという動作からも分かるように、

既視感

最初こそ構文につまづくものの、特に if が末尾に配置される条件分岐は今読んでいる部分がどこなのか戸惑ったものの、すぐに慣れてきました。

慣れたというよりも何かを思い出した気分。

どこかでこんなプログラミングをしていたという既視感。

思い出しました。 Z80アセンブラでのプログラミングです。 当時はたしかにスタックを駆使したプログラミングをしていました。

そういうものと把握すると、戸惑う構文だったものが、見慣れた構文に感じてきます。

二つの経験の間には何十年も開きがあるわけですが、こういった「肌感覚」のようなものは消えないものなのだな、と思った次第。

いつか読むはずっと読まない:語り継がれるもの

最初に読んだのが 40 年くらい前の話。 それから何回か読み返してはいるはずですが、それも前世紀。

昨年末、最終巻が国内で刊行され、最近になって重い腰を上げて第一巻から読み返しています。

作中の景色が近年の SF 作品と異なり、書かれた時代背景を反映しているのを感じます。

あらすじは忘れようもないのですが、それでも読み進めるごとにわくわくさせられます。 陳腐な言葉になりますが、この作品の面白さは本当に色褪せません。

生成AIとソフトウェア的愛情

生成AIを使ったプログラミングが百花繚乱の昨今。

かつて何度か記事にしたように、30年40年経っても、プログラミングだけは飽きずに続いています。

blog.emattsan.org blog.emattsan.org

ここにきて、人間とプログラミングの関わり方が大きく変わってきました。

職業プログラマになって十余年。 プロのプログラマにとって、この変化は不可避どころでなく、積極的にこの変化を取り込んで踏み台にしていかなければなりません。

なのですが。

個人的には、なぜか興味が湧いてこない。 よく言われるようにプログラミングの楽しい部分が生成AIに持っていかれてしまうから、という理由も考えてみたのですがどうもそれとも違う。 かっこいいコードを自分の力で書くというのは、確かにプログラミングの楽しみの一つではあるけれど、それが失われるというだけで興味が出ないというのはあまりに短絡的。 かっこいいコードを書くのは、プログラミングの一部でしかないわけですし。

上司ともそんな話をして。 その後、何日か考え続けていたのですが。 一番それっぽい理由を思いつきました。

あぁ、これはうつ病の初期症状だ。

ストレスに強くない身体で、かつて短くない期間抗うつ薬を服用していたこともあります。 強い負担がかからないように日々気を付けて過ごしていますが、この数ヶ月は確かにいろいろと特に私的なところで身体に負担がかかっていました。

と、いうわけで。

生き馬の目を抜くような生成AI界隈で、あまり悠長なことは言っていられないのも事実ですが、身体を壊すのももうこりごりなので、見失わない程度には視界におさめつつ、生成AIの時代のソフトウェア的愛情とはなんなのか、考えを巡らせてみようかと思っています。

近況

このところプログラミングができていない感じがしています。 というか、たぶんできてません。

職業プログラマなのでプログラミングしていないということはないのですが、自分の思うようなプログラミングができていない感じがします。 仕事のプログラミングだからつまらないとか窮屈だっとかそういったことでなく、何かが低調な感じです。

今月は私的に悲しい出来事があったりで気疲れしていることも影響しているかもしれません。

精神力もまた源は肉体なので、身体的にも疲労が嵩んでいるのかもしれません。

…などと思いながら過去のブログ記事を見返していたら 12 年前にも同じようなことを書いていました。

blog.emattsan.org

十二支一回りしても同じことを思っていることに、進歩がないと呆れたような、変わらず自分であることにほっとしたような。

順序を入れ替えるための覚書

今回の話は次のようなことを念頭に置いています。

1の要素を3の位置に移動する

これは順序を決定する情報を各要素が持っているばあい、たとえば次のようなばあい、

その情報を入れ替えることを意味します。

順序情報をもとに並び替えると次のようになります。

Elixir で、Elixir のデータ構造を使うケースと、Ecto (データベース)を使うケースを考えます。

タプルによる実装

まず次のようなタプル表現で考えます。

[{"Alice", 1}, {"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}]

前から後ろへ移動

前から後ろへ移動するばあい、次のような結果になればよいはずです。

[{"Alice", 1}, {"Bob", 4}, {"Charlie", 2}, {"Dave", 3}, {"Erin", 5}]

もしくは

[{"Alice", 1}, {"Charlie", 2}, {"Dave", 3}, {"Bob", 4}, {"Erin", 5}]

まず、Enum.split/2 を使って順序を入れ替える部分だけを切り出します。

users = [{"Alice", 1}, {"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}]
#=> [{"Alice", 1}, {"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}]

{leading, rest} = users |> Enum.split(1)
#=> {[{"Alice", 1}], [{"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}]}

{target, trailing} = rest |> Enum.split(3)
#=> {[{"Bob", 2}, {"Charlie", 3}, {"Dave", 4}], [{"Erin", 5}]}

これで target の範囲で順序情報を入れ替えればよいはずです。

次のコードでは Enum.chunk_every/3 を使って隣り合う要素の組みを作り、順序情報を隣に受け渡します。 末尾の要素の順序情報は、先頭の要素に受け渡します。

[{head_name, _} | _] = target
target
|> Enum.chunk_every(2, 1)
|> Enum.map(fn
  [{_, order}, {name, _}] -> {name, order}
  [{_, order}] -> {head_name, order}
end)
#=> [{"Charlie", 2}, {"Dave", 3}, {"Bob", 4}]

この結果の前後に leadingtrailing を連結すれば完了です。

後ろから前へ移動

後ろから前へ移動するばあい、分割までは同じですが、前から後ろへ移動する場合と逆の方向で順序情報を受け渡します。 先頭の要素の順序情報は、末尾の要素に受け渡します。

[{_, head_order} | _] = target
target
|> Enum.chunk_every(2, 1)
|> Enum.map(fn
  [{name, _}, {_, order}] -> {name, order}
  [{name, _}] -> {name, head_order}
end)
#=> [{"Bob", 3}, {"Charlie", 4}, {"Dave", 2}]

補足 Enum.split/2 のふるまいについて

Enum.split/2 は、分割する位置が長さよりも大きいばあいには、常に全体と空リストに分割されます。 このため指定される位置が長さを超えていてもそのまま適用するできます。

[1, 2, 3, 4, 5] |> Enum.split(4) #=> {[1, 2, 3, 4], [5]}
[1, 2, 3, 4, 5] |> Enum.split(5) #=> {[1, 2, 3, 4, 5], []}
[1, 2, 3, 4, 5] |> Enum.split(6) #=> {[1, 2, 3, 4, 5], []}
[1, 2, 3, 4, 5] |> Enum.split(7) #=> {[1, 2, 3, 4, 5], []}

負数のばあいは、末尾から分割されます。 位置が長さよりも大きいばあいと同じように、範囲外の場合に常に空リストと全体に分割されるようにするには、負数が与えられたときに 0 として扱うように調整する必要があります。

[1, 2, 3, 4, 5] |> Enum.split(1)  #=> {[1], [2, 3, 4, 5]}
[1, 2, 3, 4, 5] |> Enum.split(0)  #=> {[], [1, 2, 3, 4, 5]}
[1, 2, 3, 4, 5] |> Enum.split(-1) #=> {[1, 2, 3, 4], [5]}
[1, 2, 3, 4, 5] |> Enum.split(-2) #=> {[1, 2, 3], [4, 5]}

Ecto による実装

次に Ecto で次のようなスキーマのデータを持つばあいを考えます。

defmodule User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    has_one :order, Order
  end
end
defmodule Order do
  use Ecto.Schema

  schema "orders" do
    field :priority, :integer
    belongs_to :user, User
  end
end

またデータベースには、すでに次のようにデータが格納されているとします。

from(
  user in User,
  join: order in Order,
  on: user.id == order.user_id,
  order_by: order.priority,
  select: {user.name, order.priority}
)
|> MyApp.Repo.all
#=> [{"Alice", 1}, {"Dave", 2}, {"Bob", 3}, {"Charlie", 4}, {"Erin", 5}]

前から後ろへ移動する

users =
  from(
    user in User,
    join: order in Order,
    on: user.id == order.user_id,
    order_by: order.priority
  )
  |> preload(:order)
  |> MyApp.Repo.all()

{_leading, rest} = users |> Enum.split(1)
{target, _trailing} = rest |> Enum.split(3)

[head | _] = target

target
|> Enum.map(& &1.order)
|> Enum.chunk_every(2, 1)
|> Enum.map(fn
  [%{priority: priority}, order] ->
    Ecto.Changeset.change(order, %{priority: priority})

  [order] ->
    Ecto.Changeset.change(head.order, %{priority: order.priority})
end)
|> Enum.map(&Repo.update/1)

本来は複数のレコードの更新を一体の操作にするためにトランザクション管理が必要ですが、今回は順序操作が主題なので横着しています。 詳しくはドキュメントを参照してください。

後ろから前へ移動する

users =
  from(
    user in User,
    join: order in Order,
    on: user.id == order.user_id,
    order_by: order.priority
  )
  |> preload(:order)
  |> Repo.all()

{_leading, rest} = users |> Enum.split(1)
{target, _trailing} = rest |> Enum.split(3)

[head | _] = target

target
|> Enum.map(& &1.order)
|> Enum.chunk_every(2, 1)
|> Enum.map(fn
  [order, %{priority: priority}] ->
    Ecto.Changeset.change(order, %{priority: priority})

  [order] ->
    Ecto.Changeset.change(order, %{priority: head.order.priority})
end)
|> Enum.map(&Repo.update/1)

必要な範囲のデータを取得する

上の例ではタプルで操作したときと同じように、すでに全体のデータを取得する前提で操作を組み立てました。 全体のデータは不要であれば、offsetlimit を利用して最初から操作する範囲のみ取得して済ませることができます。

[head | _] = target = 
  from(
    user in User,
    join: order in Order,
    on: user.id == order.user_id,
    order_by: order.priority,
    offset: 1,
    limit: 3
  )
  |> preload(:order)
  |> Repo.all()

Elixir の構造体を JSON データからデシリアライズする

過去のブログ記事を検索してみても、たびたびシリアライズやデシリアライズと格闘していることが伺えます。

見れば 2018 年にも Poison パッケージを使った Elixir の構造体のシリアライズ/デシリアライズを試みていました。

主流は Posion から Jason になり、Elixir 1.18 からは JSON モジュールが標準装備されました。

Posion がデシリアライズしたデータを指定した構造体に格納する仕組みを用意しているのに対し、JSON モジュールはシンプルなエンコードとデコードを提供するのみ。

シンプルな JSON モジュールが装備された今、それを使って構造体のシリアライズとデシリアライズをどう実装するか、考えてみました。

構造体はマップである

知られているように、Elixir の構造体はあるルールに従ったマップです。

defmodule MyApp.Data do
  defstruct [:name, :value]
end
iex> %MyApp.Data{} |> Map.keys()
[:name, :value, :__struct__]

iex> %MyApp.Data{} |> Map.values()
[nil, nil, MyApp.Data]

iex> %MyApp.Data{} |> Map.to_list()
[name: nil, value: nil, __struct__: MyApp.Data]

: __struct__ というキーにモジュール名を格納しているにすぎません。 なので :__struct__ を指定してマップを作ると構造体の値になります。

iex> %{__struct__: MyApp.Data, name: "Foo", value: 42}
%MyApp.Data{name: "Foo", value: 42}

マップは JSON モジュールで encode / decode が可能です。

iex> %{name: "Foo", value: 42} |> JSON.encode!()
"{\"name\":\"Foo\",\"value\":42}"

iex> %{name: "Foo", value: 42} |> JSON.encode!() |> JSON.decode!()
%{"name" => "Foo", "value" => 42}

しかし構造体はそのままではエラーになってしまいます。

iex> %MyApp.Data{} |> JSON.encode!()
** (Protocol.UndefinedError) protocol JSON.Encoder not implemented for type MyApp.Data (a struct), the protocol must be explicitly implemented.

JSON モジュールには JSON.Encoder というプロトコルが用意されています。

defmodule MyApp.Data do
  @derive JSON.Encoder
  defstruct [:name, :value]
end

これを利用すれば構造体の値も encode できるようになります。

しかし、構造体としての情報は落ちてしまいます。

iex> %MyApp.Data{} |> JSON.encode!()
"{\"name\":null,\"value\":null}"

iex> %MyApp.Data{} |> JSON.encode!() |> JSON.decode!()
%{"name" => nil, "value" => nil}

なんとかずるができないか? と考えた結果、キーを文字列にしてしまえば普通のマップの扱いになるはず、ということに思い至りました。 すべてのキーを文字列にする必要はなく、:__struct__ だけ文字列になっていればいいはずです。

%MyApp.Data{name: "Foo", value: 42} |> Map.to_list() |> Map.new(fn
  {:__struct__, module} -> {"__struct__", module}
  kv -> kv
end)
#=> %{:name => "Foo", :value => 42, "__struct__" => MyApp.Data}

うまく行った気配です。

Map.new/2 も構造体の値を受け付けないので、Map.to_list/1 でタプルのリストにしてから変換しています。

このまま JSON.encode!/1 に投入します。

%MyApp.Data{name: "Foo", value: 42} |> Map.to_list() |> Map.new(fn
  {:__struct__, module} -> {"__struct__", module}
  kv -> kv
end) |> JSON.encode!()
#=> "{\"name\":\"Foo\",\"value\":42,\"__struct__\":\"Elixir.MyApp.Data\"}"

モジュール名の実態はアトムで、大文字から始まるアトムには内部では Elixir. という接頭辞が付いています。 これを文字列に変換したため "Elixir.MyApp.Data" という文字列で出力されました。

これを構造体の値に戻してみます。

"{\"name\":\"Foo\",\"value\":42,\"__struct__\":\"Elixir.MyApp.Data\"}" |> JSON.decode!()
#=> %{"__struct__" => "Elixir.MyApp.Data", "name" => "Foo", "value" => 42}

単純に decode するだけでは文字列をキーとしたマップが出力されるのみです。 キーとモジュール名をアトムに変換します。

"{\"name\":\"Foo\",\"value\":42,\"__struct__\":\"Elixir.MyApp.Data\"}" |> JSON.decode!() |> Map.new(fn
  {"__struct__", module} -> {:__struct__, String.to_existing_atom(module)}
  {key, value} -> {String.to_existing_atom(key), value}
end)
%MyApp.Data{name: "Foo", value: 42}

構造体の値に戻ってきました。

詳しく見れば、アトムは encode で文字列に変換され decode しても文字列のままであるため、このままでは完全には戻りません。 もうひと工夫が必要です。

そこまでするのであれば、このようなずるでなくきちんと定義した形式へ変換するのが正解のようです。 それでも、Elixir のデータ構造のシンプルさはとても扱いやすさを生んでいるように感じられます。

「Specification は述語である」という話

勉強不足を晒すことになりますが。 仕事で "Specification" なるオブジェクトが出てきたときに、しばらくは漠然としたイメージしか持てませんでした。

当面は直接関わることがなかったのでそのままにしていたのですが、原典に当たったことですっきりしました。

仕様とは、あるオブジェクトが何らかの基準を満たしているかどうかを判定する述語である。

www.shoeisha.co.jp

なるほど、述語のオブジェクトということのようです。

その応用については Martin Fowler のサイトで公開されている論文に詳しく書かれています。

そこで、この論文に出てくる CompositeSpecificationRuby で書いてみました。

module CompositeSpecification
  class AndSpecification
    include CompositeSpecification

    def initialize(left, right)
      @left = left
      @right = right
    end

    def satisfied_by?(object)
      @left.satisfied_by?(object) && @right.satisfied_by?(object)
    end
  end

  class OrSpecfication
    include CompositeSpecification

    def initialize(left, right)
      @left = left
      @right = right
    end

    def satisfied_by?(object)
      @left.satisfied_by?(object) || @right.satisfied_by?(object)
    end
  end

  class NotSpecification
    include CompositeSpecification

    def initialize(specification)
      @specification = specification
    end

    def satisfied_by?(object)
      !@specification.satisfied_by?(object)
    end
  end

  def and(another)
    AndSpecification.new(self, another)
  end

  def or(another)
    OrSpecfication.new(self, another)
  end

  def !
    NotSpecification.new(self)
  end
end

使い方の例です。

まず、 1 から 30 の数値のうち、仕様を満たす値のみを出力する関数を用意します。

def satisfy(spec)
  puts (1..30).select { |i| spec.satisfied_by?(i) }.join(' ')
end

次に 3 の倍数であることを FizzSpecification 、5 の倍数であることを BuzzSpecification と定義します。

require_relative './composite_specification'

class FizzSpecification
  include CompositeSpecification

  def satisfied_by?(n)
    (n % 3).zero?
  end
end

class BuzzSpecification
  include CompositeSpecification

  def satisfied_by?(n)
    (n % 5).zero?
  end
end

それぞれの Specification オブジェクトを適用してみます。

fizz_spec = FizzSpecification.new
satisfy(fizz_spec)
#=> 3 6 9 12 15 18 21 24 27 30

buzz_spec = BuzzSpecification.new
satisfy(buzz_spec)
#=> 5 10 15 20 25 30

CompositeSpecification を include したこれらの Specification は合成することができます。

FizzSpecificationBuzzSpecification を合成して、新しい Specification オブジェクトを作ります。

fizz_buzz_spec = fizz_spec.and(buzz_spec)
satisfy(fizz_buzz_spec)
#=> 15 30

どんどん合成できます。

satisfy(fizz_spec.or(buzz_spec))
#=> 3 5 6 9 10 12 15 18 20 21 24 25 27 30

satisfy(!fizz_spec.or(buzz_spec))
#=> 1 2 4 7 8 11 13 14 16 17 19 22 23 26 28 29

satisfy((!fizz_spec).and(!buzz_spec))
#=> 1 2 4 7 8 11 13 14 16 17 19 22 23 26 28 29

「エリック・エヴァンスのドメイン駆動設計」にはこうも書かれています。

論理プログラミングは、 「述語」と呼ばれる、独立した結合可能なルールオブジェクトの概念を提供するが、この概念をオブジェクトで完全に実装するのは面倒である。

なるほど。 それでは論理プログラミングなら簡単なのかも。

試してみます。

そんなわけで。 久々の Prolog プログラミングです。

fizz/1buzz/1 を定義して specification.prolog というファイル名で保存します。

fizz(N) :- Rem is N mod 3, Rem == 0.
buzz(N) :- Rem is N mod 5, Rem == 0.

REPL を起動します。 使うのはいつものように GNU Prolog です。

$ gprolog

プログラムを読み込みます。

| ?- [specification].       

1から30までの間で fizz/1 を満たす N をすべて見つけます。

| ?- findall(N, (between(1, 30, N), fizz(N)), Ns).
Ns = [3,6,9,12,15,18,21,24,27,30]

1から30までの間で buzz/1 を満たす N をすべて見つけます。

| ?- findall(N, (between(1, 30, N), buzz(N)), Ns).
Ns = [5,10,15,20,25,30]

Prolog ではコンマ( (',')/2 )は論理積を、セミコロン( (;)/2 )は論理和を表す演算子です。

ちなみに否定を表す演算子(\+)/1 です。

ですので fizz_buzz/1 はこのように定義できます。

fizz_buzz(N) :- fizz(N), buzz(N).
| ?- findall(N, (between(1, 30, N), fizz_buzz(N)), Ns).
Ns = [15,30]

他にも:

fizz_or_buzz(N) :- fizz(N); buzz(N).
| ?- findall(N, (between(1, 30, N), fizz_or_buzz(N)), Ns).
Ns = [3,5,6,9,10,12,15,15,18,20,21,24,25,27,30,30]
not_fizz_or_buzz(N) :- \+fizz_or_buzz(N).
| ?- findall(N, (between(1, 30, N), not_fizz_or_buzz(N)), Ns).
Ns = [1,2,4,7,8,11,13,14,16,17,19,22,23,26,28,29]

確かに合成は簡単、…というかこれは論理プログラミングそのものですね、確かに。

合成の部分に関しては。 例えば次のように演算をオブジェクトして遅延評価したいときに顔をだす構造。

module Calculable
  class Add
    include Calculable

    def initialize(left, right)
      @left = left
      @right = right
    end

    def eval
      @left.eval + @right.eval
    end
  end

  class Sub
    include Calculable

    def initialize(left, right)
      @left = left
      @right = right
    end

    def eval
      @left.eval - @right.eval
    end
  end
  
  def add(another)
    Add.new(self, another)
  end

  def sub(another)
    Sub.new(self, another)
  end
end

class Int
  include Calculable

  def initialize(n)
    @n = n
  end

  def eval
    @n
  end
end

three = Int.new(3)
two = Int.new(2)

puts (three.add(two)).sub(three.sub(two)).eval
#=> 4

これは Ruby on RailsActiveRecord のクエリメソッドで、普段からお世話になっている仕組みですね。

述語の詳細をオブジェクトで表現しなければならい状況に遭遇しないと、なぜこれが有意義なのかわかりにくいですが、よくよく調べてみると確かにそういった状況があると納得し、その解決方法の一つが Specification なのだとようやく腑に落ちたのでした。