Skip to content

織物用ソフトウェアの設計

私の設計原則を、一風変わったドメインに適用してみる

この記事は 2025 F# Advent Calendar の一部です。他にも素晴らしい記事がたくさんありますので、ぜひチェックしてみてください!そして、主催者の Sergey Tihon さんに感謝いたします。

最近、私は新しい趣味として「手織り」を始めました。瞑想的で身体的な活動であり、形に残る成果が得られるため、画面の前で過ごしすぎたり、ダラダラとSNSを眺めたり(ドゥームスクロール)することへの素晴らしい解毒剤になります。初めて試したとき、手織りのプロセスに惹かれるものがあり、もっと詳しく知りたいと少し夢中になりました。手織りという分野は、楽器の習得と同じように、底知れない奥深さがあることが分かりました。そして、分別のあるプログラマーなら誰もがそうするように、学んだことを活かしてソフトウェアを書いてみたいと思ったのです。

このシリーズでは、私がどのようにして織物用ソフトウェアを設計したか、そしてその過程で行ったいくつかの決定について説明します。このドメインは、私が普段扱っている「つまらない業務アプリケーション(BLOBAS: Boring Line Of Business Applications)」とは大きく異なります。そのため、私の標準的なアプローチが果たして役に立つのかどうか、非常に興味がありました。

私が下した設計上の決定を理解していただくためには、まずこのドメイン(領域)を理解する必要があります。読者の皆さんの多くは手織りについて何もご存知ないでしょうから、この記事の前半でその説明に時間を割くことをお許しください。

なぜ手織りはプログラマーを惹きつけるのか?

Section titled “なぜ手織りはプログラマーを惹きつけるのか?”

編み物やクロッシェ(かぎ針編み)といった、一見似ているファイバーアートとは異なり、手織りには「プログラミングのような」事前の計画が多く含まれます。さらに、2本の糸を複雑に絡み合わせることで、非常に興味深い効果が生まれます。これがプログラマーに訴えかけるのは、フラクタルや L-system のようなジェネレーティブ・アート(生成芸術)と同じ理由ではないかと思います。

手織りに惹かれるプログラマーは私だけではないようです。例えば、私が手織りのパターンを調べ始めたとき、よく Ralph Griswold という名前に出くわしました。ピンときました。なぜなら、彼が書いた Iconプログラミング言語の本は、私がプログラミングを始めたときに最初に手に入れた本の一冊だったからです。彼は手織りにも精通しており、教授を務めていたアリゾナ大学に 膨大な手織りの資料アーカイブ を構築しました。他にも、ソフトウェアアーキテクトの Kris Bruland 氏は、handweaving.net という大規模で印象的な織物サイトを構築しています。

そしてもちろん、初期のコンピューターにおけるパンチカードの使用が、ジャカード織機 に直接由来していることは、誰もが知るところです。

なぜ織物ソフトウェアなのか?

Section titled “なぜ織物ソフトウェアなのか?”

画面から離れるために手織りをしていると言いながら、なぜソフトウェアが関わってくるのでしょうか?

一本の糸で作業する編み物やクロッシェとは異なり、織物には常に「2つ」の糸が関わります。上下方向の糸である「経糸(たていと:warp)」と、左右方向の糸である「緯糸(よこいと:weft)」です。これらの糸が複雑に重なり合います。

  • 織りの組織(経糸と緯糸がどのように上下に交差するか)は、非常に複雑になることがあります。
  • 経糸と緯糸に異なる色を使うと、視覚的な混色(並置混合) が起こります。
  • 隣接する色の関係も、視覚的な効果を生み出します。

これらすべてが意味するのは、織りの組織と個々の糸の色の相互作用によって、印象的で直感に反するようなデザインが生まれるということです。

例えば、次の3つのデザインは、全く同じ織りの組織を使用していますが、糸の配色のパターンをわずかに変えています。

