Elmでやってみるシリーズ16: マウスストーカーを実装する
「リアクティブプログラミングの技術を用いてマウスストーカーを実装する - はこべブログ ♨」という記事があり、興味深いのでElmのリアクティブプログラミングで似たようなことをやってみました。
全画面表示はこちらから。
コードは以下で、プロジェクト全体はこちらにあります。
import Text import Window import Time import Mouse import List import Signal import Graphics.Element(..) import Graphics.Collage(..) import Color(..) import Signal(Signal,(<~),(~)) import AnimationFrame -- マウスの座標をCollageの座標に変換するいつもの関数 mousePos : Int -> Int -> Int -> Int -> (Float, Float) mousePos x y w h = (toFloat x-(toFloat w/2) , -(toFloat y)+(toFloat h/2)) -- ★の表示 star : Int-> Int -> (Int, Int) -> Form star w h (x, y) = Text.fromString "★" |> Text.color orange |> Text.centered |> toForm |> move (mousePos x y w h) -- ビューの定義 view : List (Int, Int) -> (Int, Int) -> Element view posList (w,h) = collage w h (List.map (star w h) posList) -- ★の座標のリストのSignalを作る stars : Signal(List (Int, Int)) stars = let trace = Time.delay 100 -- 100ms遅延を与えたSignalを生成 p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う p2 = trace p1 -- 以降、一個前の座標を追うようにする p3 = trace p2 p4 = trace p3 p5 = trace p4 p6 = trace p5 p7 = trace p6 p8 = trace p7 p9 = trace p8 p10 = trace p9 p11 = trace p10 p12 = trace p11 p13 = trace p12 p14 = trace p13 in (\a b c d e f g h i j k l m n -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n]) <~ p1 ~ p2 ~ p3 ~ p4 ~ p5 ~ p6 ~ p7 ~ p8 ~ p9 ~ p10 ~ p11 ~ p12 ~ p13 ~ p14 -- 表示関数viewをliftして★の座標をあてがう main : Signal Element main = view <~ stars ~ Window.dimensions
非常に簡潔です。
説明と注意点など
- Mouse.positionおよびTime.delayが、シグナル(beacon.jsでいうEventStreamに相当)を生成しています。ちなみに次期Elm 0.15では、Signalは名称変更(Stream/Varyingに分割)が検討されているようです
- 上記コードではマウス位置のサンプリング間隔を効率化するために AnimationFrameというコミュニティパッケージを使用しています。なので他のパッケージを使用する機能がないtry-elmやshare-elmでは実行できません。sampleOnをfps 60とかですれば、もしくはsampleOnを使用せず直接Mouse.positionを使用すれば、AnimationFrameへの依存は除去できます。
- 制限としては、ElmのCanvasベースのAPIであるGraphics.Collageを用いているため、★がifameの枠外まで追随することはありません。Nativeやportを用いてJSと連携すれば、真のマウスストーカーができるかもしれませんが試してはおりません。ちなみに次期版0.15ではJSとの連携能力が大きく変わるようです。
- 上記コードでp1〜p14を列挙していますが、例えばmapを使用してリストを生成していないのは、Signalは動的に生成できないからです。Elmでは、すべてのSignalの値の依存関係に対応する「Signalグラフ」というものがコンパイル時に静的に決定されます。そのこともあり、不定件数の「Signalのリスト(List Signal a)」というものは作成できたとしても、(Signal (List a))に変換できないのです。sequence :: [m a] -> m [a] が欲しいところですが、そういう関数はなく、定義することも型制約*1で意図的に禁止されています。
余談
もうすぐ出るらしいElm 0.15は、非常に楽しみです。
参考リンク
Elmシリーズの他のエントリはこちら。
Grails/Groovyのサイトを構築している、名もなき静的サイトジェネレータ
Grails/Groovyのサイトは、静的サイトジェネレータで作成されています。Groovyサイトは去年ぐらいからそうだったのですが、最近Grailsもそうなりました*1。
しかしこの静的サイトジェネレータの名前がわかりません。ソースコード上は、単に「generator/SiteGenerator.groovy」で、独立したgithub projectもありません。groovy-websiteやgrails-static-websiteというプロジェクトの一部としてそれぞれでカスタム化されて機能しているというだけです。
SiteGenerator.groovyは139行ぐらいでやってることもシンプルで自明なので、独立にするまでもない、ということでしょうか。処理の実行はgradleタスクでguild.graldeに書いてあります。
gradle genrateSite
こんな感じでsite/build/site配下にHTMLなどを含むサイト全体の情報が生成されます。gradle webzip
でリンク切れチェックなどが実行された上で、サイト内容をzipで固めたものが生成されます。デプロイ機能とかはないみたいです*2。
この名もなき「サイトジェネレータ」がたいへん気に入ったので紹介します。
Markup Template Engineでページを作成する
Markup Template Engineは、Groovy 2.3で導入された、HTMLを書くためのDSLであり、HTML生成を実行するGroovy標準クラスライブラリです。ドキュメントはこちら。APIリファレンスはこちら。
たとえばGrailsサイトの、pages配下にあるトップページindex.groovyは以下のような感じで記述されます。
import model.Event layout 'layouts/main.groovy', true, pageTitle: 'The Grails Framework', mainContent: contents { div(id: 'band', class: 'band') { } div(id: 'content') { include unescaped: 'html/index.html' int eventsPerRow = 3 def allEventNames = allEvents.keySet() as List int rows=allEventNames.size()/eventsPerRow + (allEventNames.size()%eventsPerRow != 0 ? 1 : 0) for(int rownum = 1; rownum <= rows; rownum++) { int offset = (rownum-1) * eventsPerRow def cssClasses = "row colset-3-article" if(rownum==1) { cssClasses += " first-event-row" } if(rownum==rows) { cssClasses += " last-event-row" } section(class: cssClasses) { if(rownum == 1) { h1 { strong "Groovy and Grails events you shouldn't miss!" } } allEventNames[offset..(Math.min(offset + (eventsPerRow-1), allEventNames.size()-1))].each { String eventName -> Event event = allEvents[eventName] article { div(class: 'content') { // Note that the event image should be 257x180 to look nice div(class: 'event-img', style: "background-image: url(${event.logo})") {} h1 { a(href: event.url) { strong eventName br() em event.location } } time event.date yieldUnescaped event.description } } } } } } include unescaped: 'html/they-use-groovy.html' }
要するに、HTMLをプログラムとして書くわけです。抽象化、モジュール化、共通部分の括りだし、合成、型チェックなどなど、プログラミング言語が持つフル機能を享受してサイトというシステムの提供機能を記述することができます。
Markup Template Engineについてはこちらの資料もどうぞ。
大規模なサイトは、構造をもったソフトウェアなのであり、「プログラムのように」ビルドやチェックができるのが利点です。さらに静的サイトジェネレータ一般の利点として、ソースコード管理システムで管理できるのがうれしいです。データをDBに保存するCMSとかは個人的には捨てるしかないです。バックアップとか面倒すぎるし、履歴を辿れないのもアレだし。かといってGititとか使ってみたこともありますが、結局私には耐えられなかったとです。
生のHTMLを使える
先のindex.groovyでは、include unescaped "html/index.html"
のようにHTML断片を使えます。
これは実際問題としては現実的です。Excelをたとえば XLS2HTMLTABLEアドインなどでテーブルに変換したりしたものをページの一部に貼る、みたいな状況ではHTMLを「そのまま」使いたいときがあり、必須です。半端なwikiとか、「不完全なHTML」だけしかつかえないCMSなどは、動的だろうが静的だろうが捨てるしかないと私は思うわけです。
「 データ」をモデルクラスとして定義、使用する
わたしはこれが一番面白いと思ったのですが、サイトで使用するデータを「モデル」として定義します。たとえば以下が「モデル」です。
- メニューの選択項目(リンクであったり、サイト内のページであったり)
- 「書籍」一覧を含むのであれば、書籍クラス(Book.groovy)
- サイトで配布物jarを列挙しているのであれば、配布物のクラス(Distribution.groovy)
さらにこれらのモデルは、sitemap.groovyというDSLで、その値を簡潔に定義できます。 sitemap.groovyの例はこちら。
Webサイトが大規模でも大規模じゃなくても、静的であっても、それがデータを扱っていることは間違いありません。静的データをHTMLのULやliでベタに書くということは、もしやたいへんに愚かしいことではなかったのか、と考え込まずにはいられません。JSで書くのもアレだし。
こちらのサイトによれば、sitemap.groovyとモデルクラスで設定するモデル群は、「低コストなデータベース」だそうです。 まさしく。コンパイル時に使用したい、データベースか。うーん、この発想はなかったな。
もちろん、既存の静的ジェネレータでも「ブログエントリ」とかはデータなんですが、もっと汎用的なもの。
リンク切れチェック
checkDeadLinks
タスクでサイト全体のリンク切れのチェックが実行されます。
事前インストールフリー
Gradleだから当たり前なんですが、サイトのプロジェクトソースさえ入手すれば、Java以外のインストールなしに、GroovyやGradle、gvmやらの明示的なインストールなしで、即刻ページ生成が可能となります。gradle wrapperってやつです。
その他
font-awesomeとかtwitter-bootstrapとかをデフォルトでだいたい含んでます。
まとめ
Spud-CMSのGrailsプラグインとかも遍歴しましたが、社内のサイトとかは、これからは当面これで作っていこうと思います。apacheだけでいいです。tomcatとかもういいです。
Elmでやってみるシリーズ15: Json.Decodeで9個以上のフィールドを持つobjectをデコードしてみる
本記事では、ElmのコアライブラリにおけるJSONデコードパッケージJson.Decodeを用いて、9個以上のフィールドをもつオブジェクトのデコード方法について説明する。
2015 08 31追記あり(andMapを使用する)
その前に
その前にJson.Decodeを簡単に説明する。 Json.Decodeは、JSONのデコード処理をするためのコアライブラリの一つである。Elm 0.14以前のものとは完全に置き換られている*1。
Jsonデコード処理の概要
一般形式としては、
decodeString : Decoder a -> String -> Result String a decodeString <デコーダ> <JSON文字列> = ...
である。decodeStringは、第二引数<JSON文字列>をJSON文字列としてパースし、<デコーダ>で指定した型のデコード結果を返却する。デコーダは1つの型パラメタaをとる多相型(Decoder a)の値であり、decodeStringにDecoder a型のデコーダを与えて呼び出したとき、パースが成功していたら、a型の結果がOk aとして返る。型の不一致やパース失敗のときはErr Stringが返る(Resultの型は、type Result error value = Ok value | Err error
)。
Json.Decodeにおいて、JSON文字列からのパース処理は、基本的にdecodeStringから始まる。
Jsonデコード処理の例
以下はJsonデコード処理の例である。
import Json.Decode (..) import Text (..) import Graphics.Element (flow,down) import List main = flow down [ asText <| decodeString string "\"abc\"" --(1) , asText <| decodeString int "3" --(2) , asText <| decodeString (list string) "[\"abc\",\"def\"]" --(3) , asText <| decodeString (list bool) "[true,false,true]" --(4) , asText <| decodeString (array bool) "[true,false,true]" --(5) , asText <| decodeString (tuple3 (,,) bool int string) "[true, 1, \"abc\"]" --(6) , asText <| decodeString ("a" := int) "{\"a\":1, \"b\":2}" --(7) , asText <| decodeString ("c" := int) "{\"a\":1, \"b\":2}" == Err ("expecting an object with field 'c' but got {\"a\":1,\"b\":2}") -- (8) , asText <| decodeString (object2 (,) ("a" := int) ("b" := string)) "{\"a\":1, \"b\":\"foo\"}" --(9) , asText <| decodeString (at ["a","b"] int) "{\"a\":{ \"b\":2 } }" --(10) , asText <| List.map (\it -> (decodeString it "{\"a\":1, \"b\":2}" )) [("a" := int), ("b" := int)] --(11) ]
- (1),(2)におけるstring、intなどはデコーダ型の値である。
- (3),(4),(5)におけるlist, arrayなどはデコーダ型の値をとってデコーダを返す関数である。いずれもJson.Decodeで定義されている。list、arrayはいずれもJSONの配列からデコードするデコーダであるが、listは結果としてList型を、arrayはArray型の値を取り出す(デコードする)。このように、デコーダは「取り出したい型、結果として得たい型」を与えるものである。JSONの配列の要素の型は一致している必要がある。
- (6)のtuple3は、3要素のJSONの配列と、それぞれの配列要素を引数としてとる3引数の関数を引数として与えると、その関数の結果をデコード型として得ることができる。「touple*」という名称だが、デコード結果として得ることができるのはタプル型に限らない。拡張レコード型だろうがリストだろうが別の関数を呼び出した何かの処理結果であろうが、なんでも御座れである。ただしたとえば返したい型がElmリストの場合は、デコード対象のJSONの配列のすべての要素の型が一致していないとデコードに失敗するであろう。
- (7)の
(a := int)
は、オブジェクトのフィールドa(Int型)をデコードするデコーダである。該当するフィールドが存在しなければErrが結果として得られる(8) - (9)複数のフィールドを同時にデコードするには
object<N>
(Nは1〜8)を用いる。ここではint型フィールドaとString型フィールドb
をデコードし、結果としてタプル(Int,String)
の値を返している。 - (10)atは、指定したフィールド
a
がオブジェクト型である場合、そのオブジェクトのフィールドb
を取得して…のようにネストしたオブジェクトを順に辿っていくためのデコーダである。
9個以上のフィールドのデコードの問題
さて、オブジェクトをデコードする場合、上記(9)で使用したように、object<N>
という関数を使用すると良い。問題は、Nが8までのしか定義されていないことである。少なっ!
9個以上のフィールドをデコードするにはどうしたら良いであろうか。一つの案は、ちまちまと:=を用いて、1個ずつフィールドを取り出せばよい。あるいは(11)のように何個かの:=に対してList.mapで一気にdecodeStringの結果を得ることもできる。しかし、decodeStringがパース処理の呼び出しそのものであり、効率が悪いという問題がある*2。さらに、結果が、Resultの配列なのも気になる。どれか1個がErrなら、全体がErrになってほしい。
この問題を解決する方法を以下に示す。
9個以上のフィールドをデコードする方法
まず、JSONのデコード結果として以下のような拡張レコードで結果を得たいものとする。タプルでも良いのだが、とりあえず。
type alias MyData = { f1 : Maybe String , f2 : Maybe Int , f3 : Maybe Float , f4 : Maybe String , f5 : Maybe String , f6 : Maybe String , f7 : Maybe String , f8 : Maybe String , f9 : Maybe String } -- 9個以上
そして、以下のようにヘルパ関数filedを準備する。
field : Decoder a -> String -> Dict.Dict String Value -> Maybe a field typ fld dic = case (case Dict.get fld dic of -- (*) Just val -> decodeValue (maybe typ) val Nothing -> Err fld) of Ok res -> res Err _ -> Nothing
ここで(*)での「Dict.get fld dic」で得られるのは、Json.Decode.Value型の値で、そこから実際の値をとりだすためにdecodeValueを使用する。ここでのdicは後述の「dict value」デコーダを用いることで得られる、「JSONオブジェクトのデコード結果としてのDictの値」である。あとはちまちまとResultやMaybeでラッピングされた値をパターンマッチングでほどいて、最後にまたMaybeでラッピングしている。
このヘルパ関数を以下のように使用して、MyDataをデコードするデコーダを定義する。
myDataDecoder : Decoder MyData myDataDecoder = customDecoder (dict value) (\dic -> Ok { f1 = field string "f1" dic , f2 = field int "f2" dic , f3 = field float "f3" dic , f4 = field string "f4" dic , f5 = field string "f5" dic , f6 = field string "f6" dic , f7 = field string "f7" dic , f8 = field string "f8" dic , f9 = field string "f9" dic })
deocdeStringに、デコーダ「dict value」を与えることが、肝心要の要点である。得られるのはキーがString、バリューがDict Json.Decode.Value
型のDict(辞書)型の値のデコーダである。ちなみに、Json.Decode.Valueは、JSONのプリミティブや配列やObjectなどを直和したADTではなく、Valueという型引数も持たない、単一のデータコンストラクタだけを持つ型である。そんな型が何の役にたつかというと、これは実はNativeのJavaScriptの中だけで意味を持つものであり、Java Script側ではJSONのノードを保持している。Json.Decode.Valueは、本来その存在を気にする必要はないようにAPIが設計されているのだが、今回のように9個以上のフィールドを一気にデコードする、とかこういうときにはおおいに気にすることになるわけである。
上記デコーダmyDataDecoderの使い方は以下のとおり。
decodeString myDataDecoder "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}"
全体のコードはShare-elm上にも置いたが、以下のとおり。
import Json.Decode (..) import Text (..) import Graphics.Element (flow,down) import List import Dict type alias MyData = { f1 : Maybe String , f2 : Maybe Int , f3 : Maybe Float , f4 : Maybe String , f5 : Maybe String , f6 : Maybe String , f7 : Maybe String , f8 : Maybe String , f9 : Maybe String } -- 9個以上 field : Decoder a -> String -> Dict.Dict String Value -> Maybe a field typ fld dic = case (case Dict.get fld dic of Just val -> decodeValue (maybe typ) val Nothing -> Err fld) of Ok res -> res Err _ -> Nothing myDataDecoder : Decoder MyData myDataDecoder = customDecoder (dict value) (\dic -> Ok { f1 = field string "f1" dic , f2 = field int "f2" dic , f3 = field float "f3" dic , f4 = field string "f4" dic , f5 = field string "f5" dic , f6 = field string "f6" dic , f7 = field string "f7" dic , f8 = field string "f8" dic , f9 = field string "f9" dic }) main = flow down [ asText <| decodeString string "\"abc\"" , asText <| decodeString int "3" , asText <| decodeString (list string) "[\"abc\",\"def\"]" , asText <| decodeString (list bool) "[true,false,true]" , asText <| decodeString (array bool) "[true,false,true]" , asText <| decodeString (tuple3 (,,) bool int string) "[true, 1, \"abc\"]" , asText <| decodeString (tuple3 (\a c b->(a,b,c)) bool int string) "[true, 1, \"abc\"]" , asText <| decodeString ("a" := int) "{\"a\":1, \"b\":2}" , asText <| decodeString ("c" := int) "{\"a\":1, \"b\":2}" == Err ("expecting an object with field 'c' but got {\"a\":1,\"b\":2}") -- (9) , asText <| decodeString (object2 (,) ("a" := int) ("b" := string)) "{\"a\":1, \"b\":\"foo\"}" --(9) , asText <| List.map (\it -> (decodeString it "{\"a\":1, \"b\":2}" )) [("a" := int), ("b" := int)] , asText <| decodeString (at ["a","b"] int) "{\"a\":{ \"b\":2 } }" , asText <| decodeString myDataDecoder "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}" , asText <| decodeString myDataDecoder "[]" == Err ("expecting an object but got []") ]
実行結果は以下のようになる。
Ok "abc" Ok 3 Ok ["abc","def"] Ok [True,False,True] Ok (Array.fromList [True,False,True]) Ok (True,1,"abc") Ok (True,"abc",1) Ok 1 True Ok (1,"foo") [Ok 1,Ok 2] Ok 2 Ok { f1 = Just "abc", f2 = Just 3, f3 = Just 3.3, f4 = Just "s", f5 = Nothing, f6 = Just "s", f7 = Just "s", f8 = Just "s", f9 = Just "s" } True
他にも良いやりかたがあったらおしえてちょう!(NativeでやるとかPortでやるのは無しとして)
まとめ
decodeStringは内部でまずはJSONをパースして、いったんかならずJson.Decode.Valueのツリーが形成されているはず。なので、そこに対してデコーダをかませていく処理は、「ツリーをパースするVisitorパターン」でありDecoderがビジターである。そしてそのVisitor=Decoderは、パーサコンビネータライブラリのように合成していく。わかってしまえばなんということはない。 ない、のだが、動的言語とかでJson扱うのとは別次元の面倒さが発生していることは否定できないなー。
しかし、8は少ないだろ、8は…。
(追記) プルリクを打ってみた: object decoder for more than 8 fields · uehaj/core@3d49629 · GitHub
(追記2)
メーリングリストで以下のやりかたを教えてもらった。
myDataDecoder = ("f1":=int) `andThen` (\x1 -> ("f2":=string) `andThen` (\x2 -> ("f3":=float) `andThen` (\x3 -> ("f4":=string) `andThen` (\x4 -> ("f5":=maybe string) `andThen` (\x5 -> ("f6":=string) `andThen` (\x6 -> ("f7":=string) `andThen` (\x7 -> ("f8":=string) `andThen` (\x8 -> ("f9":=string) `andThen` (\x9 -> succeed {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9})))))))))
Decoder.andThenはモナドのbind(>>=)です。なるほどねー。 でもこれはこれで見にくいし書きにくい。 do記法があれば
myDataDecoder = do x1 <- ("f1":=int) x2 <- ("f2":=string) x3 <- ("f3":=float) x4 <- ("f4":=string) x5 <- ("f5":=maybe string) x6 <- ("f6":=string) x7 <- ("f7":=string) x8 <- ("f8":=string) x9 <- ("f9":=string) succeed {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9}
こう行ってたとこなんだけどね。 あるいは<~と~(Haskellの<$><*>)を定義して、
myDataDecoder = (\x1 x2 x3 x4 x5 x6 x7 x8 x9 -> {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9}) <~ ("f1":=int) ~ ("f2":=string) ~ ("f3":=float) ~ ("f4":=string) ~ ("f5":=maybe string) ~ ("f6":=string) ~ ("f7":=string) ~ ("f8":=string) ~ ("f9":=string)
こう。
andMapを使う方法(追記2015/08/31)
以下で行けます。これが今のところベストなやりかたと思う。 参考: https://groups.google.com/d/msg/elm-discuss/J9ip8MqXtYM/u4aRGAlbSkcJ
import Json.Decode as Decode import Json.Decode exposing (..) import Text exposing (fromString) import Graphics.Element exposing (flow,down,show) import List type alias MyData = { f1 : String , f2 : Int , f3 : Float , f4 : String , f5 : Maybe String , f6 : String , f7 : String , f8 : String , f9 : String } andMap : Decoder (a -> b) -> Decoder a -> Decoder b andMap = Json.Decode.object2 (<|) myDataDecoder2 : Decoder MyData myDataDecoder2 = MyData `Decode.map` ("f1":=Decode.string) `andMap` ("f2":=Decode.int) `andMap` ("f3":=Decode.float) `andMap` ("f4":=Decode.string) `andMap` ("f5":=Decode.maybe Decode.string) `andMap` ("f6":=Decode.string) `andMap` ("f7":=Decode.string) `andMap` ("f8":=Decode.string) `andMap` ("f9":=Decode.string) main = flow down [ show <| decodeString myDataDecoder2 "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}" ]
リンク
Elmにおけるimportの使い方
Elmにおけるインポートの方法を説明する。本説明が対応するElmのバージョンはElm 0.14かそれ以降、ただしインポートの仕様は今後大きく変更される可能性がある(今後についての関連情報)。
他モジュールで定義され、エクスポートされた識別子(型コンストラクタ、値コンストラクタ、型エイリアス、定数・関数など)は、インポートすることで始めて使用可能となる。Elmのimportは、この点でJavaのimportと異なっている。Javaではimport宣言をしてもしなくてもFQCNを指定すればクラスパスにあるすべての識別子を参照可能であるが、Elmでは、インポートの明示的な宣言をしない限り、他モジュールの識別子を利用できない(ただし、いくつかのモジュール・識別子はデフォルトでインポートされている。後述)。
インポートの宣言をする方法は大きく分けて2つある。
- Qualified Import
- Open Import
(Open Importはunqualified importと呼ばれることもあるようだ)。いずれもソースファイル冒頭(モジュール宣言のすぐ後)に記述する。以降、それぞれについて説明する。
Qualified Import
一般形式:
- import モジュール名A
- import モジュール階層.モジュール階層.モジュール名B
- import モジュール名 as 短縮識別子C
- import モジュール階層.モジュール階層.モジュール名 as 短縮識別子D
説明:
Qualified Importは、モジュール指定もしくは短縮識別子をプリフィックスとして、他モジュールからexportされた識別子を利用する。たとえば、上記のようにimportを行ったとき、以下のように識別子をアクセスできる。
- モジュール名A.識別子
- モジュール階層.モジュール階層.モジュール名B.識別子
- 短縮識別子C.識別子
- 短縮識別子D.識別子
3,4の「短縮識別子」はこの記事を書く際に便宜上付けた名前であり正式名称は不明。 短縮識別子は短かい必要はないが1文字が多く使われる(Signal→Sなど)。4.のように階層のある名前例えば「Graphics.Element.Field」に「Field」という短縮識別子を付けるなどもできる。
短縮識別子は同じものを複数回指定できないので、Qualified Importでは識別子の衝突が発生しない。(モジュール名があらかじめ衝突していない前提であるが。)
中置演算子のインポートは、Qualified Importではできない。後述のOpen Importを使用する必要がある。
例1:
import Text main=Text.plainText "abc"
例2:
import Text import Graphics.Element main=Graphics.Element.flow Graphics.Element.right [ Text.asText "ABC" ]
例3:
import Text as T import Graphics.Element as Element main=Element.flow Element.right [ T.asText "ABC" ]
例4
import Text(asText) import Signal as S -- 短縮識別子S import List as L -- 短縮識別子L import Signal(..) main = S.map asText <| constant ( L.map (\x -> x+1) [1,2,3] )
Open Import
一般形式:
- import モジュール名A(..)
- import モジュール階層.モジュール階層.モジュール名B(..)
- import モジュール名C(識別子,識別子,識別子…)
- import モジュール階層.モジュール階層.モジュール名D(識別子,識別子,識別子…)
説明:
Open Importは、指定したモジュールで定義・エクスポートされた識別子をすべて(1,2のケース)あるいは列挙したもの(3,4のケース)を、importする側の名前空間に取り込んで、前置指定も無く利用できるようにする。識別子の衝突が発生する可能性があり、その可能性は、1,2>3.4である。
例5
import Text(..) main=plainText "abc"
例6
import Text(..) import Graphics.Element(..) main=flow right [ asText "ABC" ]
例7
import Text(asText) import List((::)) -- 中置演算子::のOpen Import import Signal import Signal(constant) -- Qualified ImportとOpen Importを組合わせる main=(Signal.map) asText (constant (2::[3,4,5]))
型のOpen Importについて
判別共用体型(Tagged Union, ADT)は、「型コンストラクタだけをimportして値コンストラクタをインポートしない」「指定した値コンストラクタだけをインポートする」などが可能である。
- import Maybe ( Maybe ) -- 型コンストラクタMaybeのみ
- import Maybe ( Maybe(..) ) -- 型コンストラクタMaybeとすべての値コンストラクタ
- import Maybe ( Maybe(Just) ) -- 型コンストラクタMaybeと値コンストラクタJust
Qualified ImportとOpen Importの使い分け
簡潔さの程度でいうと以下の順である(上の方ほど字数が少ない)。
- 識別子を指定せずにOpen Import
- 識別子を指定してOpen Inport
- Qualified importで短縮識別子使用
- Qualified importで短縮識別子使用しない
mapなどいかにも重なりそうな名前があるので、書き捨てコード以外は3か4、最悪でも2を選んでおくのが安全ではあるが、衝突は滅多に無い気もするな…はて。出現頻度が高いものは簡潔に書きたいので、importするモジュールごとに、そして書く内容によってそれぞれ判断したい気もする。elm-htmlとか既存のコードでどうやってるかを参考にするのも良いのではないかと。
デフォルトのimport
任意のElmプログラムで、以下が指定されているのと同じ動作をする。 (参考の下の方 )
- import Basics (..)
- import List ( List )
- import Maybe ( Maybe( Just, Nothing ) )
- import Result ( Result( Ok, Err ) )
- import Signal ( Signal )
Listの::や、Signalの<~, ~が(Elm 0.14では少なくとも)デフォルトインポートされていないことに注意。前はElementとかSignal(..)などもデフォルトインポートされてたんだけどな…(´・ω・`)。
おまけ:
割と最近出た書籍「Seven More Languages in Seven Weeks: Languages That Are Shaping the Future」にはElmの章があるようです。ぽちっとな。
関連リンク
「Elmでやってみる」シリーズのまとめエントリ
このブログ記載のElmの記事で、動作するElmの実行プログラムをiframeで貼っていたのですが、いくつも動かなくなっていたので、動くようにしました。 ちなみに動かなくなっていた原因は2つありました。
- Elmのコード共有サイトshare-elmの仕様がかわった。対処としてはリンクをはりなおしました。
- elm-runtimeへのリンクが、src="http://elm-lang.org/elm-runtime.jsで参照していたのが、404エラーになるようになったため。そうなった正確な理由は不明ですが、バージョンに依存するはずなので、いずれにせよこの絶対リンクが上手くうごかなくなるのはあきらかでした。対処としてはgithub-page上の固有のものにリンクをはるようにした。
たいへん失礼しました。以下のとおりです。今はすべて動くはずです。
- 「プログラムでシダを描画する」をelmで描画する - uehaj's blog
- elmでやってみるシリーズ1: ●を動かす - uehaj's blog
- elmでやってみるシリーズ2: ●を往復させる - uehaj's blog
- elmでやってみるシリーズ3: xeyesっぽい目が動く - uehaj's blog
- elmでやってみるシリーズ4: 画像ギャラリー - uehaj's blog
- elmでやってみるシリーズ5: 逆ポーランド電卓 - uehaj's blog
- elmでやってみるシリーズ6: 今日の天気予報を表示する(フォーム入力とWebAPI呼び出しとJSONで) - uehaj's blog
- elmでやってみるシリーズ7: elm-htmlでTwitter Bootstrapを適用 - uehaj's blog
- elmでやってみるシリーズ8: 赤いドットが回っているかのように見えるけど実は直進運動な錯視 - uehaj's blog
- elmでやってみるシリーズ9: JavaScript連携(JSのevalを呼ぶ) - uehaj's blog
- elmでやってみるシリーズ10: ボールの衝突回数で円周率を計算する - uehaj's blog
- Elmでやってみるシリーズ11:お絵描きツール - uehaj's blog
- Elmでやってみるシリーズ12:遅延ストリームで多段階選抜 - uehaj's blog
- Elmでやってみるシリーズ13:あらためてシダを描く - uehaj's blog
- Elmでやってみるシリーズ14:ライブラリを公開する - uehaj's blog
- Elmにおけるimportの使い方 - uehaj's blog
- Elmでやってみるシリーズ15: Json.Decodeで9個以上のフィールドを持つobjectをデコードしてみる - uehaj's blog
- Elmでやってみるシリーズ16: マウスストーカーを実装する - uehaj's blog
Elm関連記事へのリンクは、これからはこのエントリにも追記していきます。
書籍紹介
「Seven More Languages in Seven Weeks: Languages That Are Shaping the Future」にはElmの章があります。Elmの概要や作者インタビューなどもさりながら、他のどマイナー言語(IdrisやFactor, Julia, miniKanren等)の紹介も興味深かったです。
enumに継承を! traitとenumの妙な関係、もしくはGrailsのドメインクラスの選択フィールドを国際化表示するのにtraitが便利
Grailsのドメインクラスにおいて、いくつかの候補の数値のいずれか、というフィールドを作成し、scaffoldで生成した画面からCRUD操作したいとします。
簡単なのは、こうですね。
class Domain { Integer something static constraints = { something inList:[1,2,3] } }
しかし数値フィールドに量としてではなく個々の値にそれぞれの意味がある場合、たとえば、腹の状態を表すフィールドの値として、
- 1: はらぺこ
- 2:まんぷく
- 3:こばらがへった
を表現するようなものだったとします。このとき、すくなくとも、Scaffoldの画面上では数値ではなく意味のわかる文字列で表示したり選択入力させたいわけです。しかしデータそのものを文字列にするのもDB上の表現をそうしたくないのでいやだとします*1。
ということでenumの出番です。
class Domain { enum OnakaStatus { STARVATION(1), SATICEFIED(2), SOSO(3) private final int id; private OnakaStatus(int id) { this.id = id; } public int getId() { return id; } } OnakaStatus onakaStatus static constraints = { // onakaStatus inList:[OnakaStatus.STARVATION,OnakaStatus.SATICEFIED,OnakaStatus.SOSO] // よく考えるとこんなconstraintはいらない。enumなんだから。 } }
"STARVATION","SATICEFIED","SOSO"を選択肢とするセレクト/オプションタグをscaffoldが生成し、FORMパラメータとしてはその文字列、DB中には対応する数値(1,2,3)が保存されます。ナイス! GORMナイス!
でもこれだと表示が日本語にならないし。"STERVATION"の代りに"はらぺこ"というenumの識別子を使えば表示が日本語にはなるが、FORMパラメータとしてその日本語文字列が使われるのは好きくない。ではこうしてみてはどうか。
class Domain { enum OnakaStatus { STARVATION(1, "はらぺこ"), SATICEFIED(2,"まんぷく"), SOSO(3,"こばらがへった") private final int id; private final String value private OnakaStatus(int id, String value) { this.id = id; this.value = value; } public int getId() { return id; } String toString() { return value } } OnakaStatus onakaStatus }
一応これで識別子は英字のまま表示を日本語にできます。しかしながら、国際化対応ができません。
ぐぬぬ。
そういう場合は、enumにorg.springframework.context.MessageSourceResolvableを実装させて、以下のメソッドを定義し、
enum OnakaStatus implements org.springframework.context.MessageSourceResolvable { : public Object[] getArguments() { [] as Object[] } public String[] getCodes() { [ name() ] } public String getDefaultMessage() { "?-" + name() } }
この上でi18nメッセージをenumの要素をキーとして用意します。たとえば、grails-app/i18n/なんとか.propertiesに、
STARVATION=はらぺこ SATICEFIED=まんぷく SOSO=こばらが減った
でも、enumごとにメソッド追加するの〜! えー冗長。やだーやだー。
ぐぬぬぬ。
共通するメソッドを親クラスに切り出したいところですが、enumはクラスからの継承(extends)はJava/Groovyの言語仕様上許されておりません。interfaceからのimplementsなら可能なのだが。はっ!!
ということでtraitを使えばいいのです。
trait I18nEnum implements org.springframework.context.MessageSourceResolvable { public Object[] getArguments() { [] as Object[] } public String[] getCodes() { [ name() ] } public String getDefaultMessage() { "?-" + name() } }
こういうtraitを用意して、enumはこうして
enum OnakaStatus implements I18nEnum { STARVATION(1), SATICEFIED(2), SOSO(3) private int id; private OnakaStatus(int id) { this.id = id; } public int getId() { return id; } }
あらすっきり*2。
enum継承できないのでメソッドを共有させることが本来はできないわけですが、implementsはでき、試したところtraitからもできた。なのでenumでメソッドを共有化できました。めでたい。Groovy便利。
Java8のデフォルトメソッドでどうかは不明。
関連記事
実装から学ぶ型クラス…Groovyで型クラスを実現することを例にして
これは2014年のG*アドベントカレンダーの第23日目の記事のつもりでしたが、12時すぎてしまいましたorz。
HaskellやScalaやRustには型クラスという言語機能があり、個人的感想として、理解が難しかったものの一つです。いわく、インターフェースのようなもの。いわく、オープンクラスのようなもの、など。
わからなければ作ってみるのが一番です。なのでGroovyで型クラスを実装してみました。
ソースはこちら。
ただし実用的なものではなく、学習用です。また実装したのは概念のコア部分のみで、言語によって詳細は異なることに注意ください。
型クラスとは何か
型クラスとは、多相型・ジェネリクス型の型引数(仮型引数)に対して、「ある型に対して可能な操作の集合」を、制約として与え、またそれらの操作が可能であるという保証を、「クラスの継承関係」とは無縁の方法で与えるものです。
別の言い方で言うと、「クラスにデータを従属させる」「メソッドとデータを一体として管理する」「代入可能性を、クラス継承関係で与える」というようなOOPの考えかたの侵襲を拒否し、クラス継承を使用せずに、型引数を制約する方法です。
型クラスが無い場合、たとえばGroovyにおいてメソッドジェネリクスを使用した以下のコード
static<T> void runSomething(T a) { a.run() }
を、正しいコードとして静的型チェックを通すためには、
static<T implements Runnable> void runSomething(T a) { a.run() }
のように「TはRunnableを継承している」と指定します。Runnableという操作の集合による制約を、引数aの型Tに対して行ない、それによってaに対して特定の操作ができるという保証もしています。
これはこれで良いのですが、型クラスが必要になる動機は、ジェネリクス型変数の制約はしたいのだが、このようなクラス継承の使用を避けたいということです。
クラス継承による制約の何が嫌なのか
なぜ避けたいかといえば、
- クラス継承がないから(Haskellの場合)
- 多相型変数Tを「<T extends XXX>」のように制限するためには、TがXXXのサブクラスか実装クラスとして宣言されている必要がある。もし、渡したい引数のクラスがもともとそのように定義されていない場合、
- Tとして渡すクラスのソースを変更してimplements XXXを追加したりメソッドを追加する。しかし使用用途によって都度クラス定義が変化させたり、追記していかなければならないのが嫌
- Tとして渡すクラスを継承し、XXXを実装するクラスを定義して、新規インスタンス作ってメンバをコピーして渡す。でもコピーでは意味が変わってしまうので嫌(ラッパーと同程度、もしくはそれ以上に嫌)。Scalaのnew with Traitも同様。
- Tとして渡すクラスから継承させることがなんらかの理由(プリミティブ型であったり、finalクラスである場合)で不可能だったり、嫌であるなら、以下の何れかの方法でラッパークラスを使う:
- XXXを継承するクラスのメンバーとしてTを定義する。
- Groovyのトレイトの動的proxy。
- →でもラッパーを導入するとアイデンティティが破壊されるのでいやだ。オーバーヘッドが嫌だ。
- 関数型プログラミングスタイルの一流派としてあり得る方針としての「データと関数の分離」に反するので嫌なのだ
- 「継承不要」「ラッパー不要」というのはパラメトリック多相*1で普通にできていた話なのに、制約を付けようとした時点で必要になるというのは嫌だ。なるべく同じようにやりたい
- なんでもクラスにするという基本方針がそもそもの間違いで、どう覆い隠そうとしてもシンプルさが損なわれる(本質的批判)
- オブジェクトが保持するvtableへのリファレンスなどのメモリコスト、vtableを介在したメソッド呼び出しの実行時コストを避けたい*2。また、データ型のメモリイメージ表現のC言語などとの互換性を常に保持したい*3。
- 仮想関数による多態性イラネ。ていうより、原語では同じ単語で多相性とかぶってるかぶってる。
- 操作のレシーバーという概念は非対称でいやだ。レシーバーと他の引数、戻り値の型に非対称性があり、数学的概念の記述などでの一般性を阻害する
など。まるでイヤイヤ期の幼児のような、嫌だ嫌だ、の連発です。まあ嫌なのだからしょうがない。私もこう列挙されると嫌な気になってきます。
型クラスの仕組みの4段階
型クラスの仕組みを以下の4段階で考え、Groovyで実装していきます。
- (1)型クラスを定義する
- (2)型クラスのインスタンス化
- (3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する
- (4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す
それぞれに付いてモノイド型クラスのコードを例にして解説します。
ちなみに、今回サンプルとしては以下の型クラスを作っています。
型クラス interface Monoid<T> { interface Functor<F> { interface Applicative<A> extends Functor<A> { interface Monad<M> extends Applicative<M> { interface Show<T> { trait Eq<T> { // One of eq or neq, or both should be overriden. trait Ord<T> extends Eq<T> { インスタンス class OptionalMonoid<T> implements Monoid<Optional<T>> { class ListFunctor implements Functor<List> { class OptionalFunctor implements Functor<Optional> { class ListApplicative extends ListFunctor implements Applicative<List> { class ListMonad extends ListApplicative implements Monad<List> { class IntShow implements Show<Integer> { class StringShow implements Show<String> { class ListShow<T> implements Show<List<T>> { class IntEq implements Eq<Integer> { class IntOrd implements Ord<Integer> {
(1)型クラスを定義する
モノイド型クラスを定義します。型引数に適用する制約を定義するものです。
@TypeChecked interface Monoid<T> { def T mappend(T t1, T t2); def T mempty(); }
このようにします。Tではなくdef Tなのは、なぜかそうしないとコンパイルが通らないから。
見るとわかるように普通のインターフェースです。ここでインターフェースにする理由は、継承が使えるからです。例えば、Monad extends Functorのように型クラスを継承することができます。
これは先程の「クラス継承を不要としたい」に反するように思えますが、実際にはthisからのオフセットによるインスタンス変数の参照やアクセス、thisを介した仮想テーブル(vtable)とその呼び出しの機構は完全に不要で、メソッド置き場として使っているにすぎません。なので、ここでクラス継承を使用するのは便宜的なもので、本来なら、staticメソッドにして、staticメソッドを継承階層をたどって探索してくれる仕組みをAST変換で作り込めば良いところです*4が、手をぬきます(Scalaではobjectにするところ)。Monoidはデータメンバを持たない(インターフェースなので持てませんが、classで実装したとしても持たせない)ことに注意ください。
ここでやっていることをまとめると、
(2)型クラスのインスタンス化
型クラスのインスタンス化とは、制約を与えたいデータ型が、ある型クラス制約を満たす、という宣言であり、満たせるようにするための操作を補充します。
以下では、String型がMonoid型クラスの制約を満たすように、「StringのMonoid型クラスのインスタンス」を作成します。
def instance_Monoid_java$lang$String_ = new Monoid<String>() { @Override String mappend(String i1, String i2) { i1+i2 } @Override String mempty() { "" } }
型クラスのインスタンスを保持する変数名を、「instance_Monoid_java$lang$String_ 」という決められた規約に従うものにしておきます。これはスコープの探索をサボるためです。変数名がインスタンスを決め、同名のインスタンスがあれば、内側の物が勝ちます*5。この変数名の変数をstatic importすることも可能であることも期待します。
ここまでは、何の変哲もない標準のGroovyの機能で実現することです。
(3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する
さて、準備した型クラスMonoidの制約を使って、多相関数を定義してみます。
以下のようにします。
@TypeChecked class Functions { static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) } static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() } }
引数「Monoid<T> dict=Parameter.IMPLICIT)」が本実装における型クラス制約の宣言であり、意味は、mappendの返り値と、引数で使用する型変数Tは、Monoidという型クラスの制約を満たす必要がある、ということです。
dictは型クラスMonoid型の、型クラスのインスタンスが渡ってくる引数ですが、こちら側から見ると関数の名前空間だと思ってください。引数にあたえられた名前空間に転送します*6。複数の型クラスが引数に与えられたら区別して振り分けます*7。
IMPLICITは以下のように定義してあるただの定数で、AST操作の際のマーカーとして機能します。
class Parameter { static final Object IMPLICIT = null }
ちなみにここで、mappend,memptyが型クラスインスタンスの同名メソッドを呼び出しているだけの関数なのは、無駄に感じるかもしれません。その意味では、ちょっと例がわるくて、Monoid型クラス制約のついた型引数を使った、任意の静的関数が定義できます。ここでmappendやemptyをstatic関数として定義してるのは、利便のため、ユーティティ関数としてこれらをstatic空間にexportしていると思ってください。Haskellだと、インスタンス宣言で、staticな型クラスインスタンスがモジュール内グローバルに存在を開始し、関数呼び出しはそのモジュール内グローバルな空間からマッチする名前が呼ばれることになるんですよね。このGroovy型クラス実装は、Scalaでも同様だと思いますが、名前空間として機能する型クラスインスタンスを明示的に扱うので、1つクッションが入る場合があるということです。善し悪しはあるでしょうが、Haskellの方がシンプルなのは確実です。
(4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す
ここからが、そしてここのみが、山場です。:)
先程定義した関数を呼び出します。以下のようにします。
import static Functions.* : @TypeChecked(extensions='ImplicitParamTransformer.groovy') : String s = mempty() assert s == "" assert mappend("a","b") == "ab"
このコードが静的型チェックのもとでコンパイル・実行できます。
ソースコードの表面上は、関数定義での仮引数Monoid<T> dictに対応する実引数は与えていませんが、暗黙に型クラスのインスタンスが補充されて呼び出されます。これをやってるのが今回作ったImplicitParamTransformerであり、カスタム型チェッカ(型チェッカ拡張)でありながら、型チェッカであることを踏み越えて、以下を実行します。
- ジェネリクス型を使っている静的メソッド呼び出しにおいて、その静的メソッド定義で「IMPLICIT」をパラメータ初期値で使用しているなら、そのパラメータに、ジェネリクス型から定まる変数名を挿入する。呼び出しが、「mappend("a","b")」であるなら、mappendの定義が
static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) }
であるので、実引数の型から型引数の実際の型を推論・解決させて、Tにjava.lang.Stringを束縛して、IMPLICITに対応する引数を、Monoid<String>型のinstand_Monoid_java$lang$Stringという名前の変数の参照に変換します。ついでに、ごく限定されたターゲット型推論を、「String s = mempty()」の形のとき実行し、
static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() }
では、戻り値が代入される変数の型がStringであることから、Mooid<T>の型がMonoid<String>であると推論させ、変数名を決定します。
最終的には、これらのmappend,memptyは以下の呼び出しと等価になり実行されます。
String s = mempty(instance_Monoid_java$lang$String_ ) assert s == "" assert mappend("a","b",instance_Monoid_java$lang$String_ ) == "ab"
要はScalaのimplicitパラメータの機能を実行していることになります。Scalaの場合は変数名は無意味で型だけで探索するわけですが、特定の型を持った変数定義の探索が面倒なので変数名に型情報をエンコーディングしてスコープ解決させています。
直感的な説明
型クラスのざっくりイメージ的な理解の仕方としては、クラスベースOOPでは、データ型にvtableを組み込み不可分にしていたのを、ひっぺがして、動的な束縛機能を除去して、本当にそれが必要になる最後のタイミング、つまり多相関数呼び出しの時点まで結合を持ち越して、そしてその時点で多相型を解決の枠組みでインスタンスを選定し、別引数で渡し、関数の名前空間として使うということです。
他にやるべきこと
あとはいろんなシンタックスシュガーを導入できると思いますが、学習用としてはここらで打ち止めておきます。
問題点
不都合がありまして、モジュール化ができてません。たとえばEqクラスとかOrdクラスとかそれぞれを別クラスにしたかったのですが、一つの大きな誤算として、多相型のメソッドをstatic importしてもジェネリクス情報が得られません。
class Functions { static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) } :
を、
import static Functions.mappend
すると、引数や返り値のジェネリクス情報が呼び出しコード上の処理段階で不明になります。なので(4)でのジェネリクスの推論ができません。解決策はなんでしょうね。クラスファイルに残っているジェネリクス情報をリフレクションとかでたどるんでしょうかね。たいへんなのでこれ以上の追求は個人的にはあきらめます。
おまけ: Higher-kind Generics
Groovyでは、型引数をとる型構築子を型引数に(いわゆる高階型変数(Higher-kind Generics))したときに期待する動作を行います。たとえば、今回以下のようなコードが書けています。
@TypeChecked interface Applicative<A> extends Functor<A> { public <T> A<T> pure(T t) public <T,R> A<R> ap(A<Function<T,R>> func, A<T> a) // <*> :: f (a -> b) -> f a -> f b }
ここでAは引数を取れる型引数であり、たとえばMaybeやListなどを与えることができます。これはJavaのジェネリクスでは(少なくともJava8では)できないことです 。これができることによって得られる抽象化能力は、非常に大きいものです。モナドはこれがあるから型クラスになり得るのです。
とはいえ、これをもってGroovyでHigher-kind Genericsができる、と言ってよいかどうかは、微妙です。というのも、しなければならないことは「期待する動作を行う」だけではないからです。具体的には、上記を継承するインスタンスで、上記のジェネリクス条件を満たさないオーバーライド定義したときに、型エラーになること、が必要だと思っています。実際試してみると、Groovy 2.4-beta-xでは期待したようにエラーにはならないケースがあるようです。
エラーチェックが正しく出来ないなら、型チェックがなされない=動いてしまう、という結果になるのは明らかです。綿密に調べてもしバグならバグ報告したいところですが、できておりません。
ということで結論は保留です。今のところわかりません。
まとめ
- 型クラスの基本は実装がわかれば理解できる
- ただし言語ごとの実装戦略や、シンタックスの差異は当然あるのでそこは別途。
- 型クラスの構成部品はOOPから流用可能だが、コンセプト的に根本的に相入れないからこそ、型クラスが存在する
- 型クラス=FPではない。その実現に寄与する周辺機能にすぎない。但し静的型および多相型に強く結びついた重要で強力な機能である。FPにおける型クラスは、OOPにおけるクラス継承に比するべき概念である(クラス継承はOOPに必須ではない、ということとも同型)。
- Groovyはコンパイラやランタイムに手を入れずに、カスタム型チェッカやAST変換でかなりのことができる
- 静的型や多相型に関してもある程度のことができる。ただし、静的型チェッカの多相型処理を読み込むのは覚悟がいる。
- Groovy2.xの静的型チェッカの多相型サポートは、激しく進歩している。Javaのそれより強いのは間違いない。
- カスタム型チェッカ(型チェッカ拡張)は、フルのAST変換を書くのにくらべて非常に便利。ただし実行フェイズや、ハンドラを書けるイベントが決まっているので目的とする処理に適合しない可能性がある。
おまけ2:アドホック多相
アドホック多相という言葉は使いませんでしたが、これはこういうことなんじゃないかと思っています(オレオレ理解)。。
- パラメトリック多相: 処理対象のデータ型の特定をしないで処理を記述できるような、処理記述の柔軟性。ただし、処理対象となるデータに対して実施できるのは、ポインタ操作やバイトコピーなど、内容や構造に依存しないような操作のみに限る。典型的にはコンテナへの出し入れ対象として扱うだけ、とか。
- アドホック多相: 目的は同様だが、パラメトリック多相では達成できない、「データの内容に依存した処理」を可能とするために、データと独立した「操作の集合」を事前に定義し、それを関数呼び出し時のデータ引数を渡す時に、引数データの付随情報として与える。(その際に操作の集合の選定を多相型の型推論で元にして自動的に実行する。)
これらの概念は、実装方式とは独立である。たとえばパラメトリック多相についてポインタで実現するケースもあると思うが、型毎に関数実体を適切なタイミングで生成して使っても良いであろう。
*1:パラメトリック多相とは、型引数の制約が無い場合、つまりコンテナに入れる、出す、といった、データ固有の操作を行わない引数を多相にするときに使える。
*2:可能かどうかは実装による。
*3:余談になるが、多言語間呼び出しでは、OOPが長期的には無価値であることが実証されてきたと言うしかない。CORBA/RMIを見よ。SOAPを見よ。WebAPIはメソッドを持たないJSONを返す
*4:@InheritConstructorはすでに似たようなことをしているのでそれを真似すればよい。
*5:Scalaとの相違点:確かScalaでは衝突をコンパイル時エラーにするんじゃなかったかな。
*6:この振り分けは、Haskellなどのイレイジャではないジェネリクス環境で理想的な条件下であれば、静的に決定できるとされている。未確認だが。
*7:Groovyのwith句を使っても良い。