他の目的で特別なツールを使用すると、専門家から否定的なフィードバックが生じることがよくあります。ただし、意味のない興味深いタスクを解決すると、横方向の思考が訓練され、適切な解決策を探すためにさまざまな観点からツールを探索できます。
そしてさらに。正直に言うと、SQLを本来の目的で使用することは、常に退屈なことです。Coddによる同じ記事から始めて、すべての教科書にどのような例が示されているか覚えていますか?サプライヤーと部品、従業員と部門...喜びはどこにあり、楽しみはどこにありますか?私にとって、インスピレーションの源の1つは、手続き型ソリューションと宣言型ソリューションを比較することです。ジョン・コンウェイ
の生涯とは何かを説明しないでください。 -私はと言うことができる、それが判明 -のセルオートマトン使って生活を、あなたはユニバーサルチューリングマシンを構築することができます。これは壮大な事実のように私には思えます。
では、1つのSQLステートメントでゲームLifeを実装することは可能ですか?
さて、始めましょう。
私たちのフィールドは、生きている細胞の座標を含むテーブルであり、あなたが激怒しているように、2次元の配列ではありません。このビューはSQLにとってより自然であり、コードを簡素化し、フィールドの境界について考える必要がなくなります。さらに、計算の速度(ただし、ここではほとんど心配しません)は、フィールドのサイズではなく、生きているセルの数にのみ依存します。
マトリックスといえば
, :
, , — . , A(L×M) B(M×N) (L×N), ci,j = ∑k = 1...M ai,k × bk,j.
i, j, k. SQL- :
. . : . . .
, , , . .
CREATE TABLE matrix (
rw integer,
cl integer,
val float
);
, , — . , A(L×M) B(M×N) (L×N), ci,j = ∑k = 1...M ai,k × bk,j.
i, j, k. SQL- :
SELECT a.rw, b.cl, sum(a.val * b.val)
FROM a
JOIN b ON a.cl = b.rw
GROUP BY a.rw, b.cl;
. . : . . .
, , , . .
したがって、フィールド:
CREATE TABLE cells(
x integer,
y integer
);
INSERT INTO cells VALUES
(0,2), (1,2), (2,2), (2,1), (1,0); -- glider
隣人を数えるために、手続き型ループを回す代わりに、「マトリックス」を8方向すべてに1つのセルに移動し、各位置にある生きているセルの数を合計してみましょう。
WITH shift(x,y) AS (
VALUES (0,1), (0,-1), (1,0), (-1,0), (1,1), (1,-1), (-1,1), (-1,-1)
),
neighbors(x,y,cnt) AS (
SELECT t.x, t.y, count(*)
FROM (
SELECT c.x + s.x, c.y + s.y
FROM cells c
CROSS JOIN shift s
) t(x,y)
GROUP BY t.x, t.y
)
SELECT * FROM neighbors;
シフトはクエリを使用して作成することもできますが、おそらく簡単にはなりません。
隣人がいるので、どの細胞が死ぬべきか、そしてどの細胞が生まれるべきかを決定することは残っています:
WITH shift(x,y) AS (
...
),
neighbors(x,y,cnt) AS (
...
),
generation(x,y,status) AS (
SELECT coalesce(n.x,c.x),
coalesce(n.y,c.y),
CASE
WHEN c.x IS NULL THEN 'NEW'
WHEN n.cnt IN (2,3) THEN 'STAY'
ELSE 'DIE'
END
FROM neighbors n
FULL JOIN cells c ON c.x = n.x AND c.y = n.y
WHERE (c.x IS NULL AND n.cnt = 3)
OR
(c.x IS NOT NULL)
)
SELECT * FROM generation;
ここでは完全な接続が必要です。そうすることで、一方では空のセルで新しい生命が生まれ、他方では「郊外」の生きているセルを破壊することができます。サンプルに入るには、3つの条件があります。セルが空で、隣接するセルが3つだけある(その後、有効になり、新しいステータスを取得する必要があります)、または有効で、隣接するものが2つまたは3つある(その後、存続してSTAYステータスを取得する)、または有効ですが、ネイバーが2つ未満または3つを超える(その後、死ぬ運命にあり、DIEステータスを受け取ります)。
次に、新世代のセルに関する情報を使用して、競技場を更新する必要があります。ここでPostgreSQLの機能が役に立ちます。必要なことはすべて同じSQLステートメントで実行します。
WITH shift(x,y) AS (
...
),
neighbors(x,y,cnt) AS (
...
),
generation(x,y,status) AS (
...
),
del AS (
DELETE FROM cells
WHERE (x,y) IN (
SELECT x, y FROM generation WHERE status = 'DIE'
)
),
ins AS (
INSERT INTO cells
SELECT x, y FROM generation WHERE status = 'NEW'
)
SELECT *
FROM generation
WHERE status IN ('STAY','NEW');
実際、ゲームのすべてのロジックが書かれています!
このアルゴリズムに、目に馴染みのある2次元マトリックスの形式で結果を表示することはすでに簡単です。そして、ケーキの上のアイシングのように、psqlコマンドを使用してクエリを終了
\watchし、画面上で世代が毎秒互いに変化するようにすることができます。
これがクエリ全体であり、出力は最小限に抑えられています。コピー、貼り付け、お楽しみください!
WITH shift(x,y) AS (
VALUES (0,1), (0,-1), (1,0), (-1,0), (1,1), (1,-1), (-1,1), (-1,-1)
),
neighbors(x,y,cnt) AS (
SELECT t.x, t.y, count(*)
FROM (
SELECT c.x + s.x, c.y + s.y
FROM cells c
CROSS JOIN shift s
) t(x,y)
GROUP BY t.x, t.y
),
generation(x,y,status) AS (
SELECT coalesce(n.x,c.x),
coalesce(n.y,c.y),
CASE
WHEN c.x IS NULL THEN 'NEW'
WHEN n.cnt IN (2,3) THEN 'STAY'
ELSE 'DIE'
END
FROM neighbors n
FULL JOIN cells c ON c.x = n.x AND c.y = n.y
WHERE (c.x IS NULL AND n.cnt = 3)
OR
(c.x IS NOT NULL)
),
del AS (
DELETE FROM cells
WHERE (x,y) IN (
SELECT x, y FROM generation WHERE status = 'DIE'
)
),
ins AS (
INSERT INTO cells
SELECT x, y FROM generation WHERE status = 'NEW'
),
dimensions(x1,x2,y1,y2) AS (
SELECT min(x), max(x), min(y), max(y)
FROM generation
WHERE status IN ('STAY','NEW')
)
SELECT string_agg(CASE WHEN g.x IS NULL THEN ' ' ELSE '*' END, '' ORDER BY cols.x)
FROM dimensions d
CROSS JOIN generate_series(d.x1,d.x2) cols(x)
CROSS JOIN generate_series(d.y1,d.y2) lines(y)
LEFT JOIN generation g ON g.x = cols.x AND g.y = lines.y AND g.status IN ('STAY','NEW')
GROUP BY lines.y
ORDER BY lines.y
\watch 1