そして、こちらは全く同じ配色のパターン(濃い色と薄い色の交互)を使用していますが、織りの組織をわずかに変えたものです。

これらの例はたった2色で、非常に単純な織りの組織を使っています。実際には、もっと複雑になり得ます!

そのため、多くの織り手はソフトウェアを使ってデザインをプレビューします。骨の折れる織機のセットアップ作業に取り掛かる前に、組織と色の組み合わせを視覚的に確かめておけるからです。それらの多くは優れたプログラムですが、私が自分自身で実装したいと思った理由がいくつかあります。

第一に、そのほとんどがウェブアプリではなくデスクトップアプリであることです。つまり、自分の作品を共有したい場合、URLを送信するだけでなく、ファイルに書き出してそのファイルを送信しなければなりません。

第二に、そしてより重要なことですが、既存のアプリは抽象的なレベルではなく、糸一本一本の単位で動作するからです。それがどういう意味かを示すために、織物のデザインがどのようになされるかを説明しなければなりません。

「ドラフト(織り方図)」による織物デザインの伝達

Section titled “「ドラフト(織り方図)」による織物デザインの伝達”

最も基本的な組織である「平織り(ひらおり:plainweave)」から始めましょう。平織りでは、垂直方向の経糸を一筋おきに持ち上げて隙間を作り、そこに水平方向の緯糸を通します。次の段では、もう一方の半分の糸を持ち上げて、別の緯糸を通します。これにより、古典的な「上、下、上、下」の見た目が生まれます。

このプロセスを他人に伝えるにはどうすればいいでしょうか? それを行うのが「ドラフト(織り方図)」です。これは、織物のデザインを伝えるための視覚的な方法です。

ドラフトを描く最も一般的な方法は、パターンの上部と右側にグリッド(格子)を描くことです。グリッドは、各ステップでどの糸が持ち上げられているか、あるいは下がっているかに応じて塗りつぶされます。これが平織りのドラフトです。

  • デザインの上のグリッド(Threading:通し順)は、経糸がどのようにグループ化されているかを示します。グリッドは右から左に読みます。最初の垂直な経糸(一番右)はグループ1に入ります。2番目の糸はグループ2に入り、3番目は再びグループ1に入ります。といった具合です。これらのグループは「シャフト(綜絖:そうこう)」と呼ばれます。物理的な織機では、同じグループのすべての糸は、織機の全幅を横切る一本の棒(=シャフト)に繋がれており、必要に応じて一緒に持ち上げられます。
  • デザインの右側のグリッド(Liftplan:踏み順)は、各段(行)でどのシャフトを持ち上げるかを記述しています。最初の段ではシャフト1が持ち上げられ、それによって経糸の半分が持ち上がります。2番目の段ではシャフト2が持ち上げられ、残りの半分の経糸が持ち上がります。そして、段を下るにつれてシャフト1と2を交互に繰り返します。

では、別のドラフトを見てみましょう。今度は「綾織り(あやおり:twill)」と呼ばれる織り組織で、特徴的な斜めのパターン効果があります(ジーンズのデニムは綾織りです)。

  • 通し順(上)のグリッドには4つの行があり、4つの糸グループ(シャフト)があることを示しています。各経糸は線形なシーケンスでシャフトに割り当てられています。4本の糸のあと、シーケンスが繰り返されます。
  • 踏み順(右)のグリッドを見ると、最初の段ではシャフト1と2の両方を持ち上げる必要があることがわかります。2番目の段では、シャフト2と3を持ち上げます。以下同様です。4段目のあと、シーケンスが繰り返されます。

これでドラフトの仕組みが理解できたので、ほとんどの織物ソフトウェアがどのように動作するかも理解できるでしょう。ドラフトをデザインするには、上部や右側のグリッドの小さな四角をクリックして、デザインにどのような影響が出るかを確認するだけです。クリックするたびにパターンが変わるのを見るのは本当に楽しいものです。

デザインへのジェネレーティブなアプローチ

