uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

関数型とかの論争について

身と蓋がナッシング

まず身も蓋もないことをいうと、結局は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までで導入された新機能の解説記事を書きました。

http://grails.jp/g_mag_jp/images/gmagjp_8.png

PDF版もありますが、ブラウザで見れる以下のリンクを紹介します。

出稿タイミングの都合で、2.4最新版には対応しておりませんので悪しからず。

G*なWeb API

Grails3もカウントダウンな感じのおり、イベント紹介です。以下引用。

G*ワークショップZは日本Grails/Groovyユーザーグループの定例イベントです。Java仮想マシン上で動作するGroovyGrailsGradleSpockvert.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*技術を (多少) 絡めつつ, お話しします (ワークショップ形式ではありません)。

講演者紹介

こちらからお申し込みください。

G*なWeb API - 日本Grails/Groovyユーザーグループ | Doorkeeper

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-elmshare-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シリーズの他のエントリはこちら

*1:Signalはアプリカティブでモナドではない。

Grails/Groovyのサイトを構築している、名もなき静的サイトジェネレータ

Grails/Groovyのサイトは、静的サイトジェネレータで作成されています。Groovyサイトは去年ぐらいからそうだったのですが、最近Grailsもそうなりました*1

しかしこの静的サイトジェネレータの名前がわかりません。ソースコード上は、単に「generator/SiteGenerator.groovy」で、独立したgithub projectもありません。groovy-websitegrails-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-CMSGrailsプラグインとかも遍歴しましたが、社内のサイトとかは、これからは当面これで作っていこうと思います。apacheだけでいいです。tomcatとかもういいです。

*1:ただし、一部はGrailsアプリケーションとして動作しているそうです。たぶんプラグインのページ。

*2:もちろんすぐ作れます。Gradleだし!

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)におけるstringintなどはデコーダ型の値である。
  • (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\"}"
       ]

リンク

*1:以前のものの使用例はこちらでも紹介しているが、無用のものとなった

*2:本当に何度も呼びだすかは未確認。でもたぶん呼んでる。Elmにおいて、引数が同じ文字列で、純粋関数だからメモ化される、という期待はできない。さらにもちろんElmは遅延評価ではなくサンクなんかない。Signalにliftされた関数からsignal値としてのJSONが引数に与えられているなら、JSON文字列の更新がなければ多数回呼び出されないと考えてよいが、ここで考えている例の場合はあてはまらない。

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

一般形式:

  1. import モジュール名A
  2. import モジュール階層.モジュール階層.モジュール名B
  3. import モジュール名 as 短縮識別子C
  4. import モジュール階層.モジュール階層.モジュール名 as 短縮識別子D

説明:

Qualified Importは、モジュール指定もしくは短縮識別子をプリフィックスとして、他モジュールからexportされた識別子を利用する。たとえば、上記のようにimportを行ったとき、以下のように識別子をアクセスできる。

  1. モジュール名A.識別子
  2. モジュール階層.モジュール階層.モジュール名B.識別子
  3. 短縮識別子C.識別子
  4. 短縮識別子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

一般形式:

  1. import モジュール名A(..)
  2. import モジュール階層.モジュール階層.モジュール名B(..)
  3. import モジュール名C(識別子,識別子,識別子…)
  4. 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して値コンストラクタをインポートしない」「指定した値コンストラクタだけをインポートする」などが可能である。

Qualified ImportとOpen Importの使い分け

簡潔さの程度でいうと以下の順である(上の方ほど字数が少ない)。

  1. 識別子を指定せずにOpen Import
  2. 識別子を指定してOpen Inport
  3. Qualified importで短縮識別子使用
  4. 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の章があるようです。ぽちっとな。

Seven More Languages in Seven Weeks: Languages That Are Shaping the Future
Bruce A. Tate Frederic Daoud Ian Dees Jack Moffitt
Pragmatic Bookshelf
売り上げランキング: 9,168

関連リンク

「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog