Continuation-passing style で複数リソースの try-with-resource 構文を入れ子なしで書く
前回の話の続き。
末尾の参考文献によれば、CPS: Continuation-passing style(継続渡しスタイル) によって入れ子なしの記述を実現する方法もあるとのことだったので、理解を深めるために書き直しながら試してみた。 Scala の継続渡しスタイル関連の関数は、scala.coutinuationsをインポートすれば使える。 ただし、2.11では標準ライブラリから切り離されてしまったので、sbtに依存関係を書く必要がある。
このスタイルでは、ある関数に、通常のように別の関数の結果値を渡す代わりに、「継続」(残りの処理)を関数として渡す。 例えば、以下の様な感じ。
これは、reset
とshift
という関数で表現される。
reset
は、「継続」として利用する処理のスコープで、shift
は継続を渡したい処理。
shift
に渡される関数の引数cont
は、reset
の内側の処理のうち、当該shift
を評価し終わった後の残りの処理。
つまり、3行目の val num =
の代入処理と、7〜8行目の処理がcont
関数として渡される。
なので、上のプログラムは、次の順序で処理される。
- 2行目:
"A"
を出力 - 3行目:
shift
の中を評価し始める - 4行目:
"B"
を出力 - 5行目:内側の
cont(1)
を呼び出す。 1
を shiftの評価値として、「継続」(cont
関数)を実行- 3行目:
num = 1
- 7行目:
(num,1)
を出力 - 8行目:num * 2 を評価する → これが
cont(1)
の返り値 - 5行目:外側の
cont
、cont(2)
を呼び出す。 2
を shiftの評価値として、「継続」(cont
関数)を実行- 3行目:
num = 2
- 7行目:
(num,2)
を出力 - 8行目:num * 2 を評価する → これが
reset
の返り値 - 1行目:
n = 4
- 10行目:
(n,4)
を出力
これを踏まえて、複数リソースのtry-with-resource 構文を考えると、次のように書ける。
14〜26行目は、省略せずに書くと
のようになる。処理の順序は以下のとおり。
- 一つ目の
shift
を評価し始める。 - 一つ目の
using
を評価。tryの中で"op"
を出力。 - 第二引数
op
に代入されているcont
を評価。cont
は2行目のval w1 =
の代入と、5行目以降。 cont
の引数には、try
の中でop(resource)
としてnew PrintWriter("col1.txt")
が渡されているので、w1にnew PrintWriter("col1.txt")
が代入される。- 二つ目の
shift
を評価し始める。 - 二つ目の
using
を評価。tryの中で"op"
を出力。 - 第二引数
op
に代入されているcont
を評価。cont
は5行目のval w2 =
の代入と、8行目以降。 cont
の引数には、try
の中でop(resource)
としてnew PrintWriter("col2.txt")
が渡されているので、w2にnew PrintWriter("col2.txt")
が代入される。- 8〜16行目が処理される。この過程で
"A"
"B"
を出力。 - 二つ目の
shift
の中のusing
で使われているcont
(5行目のval w2 =
の代入と、8行目以降)が評価し終わったことになる。このusing
のfinally
節が実行される。"close"
を出力。 - 5行目以降は全て評価したことになったので、一つ目の
shift
の中のcont
も評価し終わったことになる。このusing
のfinally
節が実行される。"close"
を出力。
結果の出力は次のようになる。
下記のStackoverflowの例風に書くと、以下のようになる。
なるほど現状ではあまり簡潔な記述とは言いがたいが、一応このような方法でも実現可能だということがわかる。 ただし、そもそも標準ライブラリには入っていないし、Githubを見ると "The Scala Delimited Continuations Plugin and Library will continue to ship with Scala 2.11.0. However, it will no longer be included with Scala 2.12." とのことなので、新規のコードをこれを使って書くということはないだろう。 前回の記事の最後にあったライブラリのように for式を使ったスタイルがシンプルで使い勝手も良い印象。
参考文献:
法律文の構文解析はとっても難しい
法律文の構文解析がいかに難しいかをよく説明する例。 この例文の中だけでも、かなり難しい要素がある。
↓以下は「一文」です。
労働者の養育する子について、当該労働者の配偶者が当該子の 1 歳到達日以前のいずれかの日において当該子を養育するために育児休業をしている場合における第 2 章から第 5 章まで、第 24 条第 1 項及び第 12 章の規定の適用については、第 5 条第 1 項中「1 歳に満たない子」とあるのは「1 歳に満たない子(第 9 条の 2 第 1 項の規定により読み替えて適用するこの項の規定により育児休業をする場合にあっては、1 歳 2 か月に満たない子)」と、同条第 3 項各号列記以外の部分中「1 歳到達日」とあるのは「1 歳到達日(当該配偶者が第 9 条の 2 第 1 項の規定により読み替えて適用する第 1 項の規定によりした申出に係る第 9 条第 1 項(第 9 条の 2 第 1 項の規定により読み替えて適用する場合を含む。)に規定する育児休業終了予定日とされた日が当該子の 1 歳到達日後である場合にあっては、当該育児休業終了予定日とされた日)」と、同項第 1 号中「又はその配偶者が、当該子の 1 歳到達日」とあるのは「が当該子の 1 歳到達日(当該労働者が第 9 条の 2 第 1 項の規定により読み替えて適用する第 1 項の規定によりした申出に係る第 9 条第 1 項(第 9 条の 2 第 1項の規定により読み替えて適用する場合を含む。)に規定する育児休業終了予定日とされた日が当該子の 1 歳到達日後である場合にあっては、当該育児休業終了予定日とされた日)において育児休業をしている場合又は当該労働者の配偶者が当該子の 1 歳到達日(当該配偶者が第 9 条の 2 第 1 項の規定により読み替えて適用する第 1 項の規定によりした申出に係る第 9 条第 1 項(第 9 条の 2 第1 項の規定により読み替えて適用する場合を含む。)に規定する育児休業終了予定日とされた日が当該子の 1 歳到達日後である場合にあっては、当該育児休業終了予定日とされた日)」と、同条第4 項中「1 歳到達日」とあるのは「1 歳到達日(当該子を養育する労働者又はその配偶者が第 9 条の2 第 1 項の規定により読み替えて適用する第 1 項の規定によりした申出に係る第 9 条第 1 項(第 9条の 2 第 1 項の規定により読み替えて適用する場合を含む。)に規定する育児休業終了予定日とされた日が当該子の 1 歳到達日後である場合にあっては、当該育児休業終了予定日とされた日(当該労働者に係る育児休業終了予定日とされた日と当該配偶者に係る育児休業終了予定日とされた日が異なるときは、そのいずれかの日))」と、前条第 1 項中「変更後の育児休業終了予定日とされた日。次項」とあるのは「変更後の育児休業終了予定日とされた日。次項(次条第 1 項の規定により読み替えて適用する場合を含む。)において同じ。)(当該育児休業終了予定日とされた日が当該育児休業開始予定日とされた日から起算して育児休業等可能日数(当該育児休業に係る子の出生した日から当該子の 1 歳到達日までの日数をいう。)から育児休業等取得日数(当該子の出生した日以後当該労働者が労働基準法第 65 条第 1 項又は第 2 項の規定により休業した日数と当該子について育児休業をした日数を合算した日数をいう。)を差し引いた日数を経過する日より後の日であるときは、当該経過する日。次項(次条第 1 項の規定により読み替えて適用する場合を含む。)」と、同条第 2 項第 2 号中「第 5 条第 3 項」とあるのは「次条第 1 項の規定により読み替えて適用する第5 条第 1 項の規定による申出により育児休業をしている場合にあっては 1 歳 2 か月、同条第 3 項(次条第 1 項の規定により読み替えて適用する場合を含む。)」と、「、1 歳 6 か月」とあるのは「1 歳6 か月」と、第 24 条第 1 項第 1 号中「1 歳(」とあるのは「1 歳(当該労働者が第 9 条の 2 第 1 項の規定により読み替えて適用する第 5 条第 1 項の規定による申出をすることができる場合にあっては 1 歳 2 か月、」と、「、1 歳 6 か月」とあるのは「1 歳 6 か月」とするほか、必要な技術的読替えは、厚生労働省令で定める。
出典元は「育児休業、介護休業等育児又は家族介護を行う労働者の福祉に関する法律」第二章第九条の二。
これくらいになると、人間でも理解するのには図とか書いて整理しないと無理で、私は10分くらいかかりました…。 対応のないカッコがあったりして、カッコの高度な意味論と、極めて難度の高い曖昧性解消が必要。
Functional pearls
Functional pearls という素敵な論文集を見つけた。 日本語の訳本
- 作者: Richard bird,山下伸夫
- 出版社/メーカー: オーム社
- 発売日: 2014/11/12
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
Research papers/Functional pearls - HaskellWiki
Scalaで関数型プログラミングをする練習がてらに、とりあえず Snake Cube を Scala に翻訳してみました。
元原稿:
http://web.cecs.pdx.edu/~mpj/snakecube/revised-SnakeCube.pdf
Haskellのコード:
http://web.cecs.pdx.edu/~mpj/snakecube/Snake.lhs
http://web.cecs.pdx.edu/~mpj/snakecube/SnakeDraw.lhs
書いたScalaのコード:
Scalaでもほとんど同じように書けますが、関数型で書くときはHaskellのほうが簡潔に記述できる印象。 というか、Haskellなんじゃこりゃ。今までまじめに読み書きしたことがありませんでしたが、ぱっと見すごく自由。 一体どんな最適化が働くのか気になります。
続きで数独もやってみました。
これも大体同じように書ける模様。 関数型のこつが少しずつわかってきた感じ。
EnumerationのvaluesはSotedSetを返すのでマップした結果もソートされる。
今日のScalaハマリポイント: EnumerationのvaluesはSotedSetを返すので、マップした結果もソートされる。
なにかやんごとなき理由があるのかもしれないが、直感には反するのでかなりのハマリポイント。 このせいで3〜4時間は費やしてしまった気がする。気をつけましょう。
型消去の結果同じ引数型になってしまうメソッドをオーバーロードする
学生さんから、「関数リテラルを引数に取るメソッドをオーバーロードするときはどうするのがスマートだと思いますか?」と聞かれて、ちょっと調べてみた。
どういう場合かというと、
みたいな場合。引数一つの関数オブジェクトはFunction1[-T1, +R]トレイトのオブジェクトであり、 型消去の結果、上記の関数は同じ型になってしまうため、このような記述は出来ない。
案としては、個別にクラスでラップしてしまえば型が区別できるので問題を回避できるが、 ラップするとオーバーヘッドがかかってしまうのが難点。 そこで、implicit conversionでコンパイル時に型変換を適用することで、この問題を回避する。
彼がさらに調べた結果、Scalaの言語仕様に関連項目が書いてある(6.26節 p.91)らしく、どうやら この書き方がよいようである、とのことだった。適当に返した返事に、熱心に調べていて素晴らしい。
※続きもどうぞ
Scala の標準ライブラリを使って try-with-resource 構文相当の表現を書く
Scalaには標準では try-with-resource 構文が備わっていないが、自分で簡単に実装できるので、一般的にはこんなかんじのコードをそれぞれ書いて使う。
標準ライブラリの範囲でできる書き方が何かないかなと模索していたら、こんな書き方ができそうだと判明。
最初はこれでちゃんとフラッシュされるのか心配したが、どうやらフラッシュされるっぽい。
ただ、この書き方は複数のリソースを使うときは書けない。
最初の書き方も、複数のリソースを使う場合には入れ子になってしまってあまり嬉しくない。 下のように可変長引数を使って書くこともできるが、第一引数で渡すリソースの数と第二引数で渡す関数リテラルの引数の数の整合性を取れないのでいまいち。
気持ちとしては、下のような書き方が出来るようにしたいのだけど、リソースの数に合わせた関数をたくさん作るしかないのだろうか。
下の参考文献によると、for式の中で foreach的に書く記法をサポートする方法もあるらしい。 自分なりに改変してみたコードがこれ。
本質的にはforeachの入れ子のままだが、見た目上はインデントが減って見やすくなるので視覚的な問題は解決している。 このスタイルが現在のこの手のライブラリの主流のようだ。
参考文献:
for 式内での変数束縛の罠
今日のScalaハマりポイント: for 式内の変数束縛のタイミングが直観に合わない。 挙動をちゃんと知っていれば理解は出来るけれど、気を使って書かなければいけないのはデメリット。
普通に読むと、i
を束縛した後、best
を束縛して、j
を回す、それが終わったらまたi
を束縛してbest
を束縛して…と読めて、結果としてWrappedArray(21, 22, 23, 24, 25)
が出力されるはずだと思うわけだけど、実際にはそうはならず、WrappedArray(1, 2, 3, 4, 5)
が出力される。
scala -Xprint:parser
で for式の部分を見てみると、
となっていて、最初のループは foreach ではなくて、 map に変換されているのが分かる。
つまり、j
のループが回る前に best
がi
のmapとして固定されてしまうわけだ。
for内の要素が不変である事が前提ならば特に問題ないのだけど、
上記のようにループ中に要素が変更される場合には、一般的な二重ループの直観とずれが生じてしまう。
期待する結果を得るには、
のように書かなければならない。これは期待通り foreach に変換される。
あるいは、すこしオーバーヘッドが出そうだが
のような書き方をしても
と、期待通りの動作となる。