Section titled “デザインへのジェネレーティブなアプローチ”

あなたがプログラマーなら、上記のどちらのドラフトにも非常に明白なパターンがあることに気づくでしょう。例えば綾織りのドラフトでは、上部のグリッドは「1 2 3 4」というパターンが4回繰り返されています。同様に、踏み順のグリッドも「1&2, 2&3, 3&4, 1&4」という同じパターンが繰り返されています。

ユーザーとしては、100本の糸が「1-2-3-4」のパターンであることを示したい場合、通し順グリッドの100個の四角を一つずつクリックしたくはありません。同様に、同じシャフトの組み合わせを何度も持ち上げる場合、踏み順の四角を繰り返しクリックしたくもありません。

ほとんどの織物ソフトウェアは、直線やジグザグなどの一般的なグリッドパターンを簡単に描くためのツールを提供していますが、デザインの根本的なベースは依然として「糸一本単位」です。私は、パターンを指定すれば、ソフトウェアが個々の糸を生成してくれるような「ジェネレーティブ(生成的な)」アプローチが欲しいと思いました。

例えば、「パターン [1 2 3 4] を4回繰り返す」と言えば、前述の綾織りの通し順パターンを生成してほしいのです。

実際、織り手はある種の文脈ですでにこれを行っています。例えば、[1 3 2 3] という通し順パターンを「ブロックA」、[1 4 2 4] を「ブロックB」、別のパターンを「ブロックC」と呼び、「A、A、B、C」のように言うことがあります。こちらは、このようにラベル付けされたブロックを持つ「Summer and Winter」というパターンです。

色に関しても、同じことをしたいと考えました。下記の千鳥格子(ちどりごうし:houndstooth)のドラフトでは、「1番目の色を4回繰り返し、次に2番目の色を4回繰り返す」と言いたいのです。

さらに、「茶色」といった具体的な色ではなく、「1番目の色」という間接的な参照を使いたいと思います。これなら、どの色の組み合わせが一番いいか、カラーパレットを簡単に切り替えて確認することができます。

プログラマーとして、この「グループ」と「繰り返し」という概念から、すぐに正規表現を思い浮かべました。そこで、頭の中でパターンを記述するための構文を考え出しました。

パターンとは:

  • シャフトや色を表す単一の数字。例:"1"
  • 糸のグループ、またはラベル付けされたブロック。例:"[ 1 2 3 4]""[A B]"
  • 糸のグループにラベルを付ける方法。例:"A=[ 1 2 ]; B=[3 4]"
  • 上記のいずれかの繰り返し。例:"1x2 [3 4]x3 [A B]x4"

もちろん、小さな構成要素を組み合わせてジェネレーティブな手織りを行おうと考えたのは私だけではありません。例えば、Laura Devendorf 氏は AdaCAD に関する 興味深いプレゼンテーション を行いました。AdaCADは、ビジュアルなフローチャート形式で織物をデザインするアプローチです。私はプログラマーであり織り手でもありますが、実際に使うには少し圧倒されるように感じ、自分のビジョンとは一致しませんでした。

いよいよ、コードの時間です! 私は常々、良いドメインモデルはある程度自己文書化されているべきだと主張してきました。そこで、通し順グリッドのための私のドメインモデルを紹介します。コードを読むだけで、どの程度ドメインを理解できるでしょうか?

type ThreadingBlock =
/// 単一の糸
| Single of ThreadingEnd
/// 糸またはサブグループのコレクション
| InlineGroup of ThreadingBlock list
/// 文字ラベルを使用して別途定義された定義を参照する
| LabeledGroup of GroupLabel
/// 単一の糸または糸グループの繰り返し
| Repeat of ThreadingBlock * RepeatCount

見ての通り、コードの中で語彙を再現しようとしています。どこにも intstring はありません。

また、これを見ると ThreadingEnd という新しい単語が出てきていることに気づくでしょう。この「エンド」とは実際、織り手の専門用語で、経糸の通し順における1本の糸を指します。このドメインコードを読んだことで、ドメインの新しい語彙を一つ学んだというわけです!

