twitterでいけがみさんが張ってらした論文が面白かったです。
map f [a1, a2, ..., an] ==> [f a1, ..., f an]
zipWith f [a1, ..., an] [b1, ..., bn] ==> [f a1 b1, ..., f an bn]
zipWith3 f [a1, ..., an] [b1, ..., bn] [c1, ..., cn] ==> [f a1 b1 c1, ..., f an bn cn]
...
zipWith7 f [a1, ..., an] [b1, ..., bn] ... [g1, ..., gn] ==> [f a1 b1 … g1, ..., f an bn … gn]
Haskell98 の標準ライブラリの関数ですけど、
1引数関数 f と1つのリスト as を受け取って as の全要素に f を適用して返すmap
、
2引数関数 f と2つのリスト as, bs を受け取って as と bs を同時に並べて f を適用して返すzipWith
、
3引数と3つのリスト zipWith3
、以下同様に zipWith4
、zipWith5
、
zipWith6
、zipWith7
まであります。
型が違うのでこれらは別々の関数にならざるを得ないのですが、これはなんともダサい。
なんともダサいので、もっと強力な記述力のある言語を使ってこれを綺麗に書くやり方は色々知られていますがここでは略。
そんな強力な言語機能を持ち込まなくても、普通の ML や Haskell のレベルでこんな書き方を可能にできるんじゃない?
…というお話。
myZipWith one f [a1, a2, ..., an] ==> [f a1, ..., f an]
myZipWith two f [a1, ..., an] [b1, ..., bn] ==> [f a1 b1, ..., f an bn]
myZipWith three f [a1, ..., an] [b1, ..., bn] [c1, ..., cn] ==> [f a1 b1 c1, ..., f an bn cn]
...
myZipWith seven f [a1, ..., an] [b1, ..., bn] ... [g1, ..., gn] ==> [f a1 b1 … g1, ..., f an bn … gn]
myZipWith という一つの関数で、パラメタ one, two, ... を変えると map, zipWith, ... が全部作れるというもの。 もちろん、
myZipWith n = n
one = map
two = zipWith
three = zipWith3
...
こういうインチキはなしで、
myZipWith n = ...
z = ...
s n = ...
one = s z
two = s (s z)
three = s (s (s z))
...
こんな風に、z と s という基本関数から 0,1,2,3,... を全部作り出せるような形でやりましょう、と。
…は論文読んでみて下さい。すごく簡潔で感心しました。要は
zipWith3 f as bs cs = apply_each (zipWith f as bs) cs
where apply_each (f:fs) (a:as) = (f a):(apply_each fs as)
apply_each _ _ = []
みたいに zipWith[N] は zipWith[N-1] を使って書けるのがポイントだと思うんですが、 ただ、これ zipWith じゃなくて zip だとどうするんだろう。全然書ける気がしない。 要は 1 と (2,3,...,n) というタプルから (1,2,3,...,n) というタプルが作れるようになっていればいいんですが、 Haskellのタプルはそういうinductiveなものじゃなかった気がする。うーむ。
クリアしたああああああああーーーー!
腕輪なしでも90F到達ペースはコンスタントに出せるようになってきたので、 何回もやってるうちにいい腕輪拾えれば…、と思ってコツコツと挑戦してました。 64Fでパコレプキンの腕輪ゲット。65~85まで壁ジャンプで50ターン/Fくらいと飛ばして、 あとは回復アイテムが入れ替えの杖オンリーだったため、控えめに通路の斜め戦闘と行き止まりからの折り返し専用にして突破。 火印は盾縛りするときは必ず入れる程度には好きなんですがこれは不慮の事故です。 あとは、腕輪なしクリア目指したいなあ。
CiNii で有料論文が引っかかった時のガッカリ感について一言いっておくか…じゃなくて、
4月から NII の特別研究員というものになることになりました。
今後ともよろしくお願いします。任期付きなので任期が切れたら路頭に迷う予定です。
以上ご報告でした。
『型レベルプログラミングの会』 ほんとうに開催します。 具体的には C++ for Haskeller のような雰囲気のわかりやすい紹介を通して、 C++ や Haskell や Scala 使いがお互いの言語に親しめればいいんじゃないかなーと思っています。
…ってこっちで告知する前にほぼ埋まってしまったorz。 できれば ustream 辺りで中継したいと思います。
掲示板で教えていただいたのですが、 Creative Commons から CC0 というのがリリースされたそうです。
Public Domain にするということを宣言するための、Worldwide に適用できる宣言とのこと。 NYSDL で宣言したかった内容は完全に CC0 で実現できているように思いますし、 俺々ライセンスよりもスタンダードなものに合わせる方がベターなことは明らかなので、 自分の書いたドキュメントに関しては NYSDL から CC0 にシフトしていこうと思っています。 「煮るなり焼くなり好きにしやがれ」という具体的な内容は何も変わらないので、 使う側としては特に変化ないと思いますが、まあそういうことで。よろしくお願いします。
たぶん自分のマイブームと発表のあったテーマ色々が一致したためだと思いますが、 今年は例年と比べても特に楽しかったです。 Ypsilon の藤田さんと同じ部屋だったりして色々お話聞かせていただいた。やっほい。
様相論理とステージ計算(要は quote と unquote みたいなのがあるλ計算)の間の同型対応の話が何件もあってどれも面白かったです。 "◇" の計算的意味はなんだろう、というのがまだ自分の中で確固としたイメージにならずもやもやしています。 考えたい。 あとは、↓の方を見ていただければおわかりのように自分の中で限定継続ブームだったので、 そちら方向の発表もどれも興味深かったです。 亀山先生の発表に出てきた例で、うろおぼえですが
(* 'general' fibnacci : gib x y n で初期値x,yのフィボナッチ的関数の第n項 *)
let gib x y =
let rec loop n =
if n = 0 then x else
if n = 1 then y else
loop (n-1) + loop (n-2)
という関数を、できるだけ構造を崩さずに、nを与えるとそのnに特化したコードを生成する関数に変えるというのがあって、
let sgib x y =
let rec loop n =
if n = 0 then x else
if n = 1 then y else
<~(loop (n-1)) + ~(loop (n-2))>
これだと、こんな感じの
<fun x y -> ~(sgib <x> <y> 5)>
==> <fun x y -> ((y+x)+x)+(y+x)+((y+x)+x)>
y+x を何回も計算するコードになる。メモ化しても「このコードを高速に生成する関数」になるだけで、 「高速なコードを生成する関数」にはならない。これをメモ化に加えて shift/reset を使うことで
<fun x y -> ~(sgib <x> <y> 5)>
==> <fun x y -> let t0=x in let t1=y in let t2=t1+t0 in
let t3=t2+t1 in let t4=t3+t2 in t4+t3>
これが出せるようにする、という例がありました。再びうろ覚えで私が理解した範囲では要するに、
// 注意: ここでは、継続の使い方のわかりやすさのためにコードの構造は大きく変えてしまっています。
// 実際の論文や発表で紹介されていたバージョンはもっとさらにgibの形に近いです。
function sgib(varx, vary, n)
{
@memoize
function loop(n)
{
if( n==0 ) return varx
if( n==1 ) return vary
var t = generate_fresh_symbol()
return[sgib] <let ~(t) = ~(loop(n-1)) + ~(loop(n-2)) in ~(return[→sgib](t))>
}
return loop(n)
}
こういう雰囲気の限定継続の使い方をしていて、いままで自分が見たことのない効果的な使い方だったので、
なるほどなーと感心することしきり。
上位から loop(n-1)
のような形で呼ばれるときには、
そこにはt0やt1のような変数を入れたいので、
まずいったん変数を返して( return[→sgib](t)
)全体の式を作らせてから、
その外に let 束縛を追加して改めて全体の式として返す、と。これは自分で一から書ける気がしない。すごいなあ。
ところで自画自賛ですが return[→sgib]
記法はやっぱりわかりやすいと思う。
追記: 参照→継続ベース部分評価
他には、id:keigoi さんの Session Type を Haskell の型クラスメタプログラミングで、というのがツボに入りました。 Functional Dependency の使いどころもっとちゃんと把握せねば。 その後の宴会でふと思ったのだけど、 型レベルプログラミング会議とかやったら面白いかなあ。自分の知っている限りでも C++ と Haskell と D と OCaml のそれぞれで、結構特徴の違う型レベルプログラミング文化が発展している気がするのですよね。 いちど全部一同に会して見ると面白いんじゃないかと。問題は開催地だ…。
あとは……『木文法の構文解析を利用したプログラム逆計算』に Co-regular木文法 [1] [2] (文字列でいうと左線形文法に近い文法。文字列だと左も右もregularになるんですが、 木は右っぽいものだけがregularになる) を考えてみると面白いと思う!と絡んでました。しかしよく考えたら、 あれって構文解析は何も考えられてなかったかもしれない…。 線形時間でできてもおかしくないと思うので帰りの新幹線でずっと考えていたんですが、 しかし思いつきませんでした。 『統合開発環境のためのプログラミング言語拡張フレームワーク』は、Eclipse の Java コンパイラのの渡してくるエラーメッセージや微妙なエラーリカバリー結果の AST を手がかりに言語拡張を叩き込むという力業でかっこよかったです。その発想はすごい。
明日から PPL 2009 に行ってきます。 聞きに行くだけですが。面白そうな発表が多いので楽しみにしています。
Lexical scope にこだわってどこまで行けるかということで…
function foo() {
function bar(){ ... return[→foo] ... }
}
だけではなく
function foo() {
var returnFoo = return
function bar(){ ... return[→returnFoo] ... }
}
まで許せば、つまり→の右に実行時の値を指定することを許せば、 かなり使い勝手が向上するのではないかということをぼんやりと考えています。 自分が下の方に書いたシンタックスシュガー展開後のコードならどうせここは動的な値になっていますし。 ただ、一歩一歩確実に意味論がわけわかんなくなってきているので、一度整理してみないと…。
それCyan と突っ込みいただきました。おおおお! トップレベルだけ扱いが違うのか、shinhさんのコードそのままだとループしないみたいですが、 関数の中でやったら確かに上手くいきました。
main = ^():
globalVar = 0
hello = ^():
globalVar = return
return(0)
x = hello()
say x
globalVar(x+1)
main()
Cyan = Pythonっぽい構文+Lispっぽいマクロ+Ioっぽいオブジェクトシステム、 という紹介を何度か聞いて、 チュートリアル見てもそれだけな感じに見えたので面白くなさそうだーと思ってスルーしてました。 すみませんすみません。全力で反省。ちゃんと言語仕様全部読んでから判断すべきでした。これはいい!
Shiroさん からは shift/reset
の使われ方について。"ほとんど call/cc ののりで使う shift"
は、
確かに return[xxx→yyy]
的な感覚とはちょっとずれますね。なるほど。
resetは普通にresetのまま残して、上でresetがあったら問答無用でreturn[foo]が勝手に限定継続取るようになるとか…うむむ…
"call/ccは return に名前を変えた。こっちの方が分かりやすいと思う。"
おお!!今 kuzha もそんな感じになっていたりするのかな。あとでチェック…。
ブクマコメ
の方については、普通の関数でも例外飛ばしたり中で実行終了したら後続の処理は行われないので、
そこはあまり本質ではないかなーと考えてました。
あと、最終形の return[→f]
まで行き着くと実際3回呼ばれるようになりますし。
要旨:「C や JavaScript 等の関数から返る"return"を、 first-class object と思ってみると面白いんじゃないでしょうか」
function hello(x)
{
return (x+1)*2 // hello から (x+1)*2 を返す
}
ふつうですね。
function hello(x)
{
return( (x+1)*2 ) // hello から (x+1)*2 を返す
}
最近はあんまりここに括弧を付けるコードは見ないような気がしますが、付けてみました。 動作は変わりません。 ただ、なんとなく、return という名前の関数を呼び出しているように見えます。 「helloから値を返す」という動作をする関数。
function hello(x)
{
var f = return
f( (x+1)*2 ) // hello から (x+1)*2 を返す
}
関数に見えてきたので、普通の関数オブジェクトっぽく、変数に代入したりできても良さそうな気がします。 もしできたらどうなるか考えてみます。
function hello(x)
{
var f = return
function world(y)
{
f( y*2 ) // hello から (x+1)*2 を返す!
}
world( x+1 )
}
変数に代入したら、クロージャというかなんというか、内部関数から使えて欲しいです。
function world(y){ return y*2 }
だと world から y*2 を返すだけですが、
外のスコープで var f = return
したので、
上のコードは hello から一気に y*2 を返してくれると楽しそうです。
this
と同じで、return も、
関数が実行時に呼び出されたときに暗黙に値が決まる特別な識別子かなにかになるでしょう。
function findFirstNegative(arr) // 配列内の最初の負の数を返す
{
var returnFFN = return
arr.forEach(function(elem)
{
if( elem < 0 )
returnFFN( elem ) // findFirstNegative から elem を返す!
})
throw "えらー"
}
findFirstNegative( [123, 45, -67, 8, -9] ) // -67
たぶん、こういうことをするのに便利だと思います。 forEach のような制御構造の場合であれば、Ruby のブロックや D の opApply のような 「ただの関数オブジェクトではない return の飛び先を良きに計らってくれる言語機能」 を使って書く手があったりすると思うんですが、 制御構造に限らず一般の再帰的な探索を内部関数で書いて、 一気に外の関数をreturnで抜けられたりすると結構楽なこともあると思います。 シンタックスシュガーがあるともっと便利かも。
function findFirstNegative(arr) // 配列内の最初の負の数を返す
{
arr.forEach(function(elem)
{
if( elem < 0 )
return[findFirstNegative] elem // findFirstNegative から elem を返す!
})
throw "えらー"
}
上の var returnFFN = return
するコードのシンタックスシュガー。
return[関数名]
を見つけると、
処理系が、外側のスコープにある"関数名"という名前の関数の頭に var returnXXX = return
を付け加えてから return[関数名] val
を returnXXX(val)
に置き換える感じの実装とします。
あくまで「return が first-class object である」という特徴を使いやすくするシンタックスシュガーなので、
function findFirstNegative(arr) // 配列内の最初の負の数を返す
{
arr.forEach(function(elem)
{
var f = return[findFirstNegative]
if( elem < 0 )
f( elem ) // findFirstNegative から elem を返す!
})
throw "えらー"
}
依然として、変数に代入したりはできると嬉しい。
ちなみに Scala が private[パッケージ名]
とかでスコープを指定したアクセス制御ができるらしいので、
シンタックスシュガーの構文はそちらを参考にしてみました。
さて、実は、ここからが本題です。
要旨: 「"call/cc" って難しいので、
継続のプリミティブは "return" ということにしよう。特に手続き型言語では」
return をただの関数と思う事にしたので、グローバル変数に代入できたっていいですよね。
var globalVar
function hello()
{
globalVar = return
return 0
}
var x = hello()
print(x)
globalVar(x+1)
まず最初に 0 が return されるので、print(0) で 0 が表示されます。さて、次の globalVar(x+1)
はどう動くべきか?選択の余地があります。
var x = hello()
のところにジャンプして戻って x に 1 を入れる。
以下繰り返しで、このプログラムは、0、1、2、... の無限表示するプログラムになる。1番は実装が楽あるいは実装が楽もしくは実装が楽などのメリットがありますが、 ここは面白さを優先して、よりファンキーな感じの2番の動作をするバージョンを考察することにします。 そもそも、returnを普通の関数と思う事に決めたので、ある時点から突然呼び出せなくなる関数とかなってしまうのは、 ちょっと残念ですし…。
…と、考察といっても、returnがやることは、「とにかく意地でも対象の関数から値を戻す」だけなので、 まあ、それだけです。はい。別にこれ以上解説すべき点もなにもないと思います。簡単ですね。
☆
ただ、その「それだけ」「簡単」というのが重要なのではないかと最近思い始めました。 話が一瞬飛びますが、これは first-class の 継続(Continuation) の理解しやすい構文として使えるのではないかなー、と。 実際、"first-class return" があれば first-class continuation でできることは全てできます。 Scheme, Ruby 等で継続を取り出すプリミティブである call/cc が
function callcc(f) { return( f(return) ) }
こう実装できるので。というか、first-class return の動作はまさに 「関数から戻った後の計算」 という継続をキャプチャするというものなので、 何も新しいことはなくて、機能としては単に continuation を first-class object として扱っているだけです。
☆
なにか難しそうな話になってきた…と思ったら、☆から☆までの中身は忘れて下さい。要りません。
"first-class return" さえ理解できていれば、"継続" だとか "call/cc" だとかの用語を一切知らなくても、
そいつらで書ける機能は全て return
でも書けます。
「継続」は別段そんなに難しいものではないと思うのですが、少なくとも、自分の中では、 難しそうなイメージがあります。そもそも「継続とはなんぞや」という用語の説明が要る時点で難しい。 ただ、この難しさの半分くらいは、「継続」の難しさじゃなくて call/cc の難しさなんじゃないでしょうか。 でも、 "first-class return" ならば、その機能は (たとえ今まで "first-class continuation" が理解できなかったという人でも、 何割かは) この記事のここまでの説明で完全に把握できた気分になれるんじゃないかと思うんです。そう信じる。 どうだろう…だめかな。 というわけで、ここまでひとまずの結論は 「"継続" や "call/cc" という概念をを一切使わずに、"first-class return" という名前の元でこっそり first-class continuation を導入したら、 以外と結構誰でも継続になじめるようになるのではないでしょーか。」
※ 実際、いくつかの処理系では、call/cc よりも first-class return
に近いプリミティブで継続をキャプチャできるようになっています。
mozilla によるJavaScript実装 Rhino では、
return
の代わりに new Continuation()
と書けば、まったくそのまま同じ機能です。
// Rhino で実際に動くコード
function hello(x)
{
var f = new Continuation()
function world(y)
{
f( y*2 ) // hello から (x+1)*2 を返す!
}
world( x+1 )
}
ruto さんの [PDF] 合成モナドのための call/ccに対する改善の提案 の get/cc も、これまたちょっと違いますが、 継続をキャプチャする処理を、それを処理ルーチンに渡す処理から分離するという点で似ているかなーと勝手に思っています。
※ 余談。なんで call/cc がわかりにくいかと感じるかというと、"current" continuation
というのが call/cc を導入するまでは全く表に出てこない存在だからではないかと思っています。
たしかにどんな言語のどんな処理にも存在している
"継続" ですが、普段は陽には現れていません。でも、例外があって、return
文というのは継続の呼び出しが唯一陽に表に現れているポイントだと思う事ができます。
なので、こいつを一般化することでfirst-class continuationを導入するのは、
元々目に見えていた物のパワーアップになるので、その分だけ類推で把握しやすいものになる…といいなあ。
類似品として、"first-class break" や "first-class continue" を導入してもいいかもしれません。
一方で、要旨に「特に手続き型言語では」と限定条件をつけたのもこの辺りの考えがその理由で、
全てが式の関数型言語だと return
は陽に現れていないので、
結局それは "current" continuation と大差ない裏方度合いかもしれない…などと考えていました。
実は、ここまでの内容は昔書いた記事の焼き直しでただの前置きでして、
なんとここからが本当に本当の本題です。
要旨:「難しいのは継続ではないcallccだ…とすると、
難しいのは限定継続ではなくshift/resetなのでは?」
限定継続 (Delimited Continuation) という、継続の仲間のようなさらにややこしいものがあります。 これがなかなかややこしくて未だにちゃんと把握できないでいるので、 自分が理解できないのは shift/reset や control/prompt という構文のせいだと思い込むことにしました。 first-class return のように、もっと自分に把握しやすいスタイルの限定継続があるのではないか…。 考えてみます。以下、shift/reset 的な機能をなんとか表現する試み。
composable-continuations-tutorial の最後の方の例を考えてみます。foreachっぽい処理をする関数を受け取って、 リストをストリームに変える関数を返す関数…なんですけど、 ちょっと簡略化して、単純にリストをストリームに変える関数。
(define (collection->stream collection)
(reset (begin
(for-each (lambda (element)
(shift k
(stream-cons element (k))))
collection)
stream-nil)))
(shift k BODY)
は何をするかというと、
なので、(collection->stream (list 100 200))
を呼ぶと、とりあえず普通に実行が進んで、
shift が終わったところで (stream-cons 100 (k))
がまずは帰ってくる。
で、このcdr側の(k)を呼び出すと、ループ2週目が始まって、(stream-cons 200 (k))
が戻ってきて、もう1回 cdr 側を呼ぶと、stream-nil
が来る。
これ、こう書けないでしょうか。
function collection2stream(collection)
{
collection.forEach(function(elememt)
{
return[collection2stream] StreamCons(element, return[→collection2stream])
})
return StreamNil
}
正直、まだ到底わかりやすいとは言えないですが、 限定継続を取り出してくる部分と reset のところまで飛ぶ部分を分離したことで、 多少は見通しがよくなっているような気分が自分ではしています。
function collection2stream(collection)
{
var returnC2S = return
collection.forEach(function(elememt)
{
var returnUptoC2S = returnC2S.delimitedReturn()
returnC2S( StreamCons(element, returnUptoC2S) )
})
return StreamNil
}
こんな感じのコードのシンタックスシュガーのつもりです。 実装はちゃんと考えてませんが、.delimitedReturn() メソッドが頑張って限定継続を切り出してくるということで。 あと、型システムに関しては本気で何も考えていないです。
function foo()
{
function bar()
{
var whichNumberWillBeReturnedFromFoo = return[→foo]
for(var n=0 ;; ++n)
if( whichNumberWillBeReturnedFromFoo(n) > 10 )
return n
}
var x = bar() // fooの返値が10を越えるような最小の自然数(= 4)を返す
return x*x
}
他の使い方の例。
まず最初に。return[f→g]
というプリミティブだけでは、たぶん、
まだ shift/reset より真に弱い機能しか実現できません。
return[→g]
は lexical scope を前提としていて、つまり、
「ソースコードのテキスト上で現在の関数の外側にある関数 g の return まで」
という形で継続をキャプチャしようとします。
一方で、shift は、自分の理解している範囲では、ある程度 dynamic scope 寄りで、
つまり、「実行中のコールスタック上で現在の関数の外側にある reset まで」
の継続をキャプチャします。
たとえば、[PDF] sprintfの継続による型付け の例は、% to_str の関数定義の中の shift は、
ソースコード上でそれを囲む reset (そんなものは無いですが)までの継続を捕まえるのではなくて、
reset(fun () -> pat) args という形で使われたときに、pat の外を囲っている reset までの継続を捕まえます。
これが return[f→g]
形式だと、できない。
ただ、変数スコープでも dynamic より lexical の方が好ましい(と最近はされている)のと同じで、 限定継続もできるだけ lexical scope で書くのが最終的には好ましい、ということになったりしないかなー、 と勝手なことを考えています。根拠は何もありません。sprintf の例だと、 sprintf だけでなく % もマクロにすると shift/reset が lexical に対応がつくようになって、 どうにかなりそうな気がする。
もっとも、自分は限定継続の実際の使われ方をほとんど知らないので、
もし dynamic scope であることが本質的な使い方が多いのであれば、
ちょっと return[f→g]
だとパワー不足ですね。
実際どうなんでしょうか。あるいは、Java の Checked Exception みたいに注釈を付けることで、
dynamic なんだけど lexical っぽいものを実現するとかしてみるとどうだろう…
☆
あと lexical にしたことの問題点はもう一つあって、
function foo() {
return function bar() { return[→foo]( 100 ) }
}
var b = foo()
b()
これどういう挙動になるべきなんだ、という。b を呼ぶ時点では foo は終わっちゃっているので、
「fooのreturnまで」と言われても一体どこまで実行すればいいのかわからない。これってただの
return 100
扱いでいいんだろうか。
あるいはエラーにするとすればどのタイミングでエラーにするべきか。
☆
実装…に必要な注意としては、
function collection2stream(collection)
{
collection.forEach(function(elememt)
{
return[collection2stream] StreamCons(element, return[→collection2stream])
})
return StreamNil
}
上の方のコードの再掲ですが、return[collection2stream]
を単にフルの継続と思って、たとえば単純なコールスタックのコピーで実装するとおかしな挙動になります。
それがまさに、この例をとってきたSchemeのサンプルのcall/cc版ではset!が必要になっている理由で、
return[→collection2stream]
が呼ばれるたびに、
return[collection2stream]
の戻り先を変更しないといけません。
これの効率的な実装は、普通に限定継続の効率的な実装とたぶん通じることろがあると思うんですが、
詳細はめんどくさいので深く考えないことにします。ただ、return[f]
だけのサポートなら要らなかった工夫が、
return[f→g]
を考え出すと return[f]
側の実装にも必要になるので注意、と。
☆
return[→g]
以外の構文として、
function collection2stream(collection)
{
collection.forEach(function(elememt)
{
yield[collection2stream] StreamCons(element, resume[collection2stream])
})
yield StreamNil
}
こういうのはありかなー、というのはちょっと考えています。resume[f]は次の文からyield[f]までの継続を表す。 コルーチンに慣れている人なら行けるはず。たしか kmizu さんに教えてもらった Javaflow は、 要するにこれを更に少し制限した形でやっていることになるのかな。
age = -~age
といっても別に特別なことは何もないのですが。