型パラメータAを含むSeq[A]をtoArrayするときには実行時型が必要
昨日のScalaハマリポイント: 型パラメータAを含むSeq[A]をtoArrayするときには実行時型が必要
例えば、次のようなSchool
クラスがあったとする。
このクラスはStudent
もしくはその子クラスのコレクションをメンバに持っている。このクラス設計が嬉しいのは、コレクションの要素型がパラメータになっているおかげで、もし全ての生徒が男子であればstudents
はSeq[Boy]
となり、このように作られたboysSchool
に対してはBoy
だけがもつメンバfightingInstinct
にアクセスできることだ。
ただし、このようなクラス設計をした時にちょっとハマってしまうのが、
School
クラスの内部でSeq[T]
をArray
に変換しようとしたときに、以下のようにコンパイラに怒られることだ。
Error: No ClassTag available for T def studentsAsArray = students.toArray ^ Error: not enough arguments for method toArray: (implicit evidence$1: scala.reflect.ClassTag[T])Array[T]. Unspecified value parameter evidence$1. def studentsAsArray = students.toArray ^
一方で、ShoolTest
でboysSchool.students.toArray
とするぶんには問題ない。
このような現象に初めて出会うと、何のことだかさっぱりだ。
何故このようなことが起こるのかというと、ScalaのArrayは実際のところJavaのArrayであり、
JavaのArrayはプリミティブ型に対して最適化された個別のクラスを用意していて、それ以外の参照型に対するArrayとクラスが違うからである。
つまり、Arrayを作るときにこれらのクラスを作り分けるために型T
がプリミティブ型か参照型かを知る必要があるわけだが、
コンパイル時の型消去のために、School
クラスの内部ではT
が参照型([T <: School]
)であることを知ることができない。
そのため、一体どのArray
を作ればいいかわからない状態に陥ってしまう。
この問題を回避するために、JavaやScalaには実行時に型情報を取得する実行時リフレクションという機構がある。 Scala 2.11では、ClassTag という オブジェクトを使ってコンパイル時に消去されるクラス型を保持しておき、実行時に取得することができる。
上記の問題の場合、以下のように書くことでTのクラス型を保持し、実行時に知ることができるため、無事参照型のArrayを作ることできるということになる。
ClassTagの詳細については、ClassTagトレイトのソースコードも参考になる。
参考:
ScalaのSeqやMapは関数である
Array
に関数オブジェクトを突っ込んでいるときに、「Arrayだって数学的には写像なんだから、Array[A => B]
をInt => A => B
だとみなせないのかな?」と思っていたら、ScalaのSeq
がPertialFunction
トレイトを継承していることを発見。ちゃんと実現されていた。
つまり、こういうことができる。
そもそも、Array
の中に関数オブジェクトを入れたものに対して添字アクセスしていても、
見た目はカリー化された関数と変わらない。
こういうのを見ると、Scalaの添字アクセスが丸括弧なことに一層の美しさを感じる。
関数だとみなせるということは、mapメソッドに渡す事もできる。
Seqは原理上インデックスしか定義域に取れないのでPertialFunction[Int, A]
を継承している。
一方、Mapは定義域の型が自由なのでPertialFunction[A, B]
を継承しており、
下のような感じで扱える。
参考:
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)らしく、どうやら この書き方がよいようである、とのことだった。適当に返した返事に、熱心に調べていて素晴らしい。
※続きもどうぞ