TaPLのML実装をRustでやってみるシリーズ まとめ
型システム入門 −プログラミング言語と型の理論−、を読んでおります。
これをちまちまと読みつつ、せっかくなので各章にある「ML実装」の演習課題をRustにポーティングしていこうと思っています。Rustの勉強と型システムの勉強が一石二鳥かと思ったら二重苦というかなんというか、実に楽しいっすね。Rustは初めてC言語にさわったときレベルで楽しい。
できた分は以下にコード逐次公開していきます。
関数型とかの論争について
身と蓋がナッシング
まず身も蓋もないことをいうと、結局はCPUの命令セットとして動く言語ですからして、大差はないんですよね。
でもこういうことを言うのは、「どうせ宇宙なんて原子の集合だろ!」という小学生なみです。 なので言わない。でもその認識を捨ててもならない。魔法はないです。地続きではあるのです。
「命令型」か「関数型*1」か
さてその上で、問うべき問いが、「命令型」か「関数型」であるならば、関数型に明確な優位性があらわれます。イミュータブルデータ。いい響きですね。map,filter,...いいですね。for禁止、シビれます。いいか、おまえら! forは禁止だ! 了解しました軍曹殿!
この利点はあきらかで、かつ入手することが比較的容易です。scalaでもJava8でもreactでも、それなりに努力すればそれなりのものが手に入りそうです。
このレベルではOOPとの対立項はないんじゃないかと思います。併用して両方よいとこ取りすればよいでしょう。今そこにある利点。うれしいうれしい。「副作用はほどほどにね!」てなもんです。
(追記)プログラム構成方法論としてOOP vs. FP
関数型とOOPの比較の観点として、たとえばプログラムの構造がクラスの集合として構成されるのか、関数なのか、制御フローがオブジェクトに対するメソッド呼び出しやメッセージパッシングとして構成されるのか、関数や高階関数呼び出しでなのか、という構成方法の問題として語られることもあります。本論では、ここにはあまり主張はなくて、少なくとも極端なのは良くないでしょうよと。原理主義者による信仰化はおひきとりねがいたいし*2、オブジェクト指向養成ギブスとかの冗談を真にうけるのもヤメレです。
相互のエミュレーションたとえばScalazとか、HaskellのLensとかObjectiveみたいのもあることだし(理解してないけど)、結局は程度問題に見える。OOPとFPの差と、同じ関数型なら関数型同士の差(正格、非正格はともかく、純粋、非純粋の差は小さくない*3 )、OOP言語の派閥間の差(e.g. Smalltalk vs. C++)を比べると、どっちが大きいかわからなくなりそうだし、Rustみたいにどっちだかわからん言語もおるし。
優劣の問題ではなく、それぞれのスタイルの利点と欠点をちゃんと把握した上でなら、あとは好みも混じえて、解こうとする問題を、適切な労力でとける方を選べばいいじゃんでしょう。
「純粋」関数型の意義
ここらへんから、行方があやしくなってきます。関数型は良いとして、副作用を禁止までして、「純粋」にまでする必要があるのか? モのつくあれが登場してきそうです。
ブコメで純粋関数型は「状態の扱い方が普及のネックになる」旨の指摘を頂きましたが、それはその通りですね。ちなみに、実用レベルの純粋関数型言語が一生懸命にやっているのは、状態を純粋関数から分離し管理するということです。状態を「扱う」ことについては、ある意味非純粋関数以上に意識的に「扱う」のです。状態が、野放しに、プログラムの全体に渡ってばらばらに広がっているのか、そうじゃなくて言語のレベルで、言語機能や型システムのサポートのもとで例外無く強制的に管理するか、という違いです。この状態管理の強制が、純粋関数型を知らない人にとっては、敷居になり得るので、それがいかにわかりやすく普及啓蒙されるか。そのためには、つい出てくる圏論・数学・論理学用語とかがネックになり得るし、コミュニティの文化風土の問題(モヒカン問題)でもあるのかもしれません。ただ、もっと重要なのは、概念が本当に整理され、過剰な一般性や抽象化がなされすぎてはいないか、用語がわかりやすいものになっているかです。今まではそこは全然駄目で、今後、純粋関数型言語が普及していくためには、そこも本気で問われていかねばなりません。
静的でリッチな型システム
「関数型」とは直交し得るので、これが論点なのかすらについても議論があるかもしれないのが、強力な静的型システムをどう評価するのかです。多相型はいいとして、ヒンドリーは見るでしょうか見るなということでしょうか。存在型は、全称量化は、高階多相は、依存型は、リージョン型とか線形型とか幽霊型とかカリーハワード対応とか型レベルプログラミングとかどうなんでしょうか。これらは頭の良いひとたちのタワゴトなんでしょうか。正直、難しすぎやしませんかね。業務で使うとか、どうなんでしょう。我々の目の黒いうちにどうにかなる気がしませんよ。
何を未来に見据えるか?
ポイントは、現在のプログラミング言語の比較の問題ではなく、将来への流れなんだと思ってます。思うに、30年後のプログラミングというのは、極めてリッチな計算資源の元に以下が可能になっているはずです(願望)。
「 効果」「範囲」の明確な解析とそれに元付いたデバッグ、合成、分解、コンピュータによる強い支援を受けたプログラミング
たとえば、プログラムの実行結果として、Yを期待していたのにXという結果が得られたとして、その計算過程の何がYとXの差を生んだのか? 当初Xという結果だったのに、Yという結果になってしまったとき、その間に加えた変更a,b,cのどの操作がその違いを発生させたのか? aだけを適用したら、bだけを適用したら、aとcを適用したら、という順列組合せを実際に瞬時実行し、その結果を産む原因の候補としてのソースコードの変更点が瞬時にリストアップされたらどうでしょう。未来っつーぐらいだら、そうなるべきですよね。ソースの変更は、逐次意味の単位でアトミックに記録され、巻き戻したり、取捨選択して組み合わせたりが、自在にできます。gitのもっと進んだ姿。
あるいはソースを修正後に、再実行なんかはしません。個々のソースの「影響」は、分離されて記録管理されているので、特定箇所の修正は、その範囲が生むであろう「効果」がgitのfast forwardのように再適用されて、更新された実行結果を再表示するだけです*4。もちろん並列処理でのタイミングに基づく非決定性もあってはなりません。
このためには、命令型言語のように副作用を野放しにはできず、非純粋な言語でも駄目です。純粋関数型言語の延長で副作用を明示的管理しなければなりません。また、非決定性の排除が担保できたり、個々の操作の適用を分離して操作可能なのは、強い静的型の元でしかありません。合成や分割などの機械的操作の保証を得られるのも、リッチな型の元でしかありえません。数学的基盤も十全に必要でしょう。
その結果として、初めて、人間は、今は考えられないほど強力なコンピュータの支援の元で、CPUが4個とか16個じゃなく、数千数百万倍のコンピュータパワーが1人の人間がプログラミングする際に独占できるような状況下(そんなことは未来では当然できます)で、deep learningやビッグデータなんかも駆使して、より迅速に、より正しいプログラムを書けるようになるでしょう。
そのような状況に繋がる方向性は、純粋関数型や強いリッチな静的型の延長でしか有り得ません。現在は過渡期としてのそれが存在しているだけなのでその利点が十分には見えていないだけです。
まとめ
まとめると、強いリッチな静的型と結合した純粋関数型プログラミングがより重要な理由は、それが未来だからです。それが未来な理由は、コンピュータからの支援を受けるしか知的作業としてのプログラミングに未来はなく、コンピュータから支援を受ける道を開くのは、意図をより形にあらわせる静的型システムであり、分解結合が容易で副作用を客体化して扱える純粋関数型でしか有りえないからです。「名前重要」とか言ってられるのは相手が人間のときだけです。
目の黒いうちになんとかならないかなー。
*1:本エントリでは、関数型を「関数型プログミング」もしくは「関数型プログラミングスタイル」の意味であいまいに用います。命令型は命令型プログラミング
*2:じゃあ「すべてはクラス」「すべてがオブジェクト」というのがすでに信仰かどうか?ですが、まあ近いとだけ。そんなら「すべてが関数」は信仰か? うーん。うーん。ラムダこりゃ…。
*3:一方で、Haskellも*MLも同じMLファミリと考えることもできる。Erlangはしらない。
*4:こっそり追記するのもあれですが、これは驚異のタイムトラベリングデバッガ、elm-reactorがすでに実装している動作、FRPベースのリロード更新動作、を元に想像しています。
G*Magazine Vol.8に記事をかきました
G*Magazine Vol.8に、Groovy 2.3, Groovy 2.4β4までで導入された新機能の解説記事を書きました。
PDF版もありますが、ブラウザで見れる以下のリンクを紹介します。
出稿タイミングの都合で、2.4最新版には対応しておりませんので悪しからず。
G*なWeb API
Grails3もカウントダウンな感じのおり、イベント紹介です。以下引用。
G*ワークショップZは日本Grails/Groovyユーザーグループの定例イベントです。Java仮想マシン上で動作するGroovy、Grails、Gradle、Spock、vert.xといったG*技術をテーマに、ハンズオンやコードリーディングなど参加型の内容を金曜日に開催しています。
今回のG*ワークショップ"Z" 第19弾は、「G*なWeb API」と題して、昨今話題のWeb APIに関連した以下の内容です。いずれもハンズオン形式ではありません。
Grails 3でWeb APIを簡単に作ろう!
Grails 3の新機能の紹介を交えながら、GrailsでのWeb API構築がいかに容易にできるかをお話します。GrailsのREST機能と、Web Microプロファイルなどを含めてご紹介する予定です。
REAL Objects : 動的なエンタプライズAPIアーキテクチャをどう構築するか
REAL Objectsは、(例えばSmalltalkのような) メッセージ・ベースのオブジェクト指向アーキテクチャを現代の並列/分散環境で実現するものです。 現実の業務システムのような、大規模で、ドメイン・モデルが動的/多様で, 非パブリックなAPIを持つアーキテクチャをどう実現するかについて、 G*技術を (多少) 絡めつつ, お話しします (ワークショップ形式ではありません)。
講演者紹介
- 山本 和樹( @yamkazu )さん: Grails 3でWeb APIを簡単に作ろう!
- 山田 正樹( @yamadamasaki )さん: 「REAL Objects : 動的なエンタプライズAPIアーキテクチャをどう構築するか」
こちらからお申し込みください。
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\"}" ]