他にも2つの型が登場しますが、どちらも制約付きの単純な型です。

// 1..1000の範囲に制約される
type RepeatCount = RepeatCount of int
// A..Zの範囲の文字に制約される
type GroupLabel = GroupLabel of string

これらの型によるオーバーヘッドは心配していません。デザインは一般的に小さく、実際にはこれらの型が数十個割り当てられるだけだからです。

個別の文字ラベル付きグループは、次のように定義されます。

/// ドラフトを作成する際に参照できる、
/// ラベル付きの ThreadingBlock の定義
type LabeledThreadingGroup = {
Label: GroupLabel
Blocks : ThreadingBlock list
}

そして、これらをまとめて Threading(通し順)の全体定義にします。

type Threading = {
// メインの定義
Blocks: ThreadingBlock list
// メインの定義から参照できる他のグループ
LabeledGroups : LabeledThreadingGroup list
}

これらの型を使えば、上記の「Summer and Winter」の通し順パターンを、次のようなテキスト表現から生成できるはずです。

A = 1 3 2 3
B = 1 4 2 4
C = 1 5 2 5
D = 1 6 2 6
Threading = Ax2 B Cx2 D

踏み順(liftplan)グリッドのデザインも非常に似ています。新しい型 LiftplanPick に注目してください。実際、緯糸の一段のことを織り手は「ピック(pick)」と呼びます。また新しい語彙ですね。

type LiftplanBlock =
| Single of LiftplanPick
| InlineGroup of LiftplanBlock list
| LabeledGroup of GroupLabel
| Repeat of LiftplanBlock * RepeatCount
type Liftplan = {
// メインの定義
Blocks: LiftplanBlock list
// メインの定義から参照できる他のグループ
LabeledGroups : LabeledLiftplanGroup list
}

配色パターン(color patterns)のデザインも同様ですが、単一ユニットが糸ではなく、パレットの色番号(インデックス)になっています。

/// パレットの色番号。
/// 人間が読みやすいよう、1始まり(1-based)で設計。
type ColorIndex = ColorIndex of int
type ColorPatternUnit =
| Single of ColorIndex
| InlineGroup of ColorPatternUnit list
| LabeledGroup of GroupLabel
| Repeat of ColorPatternUnit * RepeatCount
type ColorPattern = {
// メインの定義
Units: ColorPatternUnit list
// メインの定義から参照できる他のグループ
LabeledGroups : LabeledPatternUnit list
}

これらすべての型が、構造において極めて似ていることがわかります。この重複を防いで、3つのデザインすべてを表現できる、よりジェネリックな型を作ることはできないでしょうか?

はい、可能です。それがこちらです。

type Block<'single> =
| Single of 'single
| InlineGroup of Block<'single> list
| LabeledGroup of GroupLabel
| Transform of Block<'single> * RepeatCount
type LabeledGroup<'single> = {
Label: GroupLabel
Blocks : Block<'single> list
}
type Pattern<'single> = {
Blocks: Block<'single> list
LabeledGroups : LabeledGroup<'single> list
}

'single という型パラメータを持たせることで、ThreadingEndLiftplanPick、または ColorIndex を使用できるようにしました。

type ThreadingBlock = Block<ThreadingEnd>
type Threading = Pattern<ThreadingEnd>
type LiftplanBlock = Block<LiftplanPick>
type Liftplan = Pattern<LiftplanPick>
type ColorPatternUnit = Block<ColorIndex>
type ColorPattern = Pattern<ColorIndex>

しかし、ここで配色パターンの構造を変更して、非常によくある「鏡像(ミラーイメージ)」を含めたいとしましょう。

type ColorPatternUnit =
| Single of ColorIndex
| InlineGroup of ColorPatternUnit list
| LabeledGroup of GroupLabel
| Repeat of ColorPatternUnit * RepeatCount
| Mirrored of ColorPatternUnit

