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

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

Pascalの試練場

"Wizardry: Proving Grounds of the Mad Overlord" *1 のコードを GitHub に見つけてしまいました。

github.com

Apple IIPascal で書かれていたという話は昔から聞いていて、ときおり思い出したように検索したりしていたのですが、とうとうヒットしてしまいました。

…とはいえ。 これが本当に当時のコードなのか確証はなく。

ただ少なくともコードの書き方から、当時使われていた UCSD Pascal のコードであることは間違いないようです。

また main ブランチの最新の状態は、デバッグされ多少改変されています。 GitHub のコミットを追いかけてみると、コミットメッセージが "original files" となっているこのコミットで最初のコードが追加されたようです。

GitHub - snafaru/Wizardry.Code at 67b86417e6824b1f1cddc4790a15cae704d6d799

読んでみると、当時のプログラミングが垣間見えて、興味深いものがあります。

TILTOWAIT!

例えば、あの有名な "TILTOWAIT" ですが、実はコード中に表記の揺れが認められます。

        TILTOWAI = 11157;
                7:  SPELLCAS := TILTOWAI;
          IF SPELL = TILTOWAIT THEN

それぞれの名前の定数が定義されているのかとも思いましたが、そのような定義も見当たりません。

ふと思い出したのが、識別子の名前として認識される最大長の存在。

以前購入した Borland C++ のマニュアルに識別子として認識される最大長が決まっていて、その長さを超えた分は区別されないという仕様がありました。

ネットで UCSD Pascal のマニュアルを探して識別子の項目を読んでみると。

Identifiers are character strings starting with an alpha character. Other characters must be alphanumeric or the ASCII underline ('_'). Only the first 8 characters are meaningful to the assembler even though more may be entered.

(識別子は、英字で始まる文字列です。その他の文字は英数字またはASCIIアンダーライン('_')でなければなりません。アセンブラにとって意味を持つのは最初の8文字のみですが、それ以上の文字を入力することもできます。)

つまり。 TILTOWAIT と書かれた名前のうち、識別されるのは先頭の 8 文字 TILTOWAI までで、ソースコードTILTOWAIT と記述してもコンパイル上は TILTOWAI として扱われてしまいます。

コンパイル時のメモリ消費などを抑えるために、識別子を短くする傾向にあったのは覚えていましたが、処理系の制限は完全に忘れていました。

INCLUDE FILE MECHANISM

初期の Pascal はモジュールの機構がなく、分割コンパイルができませんでした。 すべてのコードを一つのプログラムとして書く必要がありました。

とはいえ、大きなファイルを扱うのが大変なのは今も昔も変わりません。 むしろ、テキストエディタ等の性能を考えると大きなファイルを扱うのは一層大変だったと思います。

計測してみると、一番大きなファイルでもサイズは 30,224 バイト。 もしかすると 32,767 (7FFFh) バイトあたりに壁があったのかもしれません。

プログラムは分割できない。 ファイルは大きくできない。 そんな制限の中でプログラミングするにはどうしたらよいか。

そこで登場するのが、別のファイルを丸々取り込んでしまう INCLUDE FILE MECHANISM です。

The syntax for instructing the compiler to include another source file into the compilation is as follows:

(*$IFILENAME*)

考え方は C や C++#include と同じです。 通常はヘッダファイルを指定しますが、制限はあるわけでなく、他のソースファイルを丸ごと include することは可能です。

この機構を使って、別ファイルに分割したコードを読み込み一つのプログラムを組み立てています。

言語仕様でサポートされていない仕組みを、処理系の機構でカバーしている例です。

Wiz1A.DSK/WIZ.TEXT.txt:L444

(*$I WIZ1A:WIZ2.TEXT *)

ところが。 1979 年当時の UCSD Pascal のマニュアルを読んでみると、その後の Pascal に装備された unit というモジュール化のための仕組みが拡張仕様として記載されていました。

unit の仕組みを利用せず、include の仕組みを利用したのは、当時まだ新しかった新しい仕様を単に避けただけなのか、他の処理系を利用することも考慮されていたためなのか、残されたコードだけから推しはかることは難しそうです。

古いコードを読む意義

かつて Delphi をメインにプログラミングをしていたこともあり、古い Pascal のコードを面白く読むことができました。

とはいえ。 当時の環境を掘り起こす考古学的な面白さはありますが、技術的には今後役に立つことはまずないと思います。

それでも、かつてプレイした(実際には移植版なのでこのプログラムで動作していたわけではないものの)ゲームのソースコードを読めるとなると、心踊るものがあります。 この辺の感覚が、いまだに飽きずにプログラミングをしている理由なのかもしれません。

いつか読むはずっと読まない:完結篇

以前、結城先生のサイトで LaTexソースコードとともに公開されていた頃から読んでいた数学ガール。 とうとう完結です。

…。 この一つ前の巻を読んでいないような気がしてきた。

*1:Wizardry の第一作。邦題「ウィザードリィ 狂王の試練場」

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 のデータ構造のシンプルさはとても扱いやすさを生んでいるように感じられます。