293 Views
July 11, 26
スライド概要
書籍Functional Programming in Scala (略称"FP in Scala")で豊富な演習問題とともに語られる純粋関数型プログラミングの典型的な設計パターンや抽象化の過程について、JVM関数型言語Flix ( https://flix.dev/ )のコード例を通して親しもう。
関数型まつり2026セッション概要: https://fortee.jp/2026fp-matsuri/proposal/02925098-b3c9-4814-b6a1-e0a710399be0
「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Flix, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬
速習 FP in Scala with Flix 言語で親しむ純粋関数型のコード設計 Flix #fp_matsuri #fp_matsuri_a 1
🐬カマイルカ lagénorhynque 関数型プログラミング(言語)の愛好者/実践者 特にClojure, Haskell, Flixがエレガントで好き 関数型まつり2026の運営スタッフ(座長のひとり) 主に(CfPを含む)メインコンテンツの準備を担当 株式会社スマートラウンドのシニアエンジニア スタートアップと投資家のやり取りを効率化する データ管理プラットフォームを開発 ソフトウェア設計/開発とともに会計/財務/法務 について探求するのも好き 2
きっかけ 書籍 とは 言語の基本 純粋 関数型のコード設計 関数型プログラミング入門 関数型設計とコンビネータライブラリ 関数型設計に共通する構造 エフェクトと 1. 2. FP in Scala 3. Flix 4. ( ) 1. 2. 3. 4. I/O 3
1. きっかけ 4
関数型まつり2025での🐬の発表 関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ 5
書籍 Functional Programming in Scala (FP in Scala) 今年度は関数型プログラミングらしいコード設計/ 抽象化のパターンにより踏み込んで解説したい 関数型言語について学び始めた頃に取り組んだ本の ひとつに FP in Scala があった 演習問題が膨大で途中まで進めて積んでいたが😇 関数型まつり2026の「FP入門ハンズオン」の題材 として FP in Scala の演習問題を利用することに ハンズオン資料: fp-matsuri/2026.fp-hands-on 6
との出会い Flix Clojurian が出会ったsimple志向なJVM関数型言語Flix ——ファーストインプレッション 7
エフェクトシステムを備えた関数型言語Flix エフェクトシステムと代数的エフェクトを探求する 過程で、Flixという言語に出会った simple と easy を区別して simplicity を重視すると いう方向性もFlixの設計指針に含まれている Rich Hickeyの有名なプレゼンSimple Made Easy に由来する 🐬はClojurianなので大いに感激した😆 8
参考] Shibuya.lisp lispmeetup #118での🐬の発表 [ "Simple Made Easy" Made Easier: Clojure に学ぶsimplicity 9
「"Simple Made Easy" Made Easier 」の要約 simple は客観的な構造の単純さ、 easy は主観的な (広義の)近さの問題(なのだが誤解されがち) Clojureエコシステム/コミュニティには様々な simplicity 追求の例がある 抽象度を上げて、客体/モノと主体/ヒトの区別、 さらにはars/τέχνη (技芸)とvita/βίος (人生)の対比 と捉えることもできる 10
2. 書籍 FP in Scala とは 11
Functional Programming in Scala, Second Edition の書籍詳細ページより Manning 12
『Scala関数型デザイン&プログラミング』 第1版)の訳書 ※ Functional Programming in Scala ( インプレスブックスの書籍詳細ページより 13
📕 オブジェクト指向/関数型言語Scalaで(純粋関数型 言語Haskellのような) FP実践を目指す本 2014年初版当時のScalaコミュニティでは良くも 悪くも"better Java"扱いが珍しくなかった ScalaでFP設計を深く学べる(おそらく)最初の本 豊富な演習問題に取り組みながら理解を深め、抽象 化の必要性/必然性を体感できる教育的な構成 ただし、独習では挫折しやすい本でもある🥹 FP in Scala, a.k.a. The Red Book 14
書籍公式リポジトリfpinscala 演習問題/ヒント/解答例/テストコード Wiki: 追加の解説/チャプターノート Haskellの概説ページも [参考] HaskellコードのFlix移植例 by 🐬 cf. Scala以外の関数型言語による実装 fp-matsuri/fp-in-scala-exercises FP in Scala by lagenorhynque/fp-in-scala Clojure/Flix by ベースの関数型プログラミング演習 問題 関数型まつり 実装 🐬 15
言語の基本 3. Flix 16
とは エフェクトシステムの組み込みサポートが特徴的な 静的型付き関数型言語 副作用を伴う関数には IO エフェクトが付く Scalaと関係が深いが非オブジェクト指向言語 (サブタイピング(部分型)/継承とも無縁) JVM言語(Scala, Kotlin, Clojureなどと同じ) 主にHaskell, Scala, OCaml, Rustの影響が見られる 処理系の実装言語もScala 🐬< 大雑把には「Scalaの皮を被ったHaskell」🐑 Flix 17
ℹ️ 他の関数型言語経験者向けの補足 Hindley-Milner型システムが採用されている HaskellやML系言語と同様 ⚠️ トップレベルの定義のみ敢えて型注釈必須 関数は常にカリー化される HaskellやML系言語と同様 構文的に多引数関数に見えても実体は1引数関数 の連鎖 Haskellによく似た型クラス(type class)がある trait というキーワードはRust/Scala由来 18
コードの書き方(0): 事前準備
mkdir first-flix-project
cd first-flix-project
#
flix init
#
echo 'pub mod FirstFlixProject {}' > src/FirstFlixProject.flix
プロジェクトを生成
プロジェクトルートのモジュールファイルを追加
flix
コマンドでプロジェクトを生成する
19
コードの書き方(1): モジュールとそのトップレベル関数 // src/FirstFlixProject/SimpleMath.flix pub mod FirstFlixProject.SimpleMath { // pub def square(n: Int32): Int32 = n * n パブリック関数 プライベート関数 ⚠️ 未使用のままだとコンパイルエラー // // def double(n: Int32): Int32 = n * 2 } 20
コードの書き方(2): トップレベル関数による定数 // src/FirstFlixProject/SomeModule.flix pub mod FirstFlixProject.SomeModule { // 0 pub def a(): Int32 = 1 パブリックな 引数関数 プライベートな 引数関数 ⚠️ 未使用のままだとコンパイルエラー // 0 // def b(): Int32 = 2 } トップレベル変数は存在しない 21
コードの書き方(3): モジュールの利用
flix> use FirstFlixProject.{SimpleMath => M}; \
>
M.square(3)
9
flix> use FirstFlixProject.{SimpleMath => M}; \
>
List.map(M.square, List.range(0, 11))
0 :: 1 :: 4 :: 9 :: 16 :: 25 :: 36 :: 49 :: 64 :: 81 :: 100
:: Nil
flix> use FirstFlixProject.{SimpleMath => M}; \
>
List.range(0, 11) |> List.map(M.square)
0 :: 1 :: 4 :: 9 :: 16 :: 25 :: 36 :: 49 :: 64 :: 81 :: 100
:: Nil
は式の区切り文字
は での行継続
はパイプ(ライン)演算子
;
\ REPL
|>
22
コードの書き方(4): ローカル束縛
flix> \\
> use FirstFlixProject.{SimpleMath => M};
> let x = 5;
> let y = M.square(x);
> "square(${x}) = ${y}"
> \\
square(5) = 25
\\
はREPLでの複数行入力の開始/終了
23
コードの書き方(5): 無名関数(ラムダ式) flix> (n -> n * n * n)(2) 8 flix> List.map(n -> n * n * n, List.range(0, 11)) 0 :: 1 :: 8 :: 27 :: 64 :: 125 :: 216 :: 343 :: 512 :: 729 :: 1000 :: Nil 24
純粋)関数型のコード設計 4. ( 1. 2. 3. 4. 関数型プログラミング入門 関数型設計とコンビネータライブラリ 関数型設計に共通する構造 エフェクトとI/O 25
第1部 関数型プログラミング入門 関数型プログラミングとは で始める関数型プログラミング 関数型データ構造 例外によらないエラーハンドリング 正格性と遅延性 純粋関数型の状態 1. 2. Flix 3. 4. 5. 6. 26
第1章 関数型プログラミングとは 27
キーワード 関数型プログラミング(functional programming) 純粋関数(pure function) 副作用(side effect) 参照透過性(referential transparency) 純粋性(purity) 置換モデル(substitution model) 28
カフェでコーヒーを注文する過程を表す素朴なコード ⚠️ の かつ非 なコード // Scala OOP FP class Cafe: def buyCoffee(cc: CreditCard): Coffee = val cup = Coffee() cc.charge(cup.price) cup class CreditCard: def charge(price: Double): Unit = println("charging " + price) // class Coffee: val price: Double = 2.0 請求処理(簡単のため標準出力) 利用例 // scala> Cafe().buyCoffee(CreditCard()) charging 2.0 // val res0: Coffee = Coffee@353cb7e5 // 標準出力 戻り値 クレジットカードへの請求処理という副作用が埋め 込まれている 29
で典型的な改善策: インターフェースを導入してDI OOP ⚠️ の かつ非 なコード // Scala OOP FP class Cafe: def buyCoffee(cc: CreditCard, p: Payments): Coffee = val cup = Coffee() p.charge(cc, cup.price) cup // trait Payments: def charge(cc: CreditCard, price: Double): Unit // class SimulatedPayments extends Payments: def charge(cc: CreditCard, price: Double): Unit = println("charging " + price + " to " + cc) インターフェース定義 インターフェースに対する実装 副作用を含む実装を差し替えやすくなった しかし依然として: テストしやすいとまではいえない buyCoffee の再利用性は低い 30
での対応A: (非OOPLなので)振る舞いを関数で渡す Flix mod Coffee { // price Coffee pub enum Coffee({price = Float64}) // (ToString ) instance ToString[Coffee] { pub def toString(coffee: Coffee): String = let Coffee({price}) = coffee; "Coffee({price = ${price}})" } // ( ) pub def make(): Coffee = Coffee({price = 2.0}) } フィールドを持つレコードによる 文字列表現 型クラスの実装 型 コンストラクタ関数 ただの関数 mod CreditCard { // IO ( ) pub def charge(price: Float64): Unit \ IO = println("charging ${price}") } 金額を受け取って何も返さず、 エフェクト 副作用 を伴う関数 31
mod Cafe {
use FpInFlix.Exercises.Introduction.FirstExample.Coffee
//
IO
pub def buyCoffee(charge: Float64 -> Unit \ IO):
Coffee \ IO =
let coffee = Coffee.make();
let Coffee.Coffee({price}) = coffee;
charge(price);
coffee
}
請求関数を受け取ってコーヒーを返し、 エフェクトを伴う関数
利用例
//
flix> use FpInFlix.Exercises.Introduction.FirstExample.{Cafe,
CreditCard}; \ //
use
>
Cafe.buyCoffee(CreditCard.charge)
charging 2.0
//
Coffee({price = 2.0}) //
ℹ️ 以降はスペース節約のため を省略する
標準出力
戻り値
高階関数によるDI
高階関数は様々な設計パターンの基礎になる
32
での対応B: 振る舞いをエフェクトで表す Flix mod CreditCard { // ( ) CreditCard (Eq, ToString pub enum CreditCard(String) with Eq, ToString } // pub eff Payment { // def charge(cc: CreditCard, amount: Float64): Unit } 文字列 識別子 を持つ 型 は自動導出) 決済エフェクト クレジットカードと金額を受け取って何かする関数 mod Cafe { use FpInFlix.Exercises.Introduction.SecondExample.{ Coffee, CreditCard, Payment} // pub def buyCoffee(cc: CreditCard): Coffee \ Payment = let coffee = Coffee.make(); let Coffee.Coffee({price}) = coffee; Payment.charge(cc, price); // coffee } 請求関数を受け取ってコーヒーを返し、決済エフェクトを伴う関数 決済エフェクトの関数を利用 33
pub def example(): Unit \ IO =
let cup = run {
Cafe.buyCoffee(CreditCard.CreditCard("A"))
} with handler Payment { //
def charge(cc, amount, resume) = {
println("charging ${amount} to ${cc}");
resume() //
}
//
(
)
};
println("a cup of ${cup}")
決済エフェクトに対するハンドラー
エフェクト発生地点から続く処理を再開
継続を呼び出す
利用例
//
flix> example()
charging 2.0 to CreditCard(A)
a cup of Coffee({price = 2.0})
() //
戻り値
//
//
標準出力(Payment.charge)
標準出力(exampleの最後)
代数的エフェクトとエフェクトハンドラーによるDI
エフェクトシステムの活用例
34
での対応C: 副作用を完全に隔離する
Flix
mod Charge {
use FpInFlix.Exercises.Introduction.ThirdExample.
CreditCard
// creditCard, amount
Charge
pub enum Charge({
creditCard = CreditCard,
amount = Float64
})
//
(ToString
)
instance ToString[Charge] {
pub def toString(charge: Charge): String =
let Charge({creditCard, amount}) = charge;
"Charge({creditCard = ${creditCard}, amount =
${amount}})"
}
フィールドを持つレコードによる
文字列表現
型
型クラスの実装
「請求」をただのデータとして表す
解釈は利用側に委ねる
35
クレジットカードが同一の場合に限って つの請求をまとめる純粋関数
//
2
pub def combine(a: Charge, b: Charge): Result[String,
Charge] =
let Charge({creditCard=cardA, amount=amountA}) = a;
let Charge({creditCard=cardB, amount=amountB}) = b;
if (cardA == cardB) Ok(Charge({creditCard = cardA,
amount = amountA + amountB}))
else Err("Can't combine charges to different cards:
cardA=${cardA}, cardB=${cardB}")
請求リストをクレジットカードごとにまとめる純粋関数
//
pub def coalesce(charges: List[Charge]): Result[String,
List[Charge]] =
charges
|> List.groupBy(charge -> //
let Charge({creditCard | _}) = charge;
creditCard
)
|> Traversable.traverse(group -> // combine
Foldable.foldLeftM(combine, Nel.head(group),
Nel.tail(group)))
カードでグループ化
で集約
}
36
mod Cafe {
use FpInFlix.Exercises.Introduction.ThirdExample.
{Charge, Coffee, CreditCard}
//
pub def buyCoffee(cc: CreditCard): (Coffee, Charge) =
let coffee = Coffee.make();
let Coffee.Coffee({price}) = coffee;
let charge = Charge.Charge({creditCard = cc,
amount = price});
(coffee, charge)
カードを受け取ってコーヒーと請求を返す純粋関数
カードと個数を受け取ってコーヒーリストと請求を返す純粋関数
//
pub def buyCoffees(cc: CreditCard, n: Int32):
Result[String, (List[Coffee], Charge)] =
let (coffees, charges) = buyCoffee(cc)
|> List.repeat(n) |> List.unzip; // n
charges //
|> Foldable.foldLeftM(Charge.combine,
Charge.Charge({creditCard = cc,
amount = 0.0}))
|> Functor.map(charge -> (coffees, charge))
個繰り返し
コーヒーリストと請求のタプルに集約
}
37
利用例
//
flix> Cafe.buyCoffee(CreditCard.CreditCard("A"))
(Coffee({price = 2.0}), Charge({creditCard = CreditCard(A),
amount = 2.0})) //
戻り値
flix> Cafe.buyCoffees(CreditCard.CreditCard("A"), 3)
Ok((Coffee({price = 2.0}) :: Coffee({price = 2.0}) ::
Coffee({price = 2.0}) :: Nil, Charge({creditCard =
CreditCard(A), amount = 6.0}))) //
戻り値
データを変換する純粋関数
多様なデータを表す代数的データ型
List, Nel (non-empty list), Result, etc.
関数のパターンを抽象化した型クラス
Traversable, Foldable, Functor, etc.
38
第2章 Flixで始める関数型 プログラミング 39
キーワード 再帰関数(recursive function) 末尾再帰関数(tail-recursive function) 末尾呼び出し最適化/除去(tail-call optimization/elimination, TCO/TCE) (higher-order function) (polymorphic function) (monomorphic function) (parametric polymorphism) (type-driven development) 高階関数 多相関数 ↔ 単相関数 パラメータ多相 型駆動開発 40
ループ(繰り返し処理)したい ⇒ 再帰関数
関数型言語のチュートリアルでお馴染みの 階乗関数
末尾再帰であることを保証するアノテーション
末尾再帰関数 ループ相当に最適化される)
基底部
再帰部
// (
)
pub def factorial(n: Int32): Int32 =
@Tailrec //
def go(x, acc) = //
(
if (x <= 0) acc
//
else go(x - 1, x * acc); //
go(n, 1)
利用例
//
flix> List.map(factorial, List.range(0, 6))
1 :: 1 :: 2 :: 6 :: 24 :: 120 :: Nil
41
関数の振る舞いを切り替えたい ⇒ 高階関数
pub def formatAbs(x: Int32): String =
"The absolute value of ${x} is ${abs(x)}."
pub def formatFactorial(n: Int32): String =
"The factorial of ${n} is ${factorial(n)}."
//
pub def formatResult(name: String, n: Int32,
f: Int32 -> Int32): String =
"The ${name} of ${n} is ${f(n)}."
整数を受け取って整数を返す関数をパラメータ化して高階関数に
利用例
//
flix> formatResult("absolute value", -3, abs)
The absolute value of -3 is 3.
flix> formatResult("factorial", 5, factorial)
The factorial of 5 is 120.
42
任意の型で再利用したい ⇒ (パラメータ)多相関数 文字列のベクターをキーで線形探索してインデックスを返す関数 // pub def findFirst(key: String, ss: Vector[String]): Int32 = @Tailrec def loop(n) = if (n >= Vector.size(ss)) -1 else if (Vector.get(n, ss) == key) n else loop(n + 1); loop(0) 任意の要素型のベクターを述語関数で線形探索してインデックスを返す関数 // pub def findFirst(p: a -> Bool, xs: Vector[a]): Int32 = @Tailrec def loop(n) = if (n >= Vector.size(xs)) -1 else if (xs |> Vector.get(n) |> p) n else loop(n + 1); loop(0) 43
第3章 関数型データ構造 44
キーワード 代数的データ型(algebraic data type, ADT) 直積型(product type) 直和型(sum type) 再帰データ型(recursive data type) パターンマッチング(pattern matching) 網羅性(exhaustiveness) 純粋関数型データ型(purely functional data type), 永続データ型(persistent data type) 構造共有(structural sharing) 45
不変コレクションがほしい ⇒ 代数的データ型 リスト 単方向連結リスト // ( ) pub enum List[a] with Eq, ToString { // case Nil // ( case Cons(a, List[a]) // ( } // ( ) pub enum Tree[a] with Eq, ToString { case Leaf(a) // case Branch(Tree[a], Tree[a]) // } ツリー 二分木 Flix 等価判定、文字列表現付き 空の場合 基底部) 空でない場合 再帰部) 葉の場合(基底部) 枝の場合(再帰部) 標準ライブラリ: List, Nel, Vector, Chain, Nec Set, Map RedBlackTree 46
データを構造に沿って扱いたい ⇒ パターンマッチング リストの長さ // pub def length(l: List[a]): Int32 = match l { case Nil => 0 case Cons(_, xs) => 1 + length(xs) } // pub def size(t: Tree[a]): Int32 = match t { case Leaf(_) => 1 case Branch(left, right) => 1 + size(left) + size(right) } ツリーの要素数 利用例 // flix> length(Cons("a", Cons("b", Cons("c", Nil)))) 3 flix> size(Branch(Branch(Leaf("a"), Leaf("b")), Leaf("c"))) 5 47
データの操作パターンを抽象化したい ⇒ 高階関数 右から畳み込む関数 再帰構造に沿った集約関数 ⚠️ 愚直な実装では末尾再帰 スタックセーフにならない 工夫が必要 // ( ) // / ( ) pub def foldRight(f: (a, b) -> b \ ef, acc: b, l: List[a]): b \ ef = match l { case Nil => acc case Cons(x, xs) => f(x, foldRight(f, acc, xs)) } 利用例 // flix> foldRight((-), 0, Cons(1, Cons(2, Cons(3, Nil)))) 2 // (1 - (2 - (3 - 0))) // = (1 - (2 - 3)) // = (1 - (-1)) // = 2 48
左から畳み込む関数 末尾再帰の集約関数 末尾再帰 // ( ) @Tailrec // pub def foldLeft(f: (b, a) -> b \ ef, acc: b, l: List[a]): b \ ef = match l { case Nil => acc case Cons(x, xs) => foldLeft(f, f(acc, x), xs) } 利用例 // flix> foldLeft((-), 0, Cons(1, Cons(2, Cons(3, Nil)))) -6 // (((0 - 1) - 2) - 3) // = (((-1) - 2) - 3) // = ((-3) - 3) // = -6 ℹ️ 結合法則を満たさない(+ 単位元を持たない)演算 では foldRight と foldLeft の結果が異なる 49
リストを返す関数を各要素に適用して得られるリストを平坦化して返す関数 // pub def flatMap(f: a -> List[b] \ ef, l: List[a]): List[b] \ ef = // append: (List[a], List[a]) -> List[a] foldRight((x, acc) -> append(f(x), acc), Nil, l) ℹ️ は定義済みとする 利用例 // flix> \\ > flatMap( > x -> Cons(x, Cons(x + 1, Nil)), > Cons(1, Cons(2, Cons(3, Nil))) > ) > \\ Cons(1, Cons(2, Cons(2, Cons(3, Cons(3, Cons(4, Nil)))))) 50
関数を各要素に適用して得られるリストを返す関数 // pub def map(f: a -> b \ ef, l: List[a]): List[b] \ ef = flatMap(x -> Cons(f(x), Nil), l) 利用例 // flix> \\ > map( > x -> x * x * x, > Cons(1, Cons(2, Cons(3, Nil))) > ) > \\ Cons(1, Cons(8, Cons(27, Nil))) 51
述語を満たす要素のみのリストを返す関数 // pub def filter(p: a -> Bool \ ef, l: List[a]): List[a] \ ef = flatMap(x -> if (p(x)) Cons(x, Nil) else Nil, l) 利用例 // flix> \\ > filter( > // > x -> x `Int32.remainder` 2 != 0, > Cons(1, Cons(2, Cons(3, Nil))) > ) > \\ Cons(1, Cons(3, Nil)) ℹ️ バッククォートで括ると中置記法に 他にも多様な高階関数を定義できる 他のデータ構造についても同様に整備できる 52
第4章 例外によらないエラー ハンドリング 53
値の有無を区別したい ⇒ Option/Maybe pub enum Option[a] with Eq, ToString { case None // case Some(a) // } 値がない場合 値がある場合 例外は参照透過性を損なう副作用なので避ける 代わりに代数的データ型で表す ⇒ コレクションと同様に豊かな操作の対象に Flix標準ライブラリ: Option 54
処理の成否を区別したい ⇒ Either/Result pub enum Either[e, a] with Eq, ToString { case Left(e) // : case Right(a) // : } 失敗の場合 エラー値を持つ 成功の場合 成功値を持つ Flix 標準ライブラリ: Result 55
データの操作パターンを抽象化したい ⇒ 高階関数 pub def flatMap(f: a -> Option[b] \ ef, o: Option[a]): Option[b] \ ef = match o { case None => None case Some(x) => f(x) } pub def flatMap(f: a -> Either[e, b] \ ef, et: Either[e, a]): Either[e, b] \ ef = match et { case Left(x) => Left(x) case Right(y) => f(y) } 56
pub def map(f: a -> b \ ef, o: Option[a]): Option[b] \ ef = ??? pub def filter(p: a -> Bool \ ef, o: Option[a]): Option[a] \ ef = ??? 利用例 // flix> map(x -> x / 2, Some(42)) Some(21) flix> map(x -> x / 2, None) None flix> filter(x -> x `Int32.remainder` 2 != 0, Some(42)) None flix> filter(x -> x `Int32.remainder` 2 != 0, None) None 57
pub def map(f: a -> b \ ef, et: Either[e, a]): Either[e, b] \ ef = ??? // // ⚠️ Eitherのfilterは無条件には定義できない (述語を満たさない場合に採用するエラー値が定まらないため) 利用例 // flix> map(x -> x / 2, (Right(42): Either[String, Int32])) Right(21) flix> map(x -> x / 2, Left("falsity")) Left(falsity) 58
引数関数を 個の 値に適用して を返す関数 // 2 2 Option Option pub def map2(f: (a, b) -> c \ ef, o1: Option[a], o2: Option[b]): Option[c] \ ef = flatMap(x -> map(y -> f(x, y), o2), o1) 利用例 // flix> map2((+), Some(1), Some(2)) Some(3) flix> map2((+), None, Some(2)) None flix> map2((+), Some(1), None) None flix> map2(((+): (Int32, Int32) -> Int32), None, None) None Either についても同様に定義できる 59
を返す関数をリストの各要素に適用してリストの
を返す関数
// Option
Option
pub def traverse(f: a -> Option[b] \ ef, l: List[a]):
Option[List[b]] \ ef =
List.foldRight(
(x, acc) -> map2(List.cons, f(x), acc),
Some(Nil),
l
)
// Option
Option
pub def sequence(l: List[Option[a]]): Option[List[a]] =
traverse(identity, l)
のリストをリストの
にする関数
利用例 ℹ️ 標準ライブラリの
を利用
//
:
List
flix> traverse(x -> if (x `Int32.remainder` 2 != 0) Some(x)
else None, List#{1, 2, 3})
None
flix> traverse(x -> if (x `Int32.remainder` 2 != 0) Some(x)
else None, List#{1, 3, 5})
Some(1 :: 3 :: 5 :: Nil)
Either
についても同様に定義できる
60
エラー値を蓄積したい ⇒ Validated/Validation pub enum Validated[e, a] with Eq, ToString { case Valid(a) // : case Invalid(Nel[e]) // : } 有効な場合 有効値を持つ 無効な場合 エラー値の非空リストを持つ 標準ライブラリ: Validation ℹ️ 性能特性から Nel (non-empty list)ではなく Nec (non-empty chain)が採用されている Flix 61
第5章 正格性と遅延性 62
キーワード 非正格性(nonstrictness)、非正格評価(nonstrict evaluation) ↔ 正格性(strictness)、正格評価(strict evaluation)/先行評価(eager evaluation) 遅延性(laziness)、遅延評価(lazy evaluation) 計算の記述(description)と評価(evaluation)の分離 余再帰関数(corecursive function) 63
必要になるまで実体化されないデータ構造がほしい ⇒ 代数的データ型 + 評価を制御する工夫 言語によっては と呼ばれる は遅延評価される式を表す の組み込み型 pub enum LazyList[a] { // Stream case Empty // Lazy Flix case Cons(Lazy[a], Lazy[LazyList[a]]) } ℹ️ pub def take(n: Int32, ll: LazyList[a]): LazyList[a] = match ll { case Cons(head, tail) if n > 1 => // lazy // force Cons(head, lazy take(n - 1, force tail)) case Cons(head, _) if n == 1 => Cons(head, lazy Empty) case _ => Empty } ℹ️ Flix は直後の式を遅延評価されるようにし、 は直後の遅延評価式を実体化する 標準ライブラリ: DelayList, DelayMap 64
からの整数列 // n pub def intsFrom(n: Int32): LazyList[Int32] = Cons(lazy n, lazy intsFrom(n + 1)) // pub def fibs(): LazyList[BigInt] = def go(current, next) = Cons(lazy current, lazy go(next, current + next)); go(0ii, 1ii) フィボナッチ数列 利用例 // flix> LazyList.intsFrom(0) |> LazyList.map(x -> x * x) |> LazyList.take(5) |> LazyList.toList 0 :: 1 :: 4 :: 9 :: 16 :: Nil flix> LazyList.fibs() |> LazyList.take(10) |> LazyList.toList 0 :: 1 :: 1 :: 2 :: 3 :: 5 :: 8 :: 13 :: 21 :: 34 :: Nil 65
第6章 純粋関数型の状態 66
擬似乱数を生成したい ⇒ 状態を受け渡す純粋関数 シード値を持つ を受け取って整数と新たな を返す関数 乱数生成器 型 // Rng (random number generator, ) pub enum Rng(Int64) with Eq, ToString // Rng Rng pub def nextInt(rng: Rng): (Int32, Rng) = let Rng(seed) = rng; // let newSeed = Int64.bitwiseAnd( seed * 25214903917i64 + 11i64, 281474976710655i64); let nextRng = Rng(newSeed); let n = Int64.rightShift(newSeed, 16) |> Int64.truncateToInt32; (n, nextRng) // Rng Rng pub type alias Rand[a] = Rng -> (a, Rng) 線形合同法による乱数生成 を受け取って任意のランダム生成値と新たな を返す関数の型エイリアス Flix 標準ライブラリ: Math.Random (エフェクト) 67
自然に定義できるユーティリティ関数の例
//
pub def unit(a: a): Rand[a] = ???
pub def map(f: a -> b, ra: Rand[a]): Rand[b] = ???
pub def map2(f: (a, b) -> c, ra: Rand[a], rb: Rand[b]):
Rand[c] = ???
pub def flatMap(f: a -> Rand[b], ra: Rand[a]): Rand[b] = ???
//
pub def int(): Rand[Int32] = rng -> nextInt(rng)
pub def boolean(): Rand[Bool] =
rng ->
let (i, rng2) = nextInt(rng);
(i `Int32.remainder` 2 == 0, rng2)
ランダム関数の例
利用例
//
flix> map2((i, b) -> "int: ${i}, boolean: ${b}", int(),
boolean())(Rng(42i64))
(int: 16159453, boolean: false, Rng(197491923327988))
flix> map2((i, b) -> "int: ${i}, boolean: ${b}", int(),
boolean())(Rng(197491923327988i64))
(int: -340305902, boolean: true, Rng(149370390209998))
68
第2部 関数型設計とコンビネータ ライブラリ 7. 8. 9. 純粋関数型の並列処理 プロパティベーステスト パーサーコンビネータ 69
第7章 純粋関数型の並列処理 70
並列/並行処理したい ⇒ 処理の記述と実行の分離 マルチスレッド実行管理オブジェクトを受け取り非同期処理の結果を返す関数 // enum Par[a](ExecutorService -> Future[a] \ IO) pub def unit(a: a): Par[a] = Par(_es -> /* ExecutorService a pub def fork(pa: Lazy[Par[a]]): Par[a] = Par(es -> /* ExecutorService pub def runPar(es: ExecutorService, pa: Par[a]): Future[a] \ IO = let Par(f) = pa; f(es) を利用せず直ちに を返す */) を利用して非同期的にタスク実行 */) 標準機能: par-yield 構文による並列処理 (Go言語に似た)軽量スレッドと Channel による CSPベースの並行処理 Flix 71
自然に定義できるユーティリティ関数の例
//
pub def flatMap(f: a -> Par[b], pa: Par[a]): Par[b] = ???
pub def map2(f: (a, b) -> c, pa: Par[a], pb: Par[b]):
Par[c] = ???
pub def map(f: a -> b, pa: Par[a]): Par[b] = ???
//
pub def runWithThreadPool(pa: Par[a]): a \ IO =
let es = Executors.newFixedThreadPool(4);
let v = runPar(es, pa).get();
es.shutdown();
v
並列処理を実行する関数の例
利用例
//
flix> map2((a, b) -> a * b, unit(2), unit(3))
|> runWithThreadPool
6
flix> map2((a, b) -> a ++ b, unit(List#{1, 2}),
unit(List#{3, 4, 5})) |> runWithThreadPool
1 :: 2 :: 3 :: 4 :: 5 :: Nil
72
第8章 プロパティベーステスト 73
プロパティベーステスト(PBT)ライブラリがほしい ⇒ 状態を受け渡す純粋関数 + 処理の記述と実行の分離 のジェネレータ を受け取って と新たな を返す関数 // PBT : Rng a Rng enum Gen[a](Rng -> (a, Rng)) pub def map(f: a -> b, ga: Gen[a]): Gen[b] = ??? pub def map2(f: (a, b) -> c, ga: Gen[a], gb: Gen[b]): Gen[c] = ??? pub def flatMap(f: a -> Gen[b], ga: Gen[a]): Gen[b] = ??? // (sized) mod SGen { pub enum SGen[a](Int32 -> Gen[a]) pub def map(f: a -> b, sga: SGen[a]): SGen[b] = ??? pub def flatMap(f: a -> SGen[b], sga: SGen[a]): SGen[b] = ??? } サイズ指定付き のジェネレータ 具体的なジェネレータの例 // pub def boolean(): Gen[Bool] = ??? pub def int(): Gen[Int32] = ??? pub def list(ga: Gen[a]): SGen[List[a]] = ??? 74
のプロパティ テスト対象の性質
// PBT
(
)
enum Prop((MaxSize, TestCases, Rng) -> Result.Result)
pub def forAll(ga: Gen[a], f: a -> Bool):
Prop with ToString[a] = ???
pub def forAllSized(sga: Gen.SGen[a], f: a -> Bool):
Prop with ToString[a] = ???
pub def &&(p1: Prop, p2: Prop): Prop = ???
pub def ||(p1: Prop, p2: Prop): Prop = ???
pub def runProp(p: Prop): Unit \ {IO, Time.Clock} = ???
//
利用例
回反転したリストは元のリストに一致する
// 2
flix> forAllSized(list(int()), xs ->
xs |> List.reverse |> List.reverse == xs) \
//
>
&& forAllSized(list(int()), xs ->
List.head(xs) == xs |> List.reverse |> List.last) \
>
|> runProp
+ OK, passed 100 tests.
()
リストの先頭要素は反転したリストの最終要素に一致する
75
第9章 パーサーコンビネータ 76
パーサー(構文解析器)がほしい ⇒ 純粋関数の合成
パーサーを表すトレイト
型クラス
//
(a.k.a.
)
pub trait Parsers[parser: Type -> Type] {
pub def string(s: String): parser[String]
pub def char(c: Char): parser[Char]
pub def regex(r: Regex): parser[String]
pub def runParser(input: String, p: parser[a]):
Result[ParseError, a]
pub def map(f: a -> b, p: parser[a]): parser[b]
pub def map2(f: (a, b) -> c, p1: parser[a],
p2: Lazy[parser[b]]): parser[c]
pub def flatMap(f: a -> parser[b], p: parser[a]):
parser[b]
...
}
トレイトの実装例
//
pub enum Parser[a](Location -> ParseResult[a])
instance Parsers[Parser] {
pub def string(s: String): Parser[String] = ???
...
}
77
パーサーの例 非負整数の直後にその数だけ が繰り返す文字列
//
:
a
pub def nConsecutiveAs(): parser[Int32]
with Parsers[parser] =
Parsers.flatMap(n ->
Parsers.map(_ ->
n, // n
+
a
Parsers.listOfN(n, Parsers.char('a'))
), //
nonNegativeInt()
)
回の繰り返し 文字 のパーサー
非負整数のパーサー
利用例
//
flix> (nConsecutiveAs(): Parser[Int32])
|> Parsers.runParser("12aaaaaaaaaaaab")
Ok(12)
flix> (nConsecutiveAs(): Parser[Int32])
|> Parsers.runParser("12aaabaaaaaaaab")
Err(1.6 'a' // 1 6
a
行 文字目で文字 のパースエラー
12aaabaaaaaaaa
^)
78
第3部 関数型設計に共通する構造 10. 11. 12. モノイド モナド アプリカティブ/トラバーサブルファンクター 79
第10章 モノイド 80
キーワード 代数的構造(algebraic structure) モノイド(monoid) 結合法則/結合律(associativity, associative law) 単位元(identity element)、単位律(identity law) → モノイド則(monoid laws) 型クラス(type class) アドホック多相(ad-hoc polymorphism) Foldable 型クラス 高カインド型(higher-kinded type, HKT) 81
型クラスによるアドホック多相 (型によって異なる振る舞いを与える多相) 型クラスの定義 共通のインターフェース // : pub trait HasSize[a] { pub def size(x: a): Int32 } 型クラスのインスタンス 型ごとの実装 // : instance HasSize[String] { pub def size(x: String): Int32 = String.length(x) } instance HasSize[List[a]] { pub def size(x: List[a]): Int32 = List.size(x) } 第 章で定義した 二分木) instance HasSize[Tree[a]] { // 3 Tree ( pub def size(x: Tree[a]): Int32 = Tree.size(x) } 82
利用例
//
flix> HasSize.size("foo bar")
7
flix> HasSize.size(List#{'a', 'b', 'c'})
3
flix> HasSize.size(Branch(Leaf(1), Branch(Leaf(2), Leaf(3))))
5
パラメータ多相
型にかかわらず同じ振る舞いを可能とする多相)
Eq や ToString はFlix標準の型クラスの例
cf.
(
83
型クラス Monoid (モノイド):
結合法則を満たす + 単位元が存在する
定義
//
pub trait Monoid[a] {
//
2
a × a
pub def combine(a1: a, a2: a): a //
mappend
//
e
pub def empty(): a
//
mempty
}
//
(
)
pub def combineAll(xs: List[a]): a with Monoid[a] =
List.foldLeft(Monoid.combine, Monoid.empty(), xs)
pub def foldMap(f: a -> b, xs: List[a]): b with Monoid[b] =
List.foldLeft((b, a) -> Monoid.combine(b, f(a)),
Monoid.empty(), xs)
結合法則を満たす 項演算
単位元
派生的 定義から導出可能 な関数
Flix
別名
別名
標準ライブラリ: Monoid
84
Monoid
の具体例
文字列 リストの連結と空文字列 リスト
//
/
/
flix> Monoid.combineAll(List#{"alpha", "beta", "gamma"})
alphabetagamma
flix> Monoid.combineAll(List#{List#{2}, List#{3}, List#{5}})
2 :: 3 :: 5 :: Nil
数の和と ※ここでは、
をラップした型
数の積と ※ここでは、
をラップした型
//
0
Int32
enum SumInt(Int32)
flix> Monoid.foldMap(SumInt, List#{2, 3, 4})
SumInt(9)
//
1
Int32
enum ProductInt(Int32)
flix> Monoid.foldMap(ProductInt, List#{2, 3, 4})
ProductInt(24)
論理和と
ここでは、
をラップした型
論理積と
ここでは、
をラップした型
//
false ※
Bool
enum Or(Bool)
flix> Monoid.foldMap(Or, List#{true, false, true})
Or(true)
//
true ※
Bool
enum And(Bool)
flix> Monoid.foldMap(And, List#{true, false, true})
And(false)
85
型クラス Foldable: fold (畳み込み)できる
は高カインド型
pub trait Foldable[f: Type -> Type] { // f
//
1
pub def foldRight(f: (a, b) -> b, acc: b, fa: f[a]): b
pub def foldLeft(f: (b, a) -> b, acc: b, fa: f[a]): b
pub def foldMap(f: a -> b, fa: f[a]): b with Monoid[b]
//
(
)
pub def combineAll(fa: f[a]): a with Monoid[a]
pub def toList(fa: f[a]): List[a]
}
定義 ※いずれか つの関数から他は導出可能
派生的 定義から導出可能 な関数
利用例
//
flix> foldMap(constant(SumInt(1)), List#{'a', 'b', 'c'})
SumInt(3)
flix> foldLeft((_, x) -> "value: ${x}", "nothing", Some(42))
value: 42
Flix
標準ライブラリ: Foldable
86
第11章 モナド 87
キーワード ファンクター/関手(functor) 恒等(identity) 合成(composition) → ファンクター則(functor laws) モナド(monad): ファンクターの特殊化 結合法則/結合律(associativity, associative law) 左単位律(left identity law) 右単位律(right identity law) → モナド則(monad laws) 88
型クラス Functor: map (文脈を保って写像)できる
定義
//
pub trait Functor[f: Type -> Type] {
pub def map(f: a -> b, fa: f[a]): f[b]
}
//
別名fmap
インスタンスの例 これまでに を実装したすべての型を含む
//
:
map
instance Functor[List] {
pub def map(f: a -> b, fa: List[a]): List[b] =
List.map(f, fa)
}
利用例
//
flix> Functor.map(x -> x * x * x, List#{1, 2, 3})
1 :: 8 :: 27 :: Nil
Flix
標準ライブラリ: Functor
89
型クラス Monad: flatMap (文脈を引き継いで連鎖) できる pub trait Monad[f: Type -> Type] with Functor[f] { // flatMap compose map join pub def unit(a: a): f[a] // return pub def flatMap(f: a -> f[b], fa: f[a]): f[b] // >>= // ( ) pub def compose(f: a -> f[b], g: b -> f[c]): a -> f[c] pub def join(ffa: f[f[a]]): f[a] pub def map2(f: (a, b) -> c, fa: f[a], fb: f[b]): f[c] pub def sequence(fas: List[f[a]]): f[List[a]] pub def traverse(f: a -> f[b], xs: List[a]): f[List[b]] pub def replicateM(n: Int32, fa: f[a]): f[List[a]] } 定義 ※ は もしくは と 別名 派生的 定義から導出可能 な関数 Flix からも導出可能 別名 標準ライブラリ: Monad 90
Monad
の具体例
インスタンスの例 これまでに
を実装したすべての型を含む
状態を関数で引き回す型 第 章の乱数生成器の一般化)
//
:
flatMap
pub mod State { //
( 6
pub enum State[s, a](s -> (a, s))
pub def map(f: a -> b, sa: State[s, a]):
State[s, b] = ???
pub def unit(a: a): State[s, a] = ???
pub def flatMap(f: a -> State[s, b], sa: State[s, a]):
State[s, b] = ???
pub def get(): State[s, s] = ???
pub def set(s: s): State[s, Unit] = ???
pub def modify(f: s -> s): State[s, Unit] = ???
pub def runState(s0: s, sa: State[s, a]): (a, s) = ???
}
の実装
instance Functor[State[s]] { /* map
*/ }
instance Monad[State[s]] {
pub def unit(a: a): State[s, a] = State.unit(a)
pub redef flatMap(f: a -> State[s, b], fa: State[s, a]):
State[s, b] = State.flatMap(f, fa)
}
91
モナドを活用した関数の例
// State
pub def zipWithIndex(xs: List[a]): List[(Int32, a)] =
List.foldLeft((acc, x) ->
Monad.flatMap(ys ->
Monad.flatMap(n ->
Functor.map(_ ->
(n, x) :: ys,
State.set(n + 1) //
), State.get()
//
), acc
), Monad.unit(Nil), xs
) // => State[Int32, List[(Int32, a)]]
|> State.runState(0) // => (List[(Int32, a)], Int32)
|> fst
|> List.reverse
状態を更新
状態を取得
利用例
//
flix> zipWithIndex(List#{'a', 'b', 'c'})
(0, a) :: (1, b) :: (2, c) :: Nil
92
標準の Monad に対する糖衣構文 forM Flix flix> \\ > Monad.flatMap(x -> > Functor.map(y -> > x * y, > Some(3) > ), > Some(2) > ) > \\ Some(6) flix> \\ > forM( > x <- Some(2); > y <- Some(3) > ) yield x * y > \\ // => Option[Int32] Some(6) の 式、Haskellのdo記法 cf. Scala for 93
が
標準の
のインスタンスなら、以下のように書ける
// State Flix
Monad
pub def zipWithIndex(xs: List[a]): List[(Int32, a)] =
List.foldLeft((acc, x) ->
forM(
ys <- acc;
n <- State.get();
_ <- State.set(n + 1)
) yield (n, x) :: ys,
Applicative.point(Nil), xs
)
|> State.runState(0)
|> fst
|> List.reverse
94
第12章 アプリカティブ/ トラバーサブルファンクター 95
キーワード アプリカティブファンクター(applicative functor): ファンクターの特殊化、モナドの一般化 左単位律(left identity law) 右単位律(right identity law) 結合法則/結合律(associativity, associative law) 自然(naturality) → アプリカティブ則(applicative laws) 半群(semigroup): モノイドの一般化 トラバーサブルファンクター(traversable functor): ファンクターとFoldableの特殊化 モナドトランスフォーマー/モナド変換子(monad transformer) 96
型クラス Applicative: apply (多引数関数を適用) できる pub trait Applicative[f: Type -> Type] with Functor[f] { // apply map2 pub def unit(a: a): f[a] // point, pure pub def apply(fab: f[a -> b], fa: f[a]): f[b] // ap // ( ) pub def map2(f: (a, b) -> c, fa: f[a], fb: f[b]): f[c] pub def sequence(fas: List[f[a]]): f[List[a]] pub def traverse(f: a -> f[b], xs: List[a]): f[List[b]] pub def replicateM(n: Int32, fa: f[a]): f[List[a]] pub def product(fa: f[a], fb: f[b]): f[(a, b)] } 定義 ※ は からも導出可能 別名 派生的 定義から導出可能 な関数 別名 まで特殊化せず様々な関数が定義できる Flix標準ライブラリ: Applicative Monad 97
Applicative の特殊化として再構成した Monad pub trait Monad[f: Type -> Type] with Applicative[f] { // flatMap compose map join pub def flatMap(f: a -> f[b], fa: f[a]): f[b] // ( ) pub def compose(f: a -> f[b], g: b -> f[c]): a -> f[c] pub def join(ffa: f[f[a]]): f[a] } 定義 ※ は もしくは と 派生的 定義から導出可能 な関数 からも導出可能 特殊化の流れ: Functor → Applicative → Monad 98
Applicative
インスタンスの例 これまでに
の具体例
を実装したすべての型を含む
の実装
無限に続く の遅延リスト
//
:
map2
pub enum ZipList[a](DelayList[a]) with Eq, ToString
instance Functor[ZipList] { /* map
*/ }
instance Applicative[ZipList] {
pub def unit(a: a): ZipList[a] = //
a
DelayList.repeat(a) |> fromDelayList
pub redef map2(f: (a, b) -> c, fa: ZipList[a],
fb: ZipList[b]): ZipList[c] =
DelayList.zip(toDelayList(fa), toDelayList(fb))
|> DelayList.map(t -> let (a, b) = t; f(a, b))
|> fromDelayList
}
利用例
//
flix> Applicative.map2((a, b) -> (a, b),
>
fromDelayList(DelayList.startFrom(0)),
>
Applicative.unit('α')
>
) |> toDelayList |> DelayList.take(5)
DelayList((0, α), (1, α), (2, α), (3, α), (4, α))
99
標準の Applicative に対する糖衣構文 forA
Flix
flix> \\
> Applicative.map3(
>
(x, y, z) -> List#{x, y, z},
>
Validation.Failure(Nec.singleton("x not found")),
>
Validation.Success(3),
>
Validation.Failure(Nec.singleton("z not found"))
> )
> \\ // => Validation[String, List[Int32]]
Failure(Nec#{x not found, z not found})
flix> \\
> forA(
>
x <- Validation.Failure(Nec.singleton("x not found"));
>
y <- Validation.Success(3);
>
z <- Validation.Failure(Nec.singleton("z not found"))
> ) yield List#{x, y, z}
> \\
Failure(Nec#{x not found, z not found})
の
拡張ApplicativeDo
cf. Haskell GHC
100
型クラス Semigroup: 結合法則を満たす pub trait Semigroup[a] { pub def combine(a1: a, a2: a): a } // Semigroup Monoid pub trait Monoid[a] with Semigroup[a] { pub def empty(): a } の特殊化として再構成した だが ではない例 非空リスト // Semigroup Monoid : pub enum NonEmptyList[a](a, List[a]) with Eq, ToString instance Semigroup[NonEmptyList[a]] { pub def combine(a1: NonEmptyList[a], a2: NonEmptyList[a]): NonEmptyList[a] = let NonEmptyList(head1, tail1) = a1; let NonEmptyList(head2, tail2) = a2; NonEmptyList(head1, tail1 ++ (head2 :: tail2)) } Flix 標準ライブラリ: SemiGroup 101
型クラス Traversable: traverse (走査)できる ℹ️ 書籍の コードでは事情により と命名されている // Scala Traverse pub trait Traversable[f: Type -> Type] with Functor[f], Foldable[f] { // traverse map sequence pub def traverse(f: a -> g[b], fa: f[a]): g[f[b]] with Applicative[g] // ( ) pub def sequence(fga: f[g[a]]): g[f[a]] with Applicative[g] pub def mapAccum(f: (a, s) -> (b, s), s: s, fa: f[a]): (f[b], s) } 定義 ※ は と からも導出可能 派生的 定義から導出可能 な関数 Flix 標準ライブラリ: Traversable 102
の実装例 文脈を保って写像しつつ モナドで状態を読み書き // mapAccum : State pub def mapAccum(f: (a, s) -> (b, s), s: s, fa: f[a]): (f[b], s) = traverse(a -> Monad.flatMap(s1 -> let (b, s2) = f(a, s1); Functor.map(_ -> b, State.set(s2) ), State.get() ), fa ) |> State.runState(s) で実装できる関数の例 // mapAccum pub def zipWithIndex(fa: f[a]): f[(a, Int32)] with Traversable[f] = fa |> Traversable.mapAccum((a, s) -> ((a, s), s + 1), 0) |> fst 103
の具体例
Traversable
インスタンスの例 これまでに
を実装したすべての型を含む
//
:
traverse
instance Traversable[List] { ... }
instance Traversable[Option] { ... }
instance Traversable[Map[k]] with Order[k] { ... }
利用例
リストの要素がインデックス付きに
//
//
flix> zipWithIndex(List#{'a', 'b', 'c'})
(a, 0) :: (b, 1) :: (c, 2) :: Nil
の
の値がインデックス付きに
// Option Some
flix> zipWithIndex(Some(42))
Some((42, 0))
マップのエントリーの値がインデックス付きに
//
flix> zipWithIndex(Map#{'α' => 'a', 'β' => 'b', 'γ' => 'g'})
Map#{α => (a, 0), β => (b, 1), γ => (g, 2)}
104
13. 14. 15. 第4部 エフェクトとI/O 外部エフェクトとI/O 局所エフェクトと可変状態 ストリーム処理とインクリメンタルI/O 105
第13章 外部エフェクトとI/O 106
キーワード エフェクト/作用(effect) 副作用(side effect) 埋め込み/内部DSL (embedded/internal DSL) IO モナド Free モナド cf. 代数的エフェクト(algebraic effect) トランポリン(trampolining) tagless final 107
副作用が発生しうるコードの表現と解釈を分離したい
⇒ Free モナドによるDSL
ℹ️ 書籍の
コードとは異なり
標準ライブラリの定義を採用した
//
Scala
Haskell
mod Free {
pub enum Free[f: Type -> Type, a: Type] {
case Pure(a)
//
:
case Free(f[Free[f, a]]) //
:
}
// f Functor
Free[f] Monad
instance Functor[Free[f]] with Functor[f] { ... }
instance Applicative[Free[f]] with Functor[f] { ... }
instance Monad[Free[f]] with Functor[f] { ... }
// f[a] Free[f, a]
pub def liftF(fa: f[a]): Free[f, a] with Functor[f] =
Free(Functor.map(a -> Pure(a), fa))
}
が
なら
を
は
基底部 計算の結果
再帰部 計算の継続
になる
に持ち上げるユーティリティ
ℹ️ 関数型のDSL実装手法は Free によるもの以外に
もいろいろある
108
Free モナドによるDSLの例: コンソール入出力 pub enum Console[next] { case ReadLine(Option[String] -> next) // case PrintLine(String, next) // } // Console Functor Free[Console] Monad instance Functor[Console] { pub def map(f: a -> b \ ef, fa: Console[a]): Console[b] \ ef = match fa { case ReadLine(g) => ReadLine(s -> // unchecked_cast(f(g(s)) as _ \ {})) case PrintLine(s, next) => PrintLine(s, f(next)) } } を とすれば 文字列の入力 文字列の出力 は自動的に に ⚠️ エフェクトを含む型と噛み合わずキャストが必要 109
コンソール入出力DSLの具体例
の構成要素
// DSL
pub def readLn(): Free[Console, Option[String]] =
Free.liftF(ReadLine(identity))
pub def printLn(line: String): Free[Console, Unit] =
Free.liftF(PrintLine(line, ()))
でのコード表現の例
// DSL
pub def exampleExpr(): Free[Console, Unit] =
forM( // Free[Console] Monad
forM
_ <- printLn("Enter a temperature in °F: ");
d <- readLn() |> Functor.map(Option.flatMap(
Float64.fromString));
_ <- printLn("In °C: ${Option.map(fToC, d)}")
) yield ()
//
def fToC(f: Float64): Float64 =
(f - 32.0) * 5.0 / 9.0
は
なので糖衣構文
も使える
華氏から摂氏への換算関数
110
に対するインタープリタA (副作用あり)
DSL
pub def interpretWithSysConsole(expr: Free[Console, a]):
a \ Sys.Console =
match expr {
case Free.Pure(a) => a
case Free.Free(ReadLine(next)) =>
interpretWithSysConsole(next(Sys.Console.readln()))
case Free.Free(PrintLine(line, next)) =>
Sys.Console.println(line);
interpretWithSysConsole(next)
}
利用例
//
flix> interpretWithSysConsole(exampleExpr())
Enter a temperature in °F:
77 //
In °C: Some(25.0)
()
ユーザー入力
111
に対するインタープリタB (副作用なし)
DSL
pub def interpretWithBuffer(in: List[String],
expr: Free[Console, a]): (a, List[String]) =
def loop(x, inBuf, outBuf) =
match x {
case Free.Pure(a) => (a, List.reverse(outBuf))
case Free.Free(ReadLine(next)) =>
match inBuf {
case Nil => loop(next(None), Nil, outBuf)
case s :: ss =>
loop(next(Some(s)), ss, outBuf)
}
case Free.Free(PrintLine(line, next)) =>
loop(next, inBuf, line :: outBuf)
};
loop(expr, in, Nil)
利用例
//
flix> interpretWithBuffer(List#{"77"}, exampleExpr())
((), Enter a temperature in °F: :: In °C: Some(25.0) :: Nil)
112
らしい別解: 代数的エフェクト
Flix
pub eff Console {
def readLn(): Option[String]
def printLn(line: String): Unit
}
//
//
文字列の入力
文字列の出力
エフェクトを活用した関数の例
// Console
pub def exampleFn(): Unit \ Console =
Console.printLn("Enter a temperature in °F: ");
let d = Console.readLn() |> Option.flatMap(
Float64.fromString);
Console.printLn("In °C: ${Option.map(fToC, d)}")
Flix
標準ライブラリ: Sys.Console
113
エフェクトハンドラーA (副作用あり)
pub def runWithSysConsole(f: Unit -> a \ ef):
a \ ef - Console + Sys.Console =
run {
f()
} with handler Console {
def readLn(resume) =
resume(Sys.Console.readln())
def printLn(line, resume) = {
Sys.Console.println(line);
resume()
}
}
利用例
//
flix> run { exampleFn() } with runWithSysConsole
Enter a temperature in °F:
77 //
In °C: Some(25.0)
()
ユーザー入力
114
エフェクトハンドラーB (副作用なし)
pub def runWithBuffer(in: List[String], f: Unit -> a \ ef):
(a, List[String]) \ ef - Console =
region rc {
let inBuf = Ref.fresh(rc, in);
let outBuf = Ref.fresh(rc, Nil);
let a = run {
f()
} with handler Console {
def readLn(resume) = ???
def printLn(line, resume) = ???
};
(a, outBuf |> Ref.get |> List.reverse)
}
利用例
//
flix> run { exampleFn() } with runWithBuffer(List#{"77"})
((), Enter a temperature in °F: :: In °C: Some(25.0) :: Nil)
115
第14章 局所エフェクトと可変状態 116
局所的に可変性がほしい ⇒ ST モナド
mod St {
enum St[s, a](s -> (a, s))
instance Functor[St[s]] { ... }
instance Applicative[St[s]] { ... }
instance Monad[St[s]] { .... }
//
st: forall s. Unit -> St[s, a]
pub def runSt(st: Unit -> St[s, a]): a = ???
}
⚠️
のようには書けない
モナドで局所化された可変参照
では可変参照を扱うと必ず
mod StRef { // ST
//
Flix
r: Region
struct StRef[_s: Type, a: Type, r: Region] {
mut cell: a
}
//
pub def make(rc: Region[r], a: a): St[s, StRef[s, a, r]]
\ r = ???
pub def read(r: StRef[s, a, r]): St[s, a] \ r = ???
pub def write(a: Lazy[a], r: StRef[s, a, r]): St[s, Unit]
\ r = ???
}
⚠️
が現れる
⚠️ エフェクトを含む型と噛み合わずキャストを多用することになる
117
モナドで局所化された可変配列 では 可変 配列を扱うと必ず mod StArray { // ST // Flix ( ) r: Region struct StArray[_s: Type, a: Type, r: Region] { mut value: Array[a, r] } // pub def fromList(rc: Region[r], xs: List[a]): St[s, StArray[s, a, r]] \ r = ??? pub def read(i: Int32, arr: StArray[s, a, r]): St[s, a] \ r = ??? pub def write(i: Int32, a: a, arr: StArray[s, a, r]): St[s, Unit] \ r = ??? ... } ⚠️ が現れる ⚠️ エフェクトを含む型と噛み合わずキャストを多用することになる 少なくともFlixには適さないアプローチ エフェクトシステムでのより良い方法がある 118
らしい別解: リージョン(region)機構 Flix リージョンで局所化された可変参照 mod MyRef { // struct MyRef[a: Type, r: Region] { mut cell: a } pub def make(rc: Region[r], cell: a): MyRef[a, r] \ r = new MyRef @ rc {cell = cell} pub def read(r: MyRef[a, r]): a \ r = r->cell pub def write(a: a, r: MyRef[a, r]): Unit \ r = r->cell = a } Flix 標準ライブラリ: Ref 119
リージョンで局所化された可変配列
mod MyArray { //
struct MyArray[a: Type, r: Region] {
mut value: Array[a, r]
}
pub def fromList(rc: Region[r], xs: List[a]):
MyArray[a, r] \ r =
new MyArray @ rc {value = List.toArray(rc, xs)}
pub def read(i: Int32, arr: MyArray[a, r]): a \ r =
Array.get(i, arr->value)
pub def write(i: Int32, a: a, arr: MyArray[a, r]):
Unit \ r =
Array.put(a, i, arr->value)
...
}
Flix
標準ライブラリ: Array
120
局所的な可変性を活用した純粋関数の例
//
pub def quickSort(xs: List[Int32]): List[Int32] =
if (List.isEmpty(xs)) xs
else region rc { //
let arr = MyArray.fromList(rc, xs); //
qs(rc, 0, MyArray.size(arr) - 1, arr);
MyArray.freeze(arr)
//
}
可変性を閉じ込めるリージョンブロック
リストを配列化
配列をリスト化
def qs(rc: Region[r], l: Int32, r: Int32,
arr: MyArray[Int32, r]): Unit \ r = //
if (l < r)
let pi = partition(rc, l, r, l + (r - l) / 2, arr);
qs(rc, l, pi - 1, arr); qs(rc, pi + 1, r, arr)
else ()
def partition(rc: Region[r], l: Int32, r: Int32,
pivot: Int32, arr: MyArray[Int32, r]): Int32 \ r =
let vp = MyArray.read(pivot, arr);
MyArray.swap(pivot, r, arr);
let j = MyRef.make(rc, l);
Range.forEach(i ->
//
……
リージョンエフェクト付き
以下に実装が続く
121
第15章 ストリーム処理と インクリメンタルI/O 122
外部リソースに安全に宣言的にアクセスしたい ⇒ ストリーム変換 ℹ️ 書籍では の 類似の ストリーム処理ライブラリを実装している ストリームの文字列を華氏温度と解釈して摂氏の数値に換算する // Scala (FS2 ) // def conversion(iter: Iterator[String, ef, r]): Iterator[Float64, ef, r] = iter |> Iterator.map(String.trim) |> Iterator.filter(s -> not String.isEmpty(s)) |> Iterator.filter(s -> not String.startsWith(prefix = "#", s) ) |> Iterator.filterMap(Float64.fromString) |> Iterator.map(toCelsius) def toCelsius(fahrenheit: Float64): Float64 = (5.0 / 9.0) * (fahrenheit - 32.0) Flix 標準ライブラリ: Iterator 123
ファイルI/Oユーティリティ
ℹ️ 現時点のFlix標準ライブラリには見当たらないためJava APIで自作
// インクリメンタルに行を読み出すイテレータ
//
//
def lineIterator(rc: Region[r], reader: BufferedReader):
Iterator[String, r + IO, r] \ r =
Iterator.iterate(rc, _ ->
let line = reader.readLine();
if (Object.isNull(line)) None
else Some(line),
""
)
バッファリング機能付きの文字列ストリームリーダー ライター
//
/
def bufferedReader(file: String): BufferedReader \ IO =
Files.newBufferedReader(Paths.get(file))
def bufferedWriter(file: String): BufferedWriter \ IO =
Files.newBufferedWriter(Paths.get(file))
124
ローン
パターンのリソース管理ユーティリティ
//
(loan)
def withResource(
acquire: Unit -> Closeable \ ef1,
handle: Closeable -> a \ ef2
): Result[IOException, a] \ ef1 + ef2 + IO =
region rc {
let closable = Ref.fresh(rc, Option.empty());
let result = try {
Ref.put(Some(acquire()), closable);
closable |> Ref.get |> Option.foldLeft((_, c) ->
c |> handle |> Ok,
Err(new IOException("unexpected"))
)} catch { case e: IOException => Err(e) };
closable |> Ref.get |> Option.forEach(c -> c.close());
result }
def withReader(file: String, handle: BufferedReader -> a \ ef
): Result[IOException, a] \ ef + IO = ???
def withWriter(file: String, handle: BufferedWriter -> a \ ef
): Result[IOException, a] \ ef + IO = ???
125
ストリーム変換を活用した関数の例
//
pub def convert(inputFile: String, outputFile: String):
Result[String, Unit] \ IO =
withReader(inputFile, reader ->
withWriter(outputFile, writer ->
region rc {
lineIterator(rc, reader)
|> conversion
|> Iterator.forEach(a -> {
writer.write("${a}");
writer.newLine()
})
}))
|> Result.flatten
|> inspectEx
126
利用例 // flix> convert("input.txt", "output.txt") > \\ Ok(()) 入出力ファイル例 $ bat input.txt output.txt # ─────┬────────────────────────────────────────── │ File: input.txt ─────┼────────────────────────────────────────── 1 │ -12.3 2 │ # 0 3 │ abc 4 │ 77 ─────┴────────────────────────────────────────── ─────┬────────────────────────────────────────── │ File: output.txt ─────┼────────────────────────────────────────── 1 │ -24.61111111111111 2 │ 25.0 ─────┴────────────────────────────────────────── 127
おわりに: 関数型プログラミングの抽象化過程 書籍 FP in Scala から見えてくる、また、Flix言語でも 推奨されている段階的な習熟/活用のステップ: 1. 再帰、代数的データ型とパターンマッチに親しむ 2. よくある操作パターンを高階関数として定義/利用 できるようになる 3. 様々な型で繰り返し現れる(高階)関数を型クラスと して抽象化できるようになる 4. エフェクト/作用を上手く扱えるようになる 🐬< FP in Scala もFlixも(純粋)関数型プログラミングの 深淵への入口として非常にオススメ😈 128
Further Reading 書籍 FP in Scala Functional Programming in Scala, Second Edition cf. 1 Scala 第 版訳書『 関数型デザイン&プログラ ミング』 fpinscala: 書籍公式リポジトリ fp-matsuri/fp-in-scala-exercises: FP in Scala ベース の関数型プログラミング演習問題 by 関数型まつり lagenorhynque/fp-in-scala: Clojure/Flix実装 by 🐬 129
Flix 公式サイト が出会った 志向な 関数型言語 ファーストインプレッション 紹介 関数型プログラミング(言語) 関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ: 関数型プログラミング(言語)概説 "Simple Made Easy" Made Easier: Clojureに学ぶ simplicity: Simple Made Easy解説 The Flix Programming Language: Flix Clojurian simple JVM Flix —— : Flix 130