ここで、オブジェクト指向設計の継承階層でよく直面するのと同じ問題に突き当たります。適合しない「特殊ケース」が現れたのです。

この特定のケースは、別の型パラメータを追加して「変換(transform)」を表現することで処理できます。

type Block<'single,'transform> =
| Single of 'single
| InlineGroup of Block<'single,'transform> list
| LabeledGroup of GroupLabel
| Transform of Block<'single,'transform> * 'transform

これで、配色パターンは次のように記述できます。

type ColorPatternTransform =
| Repeat of RepeatCount
| Mirror
type ColorPatternUnit = Block<ColorIndex,ColorPatternTransform>

一方で、他の2つについては元の「繰り返し」オプションを維持できます。

type WeaveTransform =
| Repeat of RepeatCount
type ThreadingBlock = Block<ThreadingEnd,WeaveTransform>
type LiftplanBlock = Block<LiftplanPick,WeaveTransform>

物事を汎用化しすぎるのは悪いアイデアか?

Section titled “物事を汎用化しすぎるのは悪いアイデアか?”

さて、3つの型を1つのジェネリックな型に置き換え、重複を避けました。これは確かに良いアイデアですよね? イアン・マルコム博士(訳注:映画『ジュラシック・パーク』の登場人物)の見解はこうです。

(訳注:画像内のセリフ:あんたのプログラマーたちは、ジェネリックにできるかどうかに夢中になりすぎて、すべきかどうかを考えようとしなかったんだ。)

一般的に重複を減らすのは良い考えですが、今回のケースでは、ドメイン型を完全にジェネリックにしてしまうと、ドメインコードがより混乱しやすく、理解しにくくなるリスクがあると考えています。

確かに、非ジェネリックな型には構造の重複がありますが、それらに含まれるドメインの語彙は「エンド(end)」対「ピック(pick)」のように異なります。ジェネリックな型にすると、そのような語彙が消えてしまいます。ドメイン駆動設計の目標の一つは、コード内の名前を現実世界の語彙と一致させることですが、ジェネリックにすることによって、これらの用語が明示的であるという利点が失われてしまいます。

(訳注:ツイート内容:テーブルなどの命名をする際は、常に将来の変化を見据えることが大切だ。ワインを売っているからとテーブル名を「wines」にすると、ビールも扱うようになった時に困る。 「beverages(飲料)」にすると、氷を売るようになった時に困る。 「products(商品)」にすると、サービス業に拡大した時に困る。というわけで、テーブル名は「stuff(あれこれ)」や「table1」にしておくのがおすすめだ。)

また、前述のように、型がそれぞれ異なる方向に進化し始めるリスクもあります。そうなると、ジェネリック版との乖離が生じるか、あるいはジェネリック版がさらに複雑にパラメータ化されることになります。

ダイクストラは抽象化について次のように述べています。

“抽象化の目的は、曖昧にすることではなく、絶対的に精密になれる新しい意味(セマンティック)レベルを作り出すことである。” ― エドガー・W・ダイクストラ

ここで重要なフレーズは「新しい意味レベル」です。ジェネリックな List<> 型の場合のように、すべての「リスト」が本質的に同じであるような、より高い抽象レベルが確かに存在するケースもあります(*ゲフンゲフン* …自由モノイド… *ゲフンゲフン*)。しかし、このドメインに関連する「新しい意味レベル」は存在するでしょうか。私はそうは思いません。

ただし(ネタバレになりますが)、後の記事でこのジェネリックなアプローチを再考することになります。そうした懸念はあるものの、コードをよりジェネリックにすることで、実際には理解やテストが容易になる例を見ていく予定です。

上記の設計は、設計者の視点からドメインモデルを明確に表現しているという点では優れています。しかし、実際にグリッドを描画するとなると、どうでしょうか?

