Swiftの関数と代数学

440 Views

March 15, 21

スライド概要

Swiftの関数と代数学(指数)の関連性についてまとめました🙏

profile-image

iOS エンジニアをやっています。

シェア

埋め込む »CMSなどでJSが使えない場合

各ページのテキスト
1.

Swift の関数と代数学(指数) part2 (#9 Algebraic Data Types: Exponents) iOS アプリ開発のためのFunctional Architecture情報共有会

2.

今回のテーマについて 前回の続きです (前回:struct・enum と代数学) #9 Algebraic Data Types: Exponents (指数) #4 Algebraic Data Types #19 Algebraic Data Types: Generics and Recursion の struct -> 直積型、enum -> 直和型(前回) Swift の func -> 指数(今回) Swift 2

3.
[beta]
part1 Swift の復習 - struct(直積型) の struct は直積型 struct Pair<A, B> { let first: A let second: B } Pair<Bool, Pair<Bool, Pair<Bool, Pair<Bool, // Bool // 2 * 2 Bool>(first: Bool>(first: Bool>(first: Bool>(first: true, second: true) true, second: false) false, second: true) false, second: false) は true or false の 2 通りの値を持つ で全 4 パターンが想定される 3
4.
[beta]
part1 Swift の復習 - enum(直和型) の associated value を持つ enum は直和型 enum Either<A, B> { case left(A) case right(B) } Either<Bool, Either<Bool, Either<Bool, Either<Bool, // 2 + 2 Bool>.left(true) Bool>.left(false) Bool>.right(true) Bool>.right(false) で全 4 パターンが想定される 4
5.

part1 の復習 - Void は特殊 は値を⼀つだけ持つ(空のタプル) 代数的には 1 として扱う Void パターン // 2(Bool) * 1(Void) = 2 Pair<Bool, Void>(first: true, second: ()) Pair<Bool, Void>(first: false, second: ()) // 2(Bool) + Either<Bool, Either<Bool, Either<Bool, パターン 1(Void) = 3 Void>.left(true) Void>.left(false) Void>.right(()) 5

6.

part1 の復習 - Never も特殊 は case を持たない enum(値を持たない型) 代数的には 0 として扱う Never パターン // 2(Bool) * 0(Never) = 0 Pair<Bool, Never>(first: true, second: ???) // // 2(Bool) + Either<Bool, Either<Bool, Either<Bool, コンパイルできない パターン 0(Never) = 2 Never>.first(true) Never>.first(false) Never>.second(???) // コンパイルできない 6

7.

part1 の復習 - Optional について は assocaited value を持つ enum の⽚⽅の case が Void であることとほぼ同じ意味 Optional Either<Void, A> { case left(()) case right(A) } enum Optional<A> { case none case some(A) } // Bool? -> 1(Void or none) + 2(Bool) = 3 // Data? -> 1(Void or none) + 1(Data) = 2 7

8.

part1 の復習 - それで何が嬉しい? 代数学的な直感を使えば、無駄な型を簡潔にすることが できたりする(↓ の例は正確ではない。議論は Point-Free 参照) URLSession.shared .dataTask(with: url, completionHandler: (data: Data?, response: URLResponse?, error: Error?) -> Void) のタプルは単なる積なので、パターンとして、 が考えられるが無駄が多い 本当に必要なものは、 と か のみが返ってくるという情報だけ 代数的に表すと 型にすると で⾔う が近い ( ) // Swift // 2(Data?) * 2(URLResponse?) * 2(Error?) = 8 // Data URLResponse Error // Data * URLResponse + Error // Either<Pair<Data, URLResponse>, Error> // Swift Reuslt ≒ Result<(Data, Response), Error> 8

9.

代数学における指数(Exponents) 指数計算はどのように捉えることができるのか? enum Three { case one, two, three } // Bool^Three // = Bool^(1 + 1 + 1) (enum // = Bool^1 * Bool^1 * Bool^1 は単なる和と⾒なせるため) それぞれの項は Three の値によってタグ付けされていると考えられる つまり、Three の各値に Bool を割り当てることと等しいと考えられる 9

10.

Three の各値に Bool を割り当てるとは? (Three) -> Bool 、つまり関数と捉えることができる func f1(_ x: Three) -> Bool { switch x { case .one: return true case .two: return true case .three return true } } (以下 2(Bool)^3(Three) = 8 パターン分続く // ... 代数学における指数と関数を結びつけることができた 10

11.

⼀旦指数と関数の関係についてまとめる // Bool^Three // = Bool^(1 + 1 + 1) // = Bool^1 * Bool^1 * Bool^1 ≒ (Three) -> Bool ⼀般化すると // // A^B ≒ (B) -> A A^B ≒ (B) -> A という関係が得られる 11

12.
[beta]
副作⽤を含むものについては考えない 無限に挙げられるため、以下のような副作⽤を持つものについては 考えないものとする func foo1(_ x: Three) -> Bool { print("hello") return true } func foo2(_ x: Three) -> Bool { print("world") return false } func foo3(_ x: Three) -> Bool { URLSession.shared.dataTask(with: URL(string: "https://www.pointfree.co")!).resume() return true } 12
13.

指数と関数の関連性から考えられるパターン パターン1: 複数の指数計算 パターン2: 1乗の指数計算 パターン3: 0乗の指数計算 13

14.

パターン1: 複数の指数計算 // (a^b)^c = a^(b * c) // ↓ まず ^ を <- に置き換える // (a <- b) <- c = a <- (b * c) // ↓ 反転させる // c -> (b -> a) = (b * c) -> a // ↓ Swift の型システムに置き換える // (C) -> (B) -> A = (B, C) -> A 14

15.

(C) -> (B) -> A = (B, C) -> A ある関数を返す関数があれば、引数を⼆つとる関数と等価だという ことを表している Point-Free はこの左辺と右辺を⾏き来する関数として、 curry , uncurry というものを紹介している curry : カリー化 カリー化を普及させたハスケル・カリーにちなんで 名付けられている 発⾒したのはモーゼス・シェーンフィンケル 15

16.

curry, uncurry 16

17.

パターン2: 1乗の指数計算 // a^1 = a // ↓ まず ^ を <- に置き換える // a <- = a // ↓ 反転させる // 1 -> a = a // ↓ Swift の型システムに置き換える // (Void) -> A = A // Point-Free これに対して は zurry/unzurry というものを紹介している 17

18.

(Void) -> A = A 18

19.

パターン3: 0乗の指数計算 // a^0 = 1 // ↓ まず ^ を <- に置き換える // a <- 0 = 1 // ↓ 反転させる // 0 -> a = 1 // ↓ Swift の型システムに置き換える // (Never) -> A = Void 19

20.

(Never) -> A = Void この形は⾮常に奇妙 Never には何もないはず しかし Never から 他の型への関数には Void、つまり⼀つの値が あるということを⽰している これについて考えるために、それぞれを⾏ったり来たりする 関数を今までと同じように考えてみる 20

21.

(Never) -> A を Void にする関数 この場合は⾮常に簡単である func to<A>(_ f: (Never) -> A) -> Void { return () } 21

22.

Void を (Never) -> A にする関数 しかし、こちらは難しい func from<A>(_ x: Void) -> (Never) -> A { // ????? } 少しずつこの関数を完成させてみる 22

23.

Void を (Never) -> A にする関数 まず、 (Never) -> A を返すことがわかっているので、スタブで 考えてみる func from<A>(_ x: Void) -> (Never) -> A { return { never in // ????? } } には値がない そんな never を使って何かできるか? never 23

24.

Void を (Never) -> A にする関数 の値が存在しないとしても、never の値で動作する 関数を定義できないということではない Never は enum であるため、 ↓ のように定義することができる! never func from<A>(_ x: Void) -> (Never) -> A { return { never in switch never { // Will never be executed // } } } 何もないがコンパイルできる!! の警告は発⽣する 24

25.
[beta]
なぜ定義できるか? 以下のような⼀般的な enum の処理を考えてみる (Either は left と right の case を持つ enum) func isInt(_ x: Either<Int ,String>) -> Bool { switch x { case .left: return true case .right: return false } } // return ここから先では何かを する必要はない // なぜなら Swift は enum の全ての case が処理されたことを知っているから 25
26.
[beta]
何もしない関数 absurd いくつかの⾔語ではこのような何もしない関数に absurd という 名前が付けられている func absurd<A>(_ never: Never) -> A { switch never {} } の意味は「ばかげている、常識に反した、不条理など」 他の⾔語においても使うことはほぼないらしい absurd を使うことができる例として Result.fold を⾒ていく absurd 26
27.
[beta]
Result.fold extension Result { func fold<A>(ifSuccess: (Value) -> A, ifFailure: (Error) -> A) -> A { switch self { case let .success(value): return ifSuccess(value) case let .failure(error): return ifFailure(error) } } } から A, Error から A にという形で、⼆つの型を⼀つの Aという 型に「折りたたむ(Fold)」ことができる関数 Value 27
28.
[beta]
fold の使い⽅ 例えば Result に整数値が含まれていたり、エラーメッセージとして ⽂字列が含まれているとする let result: Result<Int, String> = .success(2) 例えば、その Result を⽂字列に折りたたんで表⽰したい場合 ↓ のように使うことができる result.fold( ifSuccess: { _ in "Ok" }, ifFailure: {_ in "Something went wrong" } ) // "Ok" 28
29.

Result の Error に Never を⼊れる でもその考えは利⽤されているが、Result の Error に を⼊れると失敗することがない Result を得ることができる Combine Never let infallibleResult: Result<Int, Never> = .success(2) // オートコンプリートで ↓ が出てくる infallibleResult.fold( ifSuccess: <# (Int) -> A #>, ifFailure: <# (Never) -> A #> ) (Never) -> A 29

30.
[beta]
(Never) -> A = absurd infallibleResult.fold( ifSuccess: <# (Int) -> A #>, ifFailure: <# (Never) -> A #> ) ↓↓↓↓↓↓↓ infallibleResult.fold( ifSuccess: { _ in "Ok" }, ifFailure: absurd ) 何の意味もないと思っていた absurd が使⽤できることを発⾒ 30
31.

指数と関数の関連性がわかって何が嬉しい? 以前は struct や enum を代数的に捉えることによって、 型をどのように構築すれば良いかの役に⽴つことがわかった 今回は、指数・関数の関連性についても理解することによって 関数の場合でも同じようなことができるようになった それを実感するために、もう少しだけ例を⾒ていきます 31

32.

⼀つ⽬の例 // a^(b + c) == a^b * a^c // a <- (b + c) == (a <- b) * (a <- c) // (b + c) -> a == (b -> a) * (c -> a) // (Either<B, C>) -> A == ((B) -> A, (C) -> A) は (ただの enum) // Either ↓ Either<A, B> { case left(A) case right(B) } こちらも先ほどまでと同じく左辺、右辺を⾏ったり来たりする 関数が定義できる 32

33.
[beta]
(Either<B, C>) -> A == ((B) -> A, (C) -> A) 閲覧者⽤の練習問題として⽤意されていたため、正しいかはわからないです // func to<A, B, C>(_ f: @escaping (Either<B, C>) -> A) -> ((B) -> A, (C) -> A) { return ( { b in f(.left(b)) }, { c in f(.right(c)) } ) } func from<A, B, C>(_ f: ((B) -> A, (C) -> A)) -> (Either<B, C>) -> A { return { either in switch either { case .left(let b): return f.0(b) case .right(let c): return f.1(c) } } } 33
34.

この例からわかること (Either<B, C>) -> A == ((B) -> A, (C) -> A) を取る関数は、実際には左の値を操作する関数と 右の値を操作する関数の⼆つの関数であることを⽰している 関数からわかることは関数の⼊⼒に enum の case を追加しても、 ⼩さな単位に分解できるため、複雑さはそこまで増加しないこと 仮に⼊⼒を増やした場合でも ⼊⼒である Either は enum であるた めコンパイラに修正することを強制される(⾃然な形で修正可能) Either 34

35.

⼆つ⽬の例 // (a * b)^c = a^c * b^c // (a * b) <- c = (a <- c) * (b <- c) // c -> (a * b) = (c -> a) * (c -> b) // (C) -> (A, B) = ((C) -> A, (C) -> B) 35

36.
[beta]
(C) -> (A, B) = ((C) -> A, (C) -> B) こちらも同じく合っているかはわからないです // func to<A, B, C>(_ f: @escaping (C) -> (A, B)) -> ((C) -> A, (C) -> B) { return ( { c in return f(c).0 }, { c in return f(c).1 } ) } func from<A, B, C>(_ f: ((C) -> A, (C) -> B)) -> (C) -> (A, B) { return { c in let a = f.0(c) let b = f.1(c) return (a, b) } } 36
37.

この例からわかること (C) -> (A, B) = ((C) -> A, (C) -> B) タプルへの関数はタプルの各辺にマッピングされた関数のタプルと 同じ フィールドを追加して関数の戻り値の型を拡張しても、関数の複雑 さがそれほど増すわけではない また、そうなるようにコンパイラが強制している(⾃然な形で 修正可能) 37

38.

以上の指数と関数の関連性からわかること 指数的に左辺 = 右辺が成り⽴っていれば、複雑な関数を分解 することができたりする 関数の⼊⼒や出⼒を増やしたとしても、コンパイラが⾃然な形での 修正に導いてくれる 仮に指数計算を誤っていたとしたらどうなるか?(これについては、 ⾃分の中であまり納得感を持つことができていません ) 38

39.

⼀つ⽬の誤った指数計算 // a^(b * c) != a^b * a^c // a <- (b * c) != (a <- b) * (a <- c) // (b * c) -> a != (b -> a) * (c -> a) // (B, C) -> A != ((B) -> A, (C) -> A) 39

40.

(B, C) -> A != ((B) -> A, (C) -> A) タプルからの関数はこれ以上単純化されることはないということ を⽰している つまり、フィールドを追加して関数の⼊⼒を拡張すると、 より複雑な関数になるということ 40

41.

⼆つ⽬の誤った例 // (a + b)^c != a^c + b^c // (a + b) <- c != (a <- c) + (b <- c) // c -> (a + b) != (c -> a) + (c -> b) // (C) -> Either<A, B> != Either<(C) -> A, (C) -> B> 41

42.

(C)->Either<A, B> != Either<(C) -> A, (C) -> B> にしても、それ以上単純化されることはない case を追加して関数の出⼒を増やすことは、考慮すべき case が 増えてしまうことを⽰している それにも関わらず、それらのケースを考慮することを強制するもの は何もない(正しい場合はコンパイラが強制してくれた) これは Result 型を返す関数や throw を返す関数が他の関数よりも 当然複雑になる理由も⽰している 特に (A) -> Result<B, E> という形になっているため、代数的に 捉えると (B + E)A となり、複雑であることがわかる Either 42

43.

まとめ の関数と代数学における指数には関連がある 指数を利⽤すれば、複雑な関数をコンパイラの⼿を借りながら 分解することができる 指数計算として成⽴していないものは、それ以上関数を単純化 できないということを⽰している(⾃分は納得感を持てていない) (余談): TCA の combine, pullback をしっかりと理解したくて、 Point-Free の Reducers and Stores を⾒始めています combine, pullback がなぜ必要なのか、どのように実装されてい るかなどを知ることができてもっと早く読んでおけば良かったと なっています... Swift 43