- 投稿日:2020-08-07T16:47:00+09:00
不動点コンビネータを用いた無名再帰関数の実行(C#, F#)
不動点コンビネータとは
すごく簡単に言うと、「再帰じゃない関数を引数にとって、再帰関数と同じ動きをする関数に作り替えてくれる高階関数」です。
再帰関数の例として、例えばフィボナッチ数を返す関数をJavaScriptで書いてみると
function fibonacci(n) { if (n == 0) return 0; else if (n == 1) return 1; else return fibonacci(n-1) + fibonacci(n-2); }と書いてもいいわけですが、記事の都合で末尾再帰の形にして
function fibonacci(f1, f2, counter) { if (counter == 0) return f1; else return fibonacci(f2, f1+f2, counter-1); } function fibonacci_(n) { return fibonacci(0, 1, n); }とまあこんな感じの末尾再帰にしたfibonacciを、さらにカリー化して例にします。
上のfibonacciはfunctionで定義されていますが、これをカリー化したアロー関数(匿名関数)で書きたい場合は
const fibonacci = f1 => f2 => n => (n == 0) ? f1 : fibonacci(f2)(f1+f2)(n-1);こんな感じで、
再帰するには一旦fibonacciという名前の変数を作っておいてから書き換えてやらないといけないと思ったがそんなことはなかったぜ。ここに不動点コンビネータであるfix関数があれば
const fix = /* ... */ const f = fib => f1 => f2 => n => (n == 0) ? f1 : fib(f2)(f1+f2)(n-1); // fは再帰関数ではない const fibonacci = fix fこんな感じで、再帰してないアロー関数(匿名関数)fを使って再帰関数fibonacciができあがります。
不動点コンビネータがあると嬉しい、というようなものではないのですが、まあ計算理論として興味深い、というぐらい。
不動点コンビネータ - Wikipediaも読んでおくといいでしょう。
不動点コンビネータその1 再帰関数で実装したもの
fixの方を再帰定義にしてよければ、割と簡単です。
ただし、引数を名前呼び(遅延評価)する言語と値呼び(正格評価)する言語でちょっと書き方が変わります。
C# と F# はどちらも後者になります。
fixの型を疑似的に表すと
((T1 => T2) => (T1 => T2)) => (T1 => T2)
です。C#の例です。
Fix
をスタティックメソッドにしておきました。
Fix
を呼び出しているところの型引数Fix<int, Func<int, Func<int, int>>>
がうっとうしいですが、C#のラムダ式には型がなく、外から与えてやる必要があるので、仕方ありません。型引数を省略しようとすると、今度はラムダ式をキャストして、型を明示しないといけません。Fibonacci.csusing System; class Program { static void Main(string[] args) { int fib40 = Fix<int, Func<int, Func<int, int>>>(fib => f1 => f2 => n => n == 0 ? f1 : fib(f2)(f1 + f2)(n - 1))(0)(1)(40); Console.WriteLine(fib40); } // name "Fix" is recursive static Func<T1, T2> Fix<T1, T2>(Func<Func<T1, T2>, Func<T1, T2>> f) => f(x => (Fix(f))(x)); }F# はOCamlベースで、カリー化されていて、型推論も強力なのですっきりしています。
Fibonacci.fslet rec fix f = f (fun x -> fix f x) let fib40 = fix (fun fib f1 f2 n -> if n = 0 then f1 else fib f2 (f1 + f2) (n - 1)) 0 1 40不動点コンビネータその2 Zコンビネータ―で実装したもの
不動点コンビネータ―fixの方も再帰なしで定義する例として、有名なのはYコンビネーターですが、静的型付け言語だとYコンビネーターにはうまく型が付けられないので、代わりにZコンビネータ―を定義します。
Zコンビネータ―は、関数は再帰しませんが、型の定義が再帰します。
C# や F# では、独自のデリゲート型を定義することで、型の定義が再帰するような関数やメソッドの型を用意できます。ただ、デリゲート型をF#で扱うのはちょっとだけぎこちなくなります。
C# はこちら。
Z
の定義の中でラムダ式をRecursive<T1, T2>
デリゲート型にキャストする形で型を明示しています。FibonacciZ.csusing System; // type "Recursive<T1, T2>" is recursive delegate Func<T1, T2> Recursive<T1, T2>(Recursive<T1, T2> f); class Program { static void Main(string[] args) { int fib40 = Z<int, Func<int, Func<int, int>>>(fib => f1 => f2 => n => n == 0 ? f1 : fib(f2)(f1 + f2)(n - 1))(0)(1)(40); Console.WriteLine(fib40); } static Func<T1, T2> Z<T1, T2>(Func<Func<T1, T2>, Func<T1, T2>> f) => ((Recursive<T1, T2>)(g => f(x => g(g)(x)))) ((Recursive<T1, T2>)(g => f(x => g(g)(x)))); }F# はこちら。デリゲート型をインスタンス化し、呼び出しに
Invoke
メソッドを使うなど、オブジェクト指向的なデリゲート型の扱いがC#よりもあからさまになっています(C# もバージョン1の頃はnew
でデリゲート型をインスタンス化していました)。FibonacciZ.fs// type "Recursive<'a, 'b>" is recursive type Recursive<'a, 'b> = delegate of Recursive<'a, 'b> -> ('a -> 'b) let z f = (Recursive(fun g -> f (fun x -> g.Invoke g x))).Invoke (Recursive(fun g -> f (fun x -> g.Invoke g x))) let fib40 = z (fun fib f1 f2 n -> if n = 0 then f1 else fib f2 (f1 + f2) (n - 1)) 0 1 40 printfn "%d" fib40
- 投稿日:2020-08-07T13:24:41+09:00
[Microsoft] Angularチュートリアル Tour of Heroes を Blazor で再実装する
Angularのチュートリアル: Tour of Heroes をBlazorを使って再実装してみます。
Angularチュートリアル Tour of Heroes を Blazor で再実装する 目次
- 1. 新規プロジェクトの作成
- 2. リストを表示する
- 3. 機能コンポーネントを作成する
- 4. サービスを追加
- 5. ルーティングを用いたアプリ内での画面遷移
- 番外. C#ソースコードとRazorの分離
- 6. サーバからデータを取得する
- 番外. Blazorコンポーネントのユニットテスト
- 投稿日:2020-08-07T12:15:49+09:00
C# 9.0 で範囲チェックがちょっと楽になる
int
型の変数i
が0
以上10
未満であることを判定するには次のようにします。if (i >= 0 && i < 10) { }C# 9.0 では次のようにも書けます。
if (i is >= 0 and < 10) { }何が嬉しいかと言うと、
i
の評価が一回で済むところです。二番目のコードをよく見てください。i
が一回しか書かれていませんよね?評価が一回で済むということは、変数だけではなく、副作用を持つ関数などにも使えるということです。
if (GetSomeInt() is >= 0 and < 10) { }
GetSomeInt()
が副作用を持つ場合、今までは評価(この場合は関数呼び出し)を一回で済ませるため、戻り値を一度変数に入れなければいけませんでした。しかしこれからはその必要はありません。このコードは C# 9.0 でのパターンマッチングの進化によって可能になります。パターンマッチングと言えば
switch
式ですが、それ以外にもif
や三項演算子などbool
が必要なところで使うことができます。型パターン、位置パターン、プロパティパターンなど様々なパターン(将来はもっと予定されています)が論理演算できるようになるので、これからは条件式の形が大きく変わりそうです。「パターンマッチング実は少し苦手なんだけど」という人も、避けて通れなくなるのではないでしょうか。
- 投稿日:2020-08-07T12:15:49+09:00
C# 9.0 で条件式が革命を起こす
int
型の変数i
が0
以上10
未満であることを判定するには次のようにします。if (i >= 0 && i < 10) { }C# 9.0 では次のようにも書けます。
if (i is >= 0 and < 10) { }何が嬉しいかと言うと、
i
の評価が一回で済むところです。二番目のコードをよく見てください。i
が一回しか書かれていませんよね?評価が一回で済むということは、変数だけではなく、副作用を持つ関数などにも使えるということです。
if (GetSomeInt() is >= 0 and < 10) { }
GetSomeInt()
が副作用を持つ場合、今までは評価(この場合は関数呼び出し)を一回で済ませるため、戻り値を一度変数に入れなければいけませんでした。しかしこれからはその必要はありません。ちなみに、
i
が2
3
5
のいずれか、または10
以上かどうかを判定するには次のようにします。if (i is 2 or 3 or 5 or >= 10) { }また、カッコや
not
を使ってもっと複雑な論理演算もできます。if (i is (<= 0 or 3 or >= 10) and not 15) { }これらのコードは C# 9.0 でのパターンマッチングの進化によって可能になります。パターンマッチングと言えば
switch
式ですが、それ以外にもif
や三項演算子などbool
が必要なところで使うことができます。型パターン、位置パターン、プロパティパターンなど様々なパターン(将来はもっと予定されています)が論理演算できるようになるので、これからは条件式の形が大きく変わりそうです。「パターンマッチング実は少し苦手なんだけど」という人も、避けて通れなくなるのではないでしょうか。
参考: パターン マッチング - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
パターンを組み合わせるとこんなこともできます。たとえば次のコードは
object
型の変数をint
と比較しているのでコンパイルエラーになります。object o = 1; if (o > 0) { }これを次のようにパターンマッチングで書き換えるとコンパイルが通ります。この時、
o
がnull
などint
でない場合はif
文の中身は実行されないため、引数で渡された時などのnull
判定を省略できます。object o = 1; if (o is > 0) { }しかし
o
は依然object
なので次はコンパイルエラーになります。object o = 1; if (o is > 0) { o += 2; }ここに型パターンを組み合わせます。
object o = 1; if (o is int i and > 0) { i += 2; }また、うるう年判定は次のように書けます。
bool IsLeapYear(int year) => (year % 400, year % 100, year % 4) is (0, _, _) or (_, not 0, 0);このようにタプルと位置パターンを組み合わせると、条件演算のネストがなくなってフラットに判定できることがあります。