明らかに、グリッドを描画するには、行と列を持つ一種の2次元行列が必要であり、特定の行と列に対して何を描画すべきかを素早く見つける方法が必要です。その観点から見ると、このドメインモデルは非常に「悪い」設計です!

これはDDD(ドメイン駆動設計)においてよくある問題で、内部実装にはユーザーのモデルとは大きく異なるモデルが必要になることが多々あります。

このような場合に極めて重要なのは、一つのモデルをすべての状況に適合させようとしないことです! あらゆる状況に合わせようとして、結局どの状況にも適さない、歪んで醜いモデルになってしまいます。

ですから、一つのモデルですべてを済まそうとするのではなく、織り方図(ドラフト)のグリッド描画に最適化された、別の明確なモデルを設計すべきです。これを「ドラフト(draft)」ドメインと呼び、ドラフト描画に使用する型を定義します。こちらがドラフトドメインにおける通し順グリッドのモデルです。

/// WeaveStructure の End とは異なり、DraftEnd には
/// シャフトだけでなく色も含まれます
type DraftEnd = {
Shaft : int
Width : int
Color : int
}
/// DraftThreading は、Threading を「展開した」バージョンであり、
/// すべてのグループと繰り返しを単一の糸に拡張したものです
type DraftThreading = {
Ends : DraftEnd[]
}

そして、こちらが踏み順(liftplan)のモデルです。

type DraftLiftplanShafts = int[]
type DraftLiftplanPick = {
Shafts: DraftLiftplanShafts
Width : int
Color : int
}
/// DraftLiftplan は、Liftplan を「展開した」バージョンであり、
/// すべてのグループと繰り返しを単一の段(行)に拡張したものです
type DraftLiftplan = {
Picks : DraftLiftplanPick[]
}

ご覧の通り、この「ドラフトドメイン」の設計は「デザインドメイン」のモデルとは大きな違いがあります。

  • ネストされた構造ではなく、高速なインデックスアクセスのための線形配列になっています。
  • 元の ThreadingEndLiftplanPick 型の代わりに、新しい DraftEndDraftLiftplanPick 型を使用し、以前ラップされていた型を原始的な int に展開しています。
  • デザインモデルでは織りの組織と配色パターンが分離されており、独立して変更可能でした。このドラフト設計では、グリッド描画時のアクセスを容易にするために、織りの組織と色が統合されています。

このように2つの独立したモデルを持つことで、それぞれのモデルをその目的に合わせて適切に設計でき、独立して進化させることができます。

次に、2つのドメインを接続する必要があります。これは、ブロックを配列に展開することで、デザインモデルからドラフトモデルを構築するいくつかの関数で行うことができます。これらの関数の実装は簡単です。

module DraftThreading =
val build: Threading -> ColorPattern -> DraftThreading
module DraftLiftplan =
val build: Liftplan -> ColorPattern -> DraftLiftplan

これで、両方の長所を活かすことができます。2つのドメイン特化モデルを持ちつつ、必要に応じてそれらを変換できるのです。

これまでのところ、予備的なドメインモデリング段階で行った決定には満足しています(ただし、実装を進めるにつれてリファクタリングする権利は留保しておきます!)。これに基づいた、ドメインモデリング段階での一般的なアドバイスは以下の通りです。

  • ドメインモデルを設計する際は、そのドメインの言葉だけを使うように努めること。
  • ドメインの語彙が失われ、その抽象化にドメイン特有の意味がないのであれば、設計をあまりに抽象化・汎用化しすぎないこと。
  • 一つのドメインモデルをあらゆる目的に使い回そうとしないこと。異なるサブドメインに対して複数のドメインモデルを作成し、それらの間をマッピングや変換で繋ぐこと。

これはシリーズの最初の記事として計画されています。今後の記事では、以下の内容を扱う予定です。

  • モデルのテキスト表現の作成とパース
  • デザインモデルからドラフトモデルへの変換
  • モデルのSVGファイルへのレンダリング
  • その他

読んでいただきありがとうございました!