目次
背景
- 前回の記事にて, F#を用いて数独のソルバを実装してみた
- 制約式の生成で
for文を使っていた部分を関数型で書き直したくなった - 複数ファイルの並列処理を行いたくなった
- ついでに処理時間を計測したくなった
以上の経緯で記事を書きます. ディレクトリ構成を以下に示しておきます. 必要に応じてご参照ください.
※前回の記事と同じです.
sudoku-solver/
├── Core/
│ ├── Optimizer/
│ │ └── SudokuSolver.fs # ソルバ
│ ├── Utils/
│ │ ├── CsvHandler.fs # 問題のCSV読み込み処理
│ │ ├── Models.fs # 問題の型や解のタイプ等
│ │ ├── PathHandler.fs # パスのハンドリングを行う
│ │ └── Visualizer.fs # ソルバの結果を可視化する
│ ├── Core.fsproj
│ └── Program.fs # エントリポイント
├── Data/
│ └── base/ # 問題集
│ ├── L7_1_01.csv
│ ...
│ └── L7_1_10.csv
└── sudoku-solver.sln
制約式からfor文を撲滅する
for文がもたらす副作用
まず, 副作用の代表格(主観)である, for文です.
using System;
namespace Core
{
static class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
}
}
}
上記コードにおいて, for文内部の変数iが毎回書き換えられています.
ループ1回ごとに内部の振る舞いが変わるため, for文は典型的な副作用であると言えます.
それでは改めて, 数独ソルバの制約式を見てみましょう.
ヒント数字に関する制約
ヒント数字に関する制約の実装を提示します.
module SudokuSolver =
open Google.OrTools.Sat
/// 制約を追加するプライベートモジュール
module private ConstraintAdder =
/// 初期の制約を追加する
let private addConstraintsInitial (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
let idxs = [0..8]
// 初期の制約を追加する
for r in idxs do // <- ココと,
for c in idxs do // <- ココ.
let value = puzzle.Cells[r, c]
if value <> 0 then
[vars.[r,c,value-1] :> ILiteral]
|> List.toSeq
|> model.AddBoolAnd
|> ignore
上記のコードにおいて, for文が連続しています.
行と列の数だけ, 0~8までのインデックスを回す処理になっています.
これを, 次のように変更しました.
module SudokuSolver =
open Google.OrTools.Sat
/// 制約を追加するプライベートモジュール
module private ConstraintAdder =
/// 初期の制約を追加する
let private addConstraintsInitial (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
puzzle.Cells
|> Array2D.iteri (fun r c value ->
if value <> 0 then
[ vars.[r, c, value-1] :> ILiteral]
|> model.AddBoolAnd
|> ignore
)
Array2D.iteriについて. F#の二次元配列が持つ機能で,
- 行方向のインデックス
- 列方向のインデックス
- 上記行, 列インデックスが指し示す値
の3つを同時に回すことが出来ます.
個人的には衝撃的なツールで, forの2重ループを撲滅出来るのが素敵です.
(PythonでもC#でも, ネストが深くなってちょっとイヤでした...)
そこから, もしヒント数字が入っていたら, それをインデックスとして書き換え(マイナス1),
ILiteralに変換し, model.AddBoolAndでtrueを確定させる, という流れです.
書いてあるそのまんまで良いですね.
各セルにはちょうど1つの数字が入る制約
各セルにはちょうど1つの数字が入る制約の実装を提示します.
module SudokuSolver =
open Google.OrTools.Sat
/// 制約を追加するプライベートモジュール
module private ConstraintAdder =
/// 各セルにはちょうど1つの数字が入る
let private addConstraintsOne (model: CpModel) (vars: BoolVar[,,]): unit =
let idxs = [0..8]
// 各セルにはちょうど1つの数字が入る
for r in idxs do
for c in idxs do
[ for v in idxs -> vars.[r, c, v] :> ILiteral ]
|> model.AddExactlyOne
|> ignore
先ほどと同様, 上記のコードにおいて, for文が連続しています.
これを, 次のように変更しました.
module SudokuSolver =
open Google.OrTools.Sat
/// 制約を追加するプライベートモジュール
module private ConstraintAdder =
/// 各セルにはちょうど1つの数字が入る
let private addConstraintsOne (model: CpModel) (vars: BoolVar[,,]): unit =
Array2D.init 9 9 (fun r c -> vars.[r, c, *])
|> Seq.cast<BoolVar[]>
|> Seq.iter (fun slice ->
slice
|> Seq.map (fun v -> v :> ILiteral)
|> model.AddExactlyOne
|> ignore
)
ちょっと独特なことをしています.
前提として, varsは三次元のキューブ状に変数が並んだ箱だと考えてください.
以下のような操作をしています
-
Array2D.init 9 9で,9x9の二次元配列 - 上記配列の中に,
varsの三次元キューブから,r, cに対応する特定のマスに対して, 奥行き(vの方向)の変数9個を, 1本ずつ串としてごそっと取ってくる -
Seq.cast<BoolVar[]>で, 上記の串1本をBoolVar型の配列のSeqに変換 - 上記
SeqをILiteralに変換し, 串1本につきtrueになる変数は一つという制約条件を与える
特筆すべきは, Pythonでいう所のスライスを使用しているところですね.
Array2D.init 9 9 (fun r c -> vars.[r, c, *])
コードインデックスr cで走査しますが,
高さ分のインデックスvは*でスライスされ,
その分はSeq.iterの部分で使われます.
numpyでいう所のvars[:, c, v]の書き方に共通した考え方です.
1~9の数値が出てくるのは3×3の領域それぞれで1回
1~9の数値が出てくるのは3×3の領域それぞれで1回の制約の実装を提示します.
module SudokuSolver =
open Google.OrTools.Sat
/// 制約を追加するプライベートモジュール
module private ConstraintAdder =
/// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
let idxs = [0..8]
// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let blockIdx = [0..2]
for v in idxs do
for rBlock in blockIdx do
for cBlock in blockIdx do
[
for r in blockIdx do
for c in blockIdx
-> vars.[r+3*rBlock, c+3*cBlock, v]
:> ILiteral
]
|> List.toSeq
|> model.AddExactlyOne
|> ignore
for文撲滅します.
module SudokuSolver =
open Google.OrTools.Sat
/// 制約を追加するプライベートモジュール
module private ConstraintAdder =
/// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
// 9つの各ブロック(k)において、3x3の範囲スライスを切り出して1次元配列に潰す
Array2D.init 9 9 (fun v k ->
let rBlock = (k / 3) * 3
let cBlock = (k % 3) * 3
vars.[rBlock .. rBlock + 2, cBlock .. cBlock + 2, v]
|> Seq.cast<BoolVar>
|> Seq.toArray
)
|> Seq.cast<BoolVar[]>
|> Seq.iter (fun slice ->
slice
|> Seq.map (fun v -> v :> ILiteral)
|> model.AddExactlyOne
|> ignore
)
この制約は若干骨が折れますね. 前半と後半に分けましょう.
まずは前半から.
/// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
// 9つの各ブロック(k)において、3x3の範囲スライスを切り出して1次元配列に潰す
Array2D.init 9 9 (fun v k ->
let rBlock = (k / 3) * 3
let cBlock = (k % 3) * 3
vars.[rBlock .. rBlock + 2, cBlock .. cBlock + 2, v]
|> Seq.cast<BoolVar>
|> Seq.toArray
)
|> Seq.cast<BoolVar[]>
// 後半略
ちょっと複雑な書き方してますが...
-
rBlockとcBlockで, 3で割った商と剰余に対して3をかけて, 3x3ブロックの"左上"のインデックスを取得 - 上記3x3ブロックの"左上"のインデックスに対し,
varsから各3x3ブロックの変数群を取得 - 上記3x3ブロックの変数を,
Seq.cast<BoolVar>で1次元の配列にする
続いて後半です.
/// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
// 前半略
|> Seq.iter (fun slice ->
slice
|> Seq.map (fun v -> v :> ILiteral)
|> model.AddExactlyOne
|> ignore
)
前半で作成した1次元の配列を受け取り, 各配列に対してtrueは一つだけという制約式を与えます.
並列処理を行う
先ほどまでは数独ソルバの中身について論じましたが, ここからは並列処理について記載します.
まずは全体のコードです. 通常の処理も一緒に書きます
// Core/Program.fs
namespace Core
module Program =
open Core.Utils.CsvHandler
open Core.Utils.PathHandler
open Core.Utils.Visualizer
open Core.Optimizer.SudokuSolver
/// 順番に処理する
let syncRun (root: string) (targetFiles: string list): unit =
// ターゲットのファイルリストに対し,
// 絶対パス取得->問題取得->求解->可視化
targetFiles
|> List.map (combinePaths root)
|> List.iter (
fun csvPath ->
printfn "Loading: %s" csvPath
csvPath
|> loadPuzzle
|> solvePuzzle
|> visualizeResult
)
/// 並列で処理する
let asyncRun (root: string) (targetFiles: string list): unit =
targetFiles
|> List.map (fun path ->
async {
let result =
path
|> combinePaths root
|> loadPuzzle
|> solvePuzzle
return (path, result)
}
)
|> Async.Parallel
|> Async.RunSynchronously
|> Array.iter (fun (path, result) ->
printfn "Result for %s:" path
visualizeResult result
)
順番に処理するところは前回と一緒ですね. 並列処理の部分を解説します.
まずはこの部分からです.
targetFiles |> List.map (fun path -> async { ... })
ここで, 「パス結合->問題をローディング->解を求める->パスと結果を返す」の一連の流れを,
ファイル数分だけ作成します. この時点では実行されません.
ここからの流れは全体を見た方が良いので, 上記の部分を省略して再掲します.
/// 並列で処理する
let asyncRun (root: string) (targetFiles: string list): unit =
targetFiles
|> List.map (fun path -> async { ... })
|> Async.Parallel
|> Async.RunSynchronously
|> Array.iter (fun (path, result) ->
printfn "Result for %s:" path
visualizeResult result
)
上記「パス結合->問題をローディング->解を求める->パスと結果を返す」のパックを以下のように処理します
-
Asyc.Parallel- PCのCPUにタスクアロケーションする
-
Async.RunSynchronously- 各CPUコアに「よーいドン」で解かせる
- 全部が解き終わるまで待ち, 配列に格納する
-
Array.iter- 上記で解き終わった結果の配列を画面に出力する
こんな感じで, シンプルな並列処理を実装しました.
処理時間を計測する
並列処理を実装したら, どのくらい早くなったのか, 計測したくなりますよね?当然ですよね?
実装
と, いうわけで, 実装してみました. まずは計測する関数です.
// Core/Program.fs
namespace Core
module Program =
open System.Diagnostics
// 任意の処理(f)を実行し, 計測時間と結果を返す
let measureTime (f: unit -> 'T): int64 * 'T =
let sw = Stopwatch.StartNew()
let result = f()
sw.Stop()
(sw.ElapsedMilliseconds, result)
シンプルですね. C#ラブな皆様なら割とメジャーかな?と思いますが...
System.Diagnosticsを使用して, ミリ秒単位で返します.
任意の関数の返り値はresultでラッピングしてあげて, 一緒に返してあげます.
使い方は以下の通りです.
// Core/Program.fs
namespace Core
module Program =
// 省略
// 任意の処理(f)を実行し, 計測時間と結果を返す
let measureTime (f: unit -> 'T): int64 * 'T =
// 省略
/// 順番に処理する
let syncRun (root: string) (targetFiles: string list): unit =
// 省略
/// 並列で処理する
let asyncRun (root: string) (targetFiles: string list): unit =
// 省略
// エントリポイント
[<EntryPoint>]
let main args =
// ルートパスを取得する
let root = resolveProjectRoot args
printfn "ProjectRoot : %s" root
// ターゲットのファイルをリストでベタ書きする
let targetFiles = [
"Data/base/L7_1_01.csv";
// 省略
"Data/base/L7_1_10.csv";
]
// 順番に処理する
let time1, _ = measureTime (fun () -> syncRun root targetFiles)
// 並列で処理する
let time2, _ = measureTime (fun () -> asyncRun root targetFiles)
// 比較用に書き出す
printfn "Sync elapsed: %d ms" time1
printfn "Async elapsed: %d ms" time2
0
重要なポイントがあるので, 解説しておきます.
順番に処理する部分で, 以下の記載があります.
// 順番に処理する
let time1, _ = measureTime (fun () -> syncRun root targetFiles)
ここ, 理解出来たら何てことないのですが, 私はちょっとミスしたので共有します.
私は最初, 以下のように書いていました.
// 順番に処理する
let time1, _ = measureTime (syncRun root targetFiles)
高階関数に慣れ始めて, 上記のような書き方をしていました. これの書き方がなぜダメかと言うと,
-
measureTimeを実行する前に,syncRun root targetFilesを実行する - 数独を全て解き終わった後の結果(
unit)を,measureTimeに渡す
という処理になってしまいます.
再掲しますが, 意図通りに動く書き方は以下の通りです.
// 順番に処理する
let time1, _ = measureTime (fun () -> syncRun root targetFiles)
匿名関数であるfun () -> ...を使用します.
この匿名関数でまずはsyncRun root targetFilesを実行せずにmeasureTimeに渡し,
measureTime内で実行する, という動きになります.
計測結果
10回ずつ実行して時間を計測してみました. まずは箱ひげ図をどうぞ.
細かい集計とデータは以下をご覧ください.
ざっくりですが, 並列処理により4倍程度の高速化になっていますね.
集計データです.
| Term | Sync[ms] | Async[ms] | Ratio (Sync/Async) |
|---|---|---|---|
| average | 212.5 | 51.1 | 4.16 |
| median | 209.5 | 50.5 | 4.15 |
各試行データです
| Sync[ms] | Async[ms] | |
|---|---|---|
| 1 | 202 | 52 |
| 2 | 224 | 55 |
| 3 | 213 | 51 |
| 4 | 213 | 50 |
| 5 | 210 | 54 |
| 6 | 207 | 54 |
| 7 | 206 | 48 |
| 8 | 209 | 49 |
| 9 | 206 | 49 |
| 10 | 235 | 49 |
最後に余談
F#の数独ソルバを良い感じに出来たかな?と思います.
普段だったらこれで終わりますが, ちょっとだけ余談です.
次の画像は, qiitaでC#タグ検索した結果です(2026年6月29日時点)
一方, F#でタグ検索すると...
| C# | F# | |
|---|---|---|
| articles | 20031 | 443 |
| follower | 56752 | 364 |
私もC#は好きですよ?オブジェクト指向で, 型は硬いし, 大規模開発するなら私はC#を一番に選びます. しかし, F#の良いところも見逃せないと思います.
実際, F#だと数式っぽく書けるし, パイプラインで美しく書けるし,
何より手続き型やオブジェクト指向っぽい書き方も出来て, 初心者に優しい.
良いところを挙げ出したらキリがないくらい, 良い言語だと思います.
もう少し盛り上がってくれないかなぁ...
私はチマチマと記事を書きます. 最後まで読んでもらえて嬉しいです. ありがとうございました.
Appendix
ひたすら実装したコードの全文を張ります.
CsvHandler
// Core/Utils/CsvHandler.fs
namespace Core.Utils
open System
open System.IO
open Core.Utils.Models
// CSVパーサー
module CsvHandler =
/// 文字をパースする. 空白はゼロ, それ以外はintに変換
let private parseCell (s: string): int =
match s.Trim() with
| "" -> 0
| s -> int s
/// 1行をカンマで区切り, 各文字をパースして配列で返す
let private parseLine (line: string) : int[] =
line.Split(',') |> Array.map parseCell
/// CSVパスを受け取り, 2次元配列に整形, 問題の型にして返す
let loadPuzzle (path: string) : SudokuPuzzle =
let cells =
File.ReadLines(path)
|> Seq.filter (fun line -> not (String.IsNullOrWhiteSpace(line)))
|> Seq.map parseLine
|> Seq.toArray
|> fun rows -> Array2D.init 9 9 (fun r c -> rows.[r].[c])
{ Cells = cells }
Models
// Core/Utils/Models.fs
namespace Core.Utils
/// 型定義
module Models =
/// 問題の型
type SudokuPuzzle =
{ Cells: int[,] }
/// ソルバの結果型
type SolveResult =
/// 解が見つかった
| Solved of SudokuPuzzle
/// 解無し
| Infeasible
/// タイムアウト等
| Unknown
PathHandler
namespace Core.Utils
open System
open System.IO
/// パスのハンドリング
module PathHandler =
/// ルートパスを何とかして返す関数
let resolveProjectRoot (args: string[]) =
// ステップその1. --root引数を探し出す. 見つからなければNone
let fromArgs =
args
|> Array.pairwise
|> Array.tryFind (fun (k, _) -> k = "--root")
|> Option.map snd
// ステップその1でルートパスが見つかっていればそのまま
match fromArgs with
| Some root -> root
| None ->
// cwdでカレントディレクトリを取得
let cwd = Directory.GetCurrentDirectory()
if Directory.Exists(Path.Combine(cwd, "Data", "base")) then
// カレントディレクトリにデータのディレクトリが存在が見つかったので,
// ルートパスとして適切. そのまま返す
cwd
else
// カレントディレクトリにデータのディレクトリが存在しない
// カレントディレクトリがnetX.0のディレクトリになっている
// 親(Debug or Release)の親(bin)の親(Core)のフルパスを取得して返す
AppDomain.CurrentDomain.BaseDirectory
|> Directory.GetParent
|> fun d -> d.Parent
|> fun d -> d.Parent
|> fun d -> d.FullName
/// 2つの文字列をパスとして結合する
let combinePaths (root: string) (relativePath: string) =
Path.Combine(root, relativePath)
Visualizer
// Core/Utils/Visualizer.fs
namespace Core.Utils
open Core.Utils.Models
/// 可視化用モジュール
module Visualizer =
let private getRow (arr: int[,]) (r: int) : int[] =
[| for c in 0..8 -> arr.[r, c] |]
let private getVisRow (row: int[]): string =
// 分かりにくいが, 要するに以下のようにする
// [1; 2; 3; 4; 5; 6; 7; 8; 9]
// -> "|1 2 3|4 5 6|7 8 9|"
$"|{row.[0]} {row.[1]} {row.[2]}|{row.[3]} {row.[4]} {row.[5]}|{row.[6]} {row.[7]} {row.[8]}|"
let private getVisList (sep: string) (arr: int[,]) =
let row r = getRow arr r |> getVisRow
[0..8]
|> List.collect (
fun r ->
if r % 3 = 0 then
[ sep; row r ]
else
[ row r ]
)
|> fun lines -> lines @ [ sep ]
let private visualizeCells (arr: int[,]) : unit =
let sep = "+-----+-----+-----+"
getVisList sep arr |> List.iter (printfn "%s")
/// ソルバの結果を受け取り, 可視化を行う
let visualizeResult (result: SolveResult): unit =
match result with
| Solved p -> visualizeCells p.Cells
| Infeasible -> eprintfn "Infeasible"
| Unknown -> eprintfn "Interrupted"
SudokuSolver
// Core/Optimizer/SudokuSolver.fs
namespace Core.Optimizer
open Core.Utils.Models
/// ソルバ
module SudokuSolver =
open Google.OrTools.Sat
/// モデル, 変数の生成を行うプライベートモジュール
module private ModelBuilder =
/// モデル, 変数を生成する
let buildModel (): CpModel*BoolVar[,,] =
// モデルを生成する
let model = CpModel()
// ブーリアン変数を9*9*9個作成する
let vars = Array3D.init 9 9 9 (fun r c v -> model.NewBoolVar($"x_{r}_{c}_{v}"))
model, vars
/// 制約を追加するプライベートモジュール
module private ConstraintAdder =
/// 初期の制約を追加する
let private addConstraintsInitial (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
puzzle.Cells
|> Array2D.iteri (fun r c value ->
if value <> 0 then
[ vars.[r, c, value-1] :> ILiteral]
|> model.AddBoolAnd
|> ignore
)
/// 各セルにはちょうど1つの数字が入る
let private addConstraintsOne (model: CpModel) (vars: BoolVar[,,]): unit =
Array2D.init 9 9 (fun r c -> vars.[r, c, *])
|> Seq.cast<BoolVar[]>
|> Seq.iter (fun slice ->
slice
|> Seq.map (fun v -> v :> ILiteral)
|> model.AddExactlyOne
|> ignore
)
/// 1~9の数値が出てくるのは1列に1回
let private addConstraintsColumn (model: CpModel) (vars: BoolVar[,,]): unit =
Array2D.init 9 9 (fun c v -> vars.[*, c, v])
|> Seq.cast<BoolVar[]>
|> Seq.iter (fun slice ->
slice
|> Seq.map (fun x -> x :> ILiteral)
|> model.AddExactlyOne
|> ignore
)
/// 1~9の数値が出てくるのは1行に1回
let private addConstraintsRow (model: CpModel) (vars: BoolVar[,,]): unit =
Array2D.init 9 9 (fun r v -> vars.[r, *, v])
|> Seq.cast<BoolVar[]>
|> Seq.iter (fun slice ->
slice
|> Seq.map (fun x -> x:> ILiteral)
|> model.AddExactlyOne
|> ignore
)
/// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
// 9つの各ブロック(k)において、3x3の範囲スライスを切り出して1次元配列に潰す
Array2D.init 9 9 (fun v k ->
let rBlock = (k / 3) * 3
let cBlock = (k % 3) * 3
vars.[rBlock .. rBlock + 2, cBlock .. cBlock + 2, v]
|> Seq.cast<BoolVar>
|> Seq.toArray
)
|> Seq.cast<BoolVar[]>
|> Seq.iter (fun slice ->
slice
|> Seq.map (fun v -> v :> ILiteral)
|> model.AddExactlyOne
|> ignore
)
/// 制約を追加する
let addConstraints (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
// 初期の制約を追加する
addConstraintsInitial model vars puzzle
// 各セルにはちょうど1つの数字が入る
addConstraintsOne model vars
// 1~9の数値が出てくるのは1列に1回
addConstraintsColumn model vars
// 1~9の数値が出てくるのは1行に1回
addConstraintsRow model vars
// 1~9の数値が出てくるのは3×3の領域それぞれで1回
addConstraintsBlock model vars
/// ソルバのプライベートモジュール
module private Solver =
/// 変数の配列を受け取り, Trueになっている変数のインデックスに1を足して返す
let private getIdx (solver: CpSolver) (vars: BoolVar[]): int =
vars
|> Array.findIndex (fun v -> solver.BooleanValue v)
|> (+) 1
/// 求解する
let solve (model: CpModel) (vars: BoolVar[,,]): SolveResult =
// ソルバを生成して求解する
let solver = new CpSolver()
let status = solver.Solve(model)
if status = CpSolverStatus.Optimal || status = CpSolverStatus.Feasible then
// 2次元配列に解のインデックスを入れ込む
let cells = Array2D.init 9 9 (fun r c ->
vars.[r, c, *]
|> getIdx solver
)
// 解けたので判別共用体にして返す
Solved {Cells = cells}
else
// 何らかの理由で解が見つからなかった
match status with
| CpSolverStatus.Infeasible -> Infeasible
| _ -> Unknown
/// 問題を受け取り, 結果を返す
let solvePuzzle (puzzle: SudokuPuzzle): SolveResult =
// モデル, 変数を生成する
let model, vars = ModelBuilder.buildModel()
// 制約を作成して追加する
ConstraintAdder.addConstraints model vars puzzle
// 求解する
Solver.solve model vars
Program
// Core/Program.fs
namespace Core
module Program =
open System.Diagnostics
open Core.Utils.CsvHandler
open Core.Utils.PathHandler
open Core.Utils.Visualizer
open Core.Optimizer.SudokuSolver
// 任意の処理(f)を実行し, 計測時間と結果を返す
let measureTime (f: unit -> 'T): int64 * 'T =
let sw = Stopwatch.StartNew()
let result = f()
sw.Stop()
(sw.ElapsedMilliseconds, result)
/// 順番に処理する
let syncRun (root: string) (targetFiles: string list): unit =
// ターゲットのファイルリストに対し,
// 絶対パス取得->問題取得->求解->可視化
targetFiles
|> List.map (combinePaths root)
|> List.iter (
fun csvPath ->
printfn "Loading: %s" csvPath
csvPath
|> loadPuzzle
|> solvePuzzle
|> visualizeResult
)
/// 並列で処理する
let asyncRun (root: string) (targetFiles: string list): unit =
targetFiles
|> List.map (fun path ->
async {
let result =
path
|> combinePaths root
|> loadPuzzle
|> solvePuzzle
return (path, result)
}
)
|> Async.Parallel
|> Async.RunSynchronously
|> Array.iter (fun (path, result) ->
printfn "Result for %s:" path
visualizeResult result
)
// エントリポイント
[<EntryPoint>]
let main args =
// ルートパスを取得する
let root = resolveProjectRoot args
printfn "ProjectRoot : %s" root
// ターゲットのファイルをリストでベタ書きする
let targetFiles = [
"Data/base/L7_1_01.csv";
"Data/base/L7_1_02.csv";
"Data/base/L7_1_03.csv";
"Data/base/L7_1_04.csv";
"Data/base/L7_1_05.csv";
"Data/base/L7_1_06.csv";
"Data/base/L7_1_07.csv";
"Data/base/L7_1_08.csv";
"Data/base/L7_1_09.csv";
"Data/base/L7_1_10.csv";
]
// 順番に処理する
let time1, _ = measureTime (fun () -> syncRun root targetFiles)
// 並列で処理する
let time2, _ = measureTime (fun () -> asyncRun root targetFiles)
// 比較用に書き出す
printfn "Sync elapsed: %d ms" time1
printfn "Async elapsed: %d ms" time2
0
fsproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Utils/Models.fs" />
<Compile Include="Utils/CsvHandler.fs" />
<Compile Include="Utils/PathHandler.fs" />
<Compile Include="Utils/Visualizer.fs" />
<Compile Include="Optimizer/SudokuSolver.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.OrTools" Version="9.15.6755" />
</ItemGroup>
</Project>


