uehaj's blog

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

elmでやってみるシリーズ6: 今日の天気予報を表示する(フォーム入力とWebAPI呼び出しとJSONで)

Elmでやってみるシリーズ6: 今日の天気予報を表示する。

今回は、以下の手順でお天気情報を表示します。

  1. Graphics.Input.dropDownとGraphics.Input.Field.fieldで、場所情報を入力させる
  2. 上記の2つはInputを共有させる
  3. 変更があったらHttp.sendGetでhttp://openweathermap.org/の各地天気を得るWeb APIを呼び出す
    • ここを選んだ理由は、認証が無いことと、CORSに対応するクロスドメインになってるからです。具体的には、レスポンスヘッダに「Access-Control-Allow-Origin: *」が付いているからです。
  4. 得られた結果をJSONとしてパースして、天気情報に相当する部分を取り出して表示する。

コードは以下のとおり。

import Http
import Graphics.Input
import Graphics.Input.Field as Field
import Json
import Dict

-- フィールドの入力値を抽象化したもの。
inp : Graphics.Input.Input Field.Content
inp = Graphics.Input.input Field.noContent

-- 文字列をフィールド入力値に変換(文字方向、テキスト選択領域などは使わないので適当に設定)
cont : String -> Field.Content
cont str =  Field.Content str (Field.Selection 0 0 Field.Forward)

-- 都市名を選択するセレクトフィールド。入力内容は、inpに伝播する。
citySelect : Element
citySelect = plainText "Select City:" `beside` Graphics.Input.dropDown inp.handle
        [ (""        , cont "")
        , ("Yokohama", cont "Yokohama,JP" )
        , ("Tokyo" ,  cont "Tokyo,JP")
        , ("Okinawa" , cont "Okinawa,JP") ]

-- 都市名を自由入力する入力フィールド。入力内容は、inpに伝播する。
cityInput : Field.Content -> Element
cityInput fldCont =  plainText "or Input City:" `beside`
   Field.field Field.defaultStyle inp.handle id "Please input city name" fldCont

-- 天気予報Web APIのURL
base : String
base = "http://api.openweathermap.org/data/2.5/weather?q="

-- 天気予報Web APIの呼び出し結果であるJSON文字列から、結果部分のみを抜き出す
getWeather : String -> String -> String
getWeather city body = case (Json.fromString body) of
  Just (Json.Object dict) -> case (Dict.get "weather" dict) of
    Just (Json.Array [Json.Object d]) -> case (Dict.get "description" d) of
       Just (Json.String description) -> "\n\nTenki of "++city++" is "++ description
    _ -> "nothing"
  _ -> "nothing"


-- 画面下部の結果表示部分
resultPart : String -> Http.Response String -> Element
resultPart city resp =
  if city=="" then plainText "Please select or input city"
              else case resp of
                Http.Success body -> plainText <| getWeather city body
                Http.Waiting -> image 16 16 "waiting.gif"
                Http.Failure _ _ -> asText resp

-- セレクトフィールドcitySelectもしくは入力フィールドcityInputのいずれかに入力された都市名のシグナル(inp.signal)に依存してHttp.sendGetを発行させ、その結果が画面下部に表示されるようにする(そういうシグナル間の依存関係を作る)。
main : Signal Element
main = let display city resp  = flow down [citySelect, cityInput city, resultPart city.string resp]
           toUrl city = base++city.string
       in display <~ inp.signal ~ (Http.sendGet (toUrl <~ inp.signal))

実行画面(操作可能)

上記のソースを変更した上で実行したい場合はこちらからどうぞ。ブラウザ内で実行できます。

気づいたこと

  • Http.sendGetの出力はいいとして、入力となるURLもSignalである。こうなってる理由はよくわからないが、
    • まずピュアな値をシグナルに変換するのはSignal.constantでも可能で簡単だから。Signalに対応させておけばピュア値もSignal値にも両対応になる、ということがあるかもしれない。Signalであれば、いちいちliftしなくてもInputの入力を繋げることができ、今回のような用途には向いている。
    • 次に、たぶんこれが正解だと思うのだが、ConcurrentFRP特有の事情がある。Elmの処理の実行は、変更伝搬を元にしている。Input Signal*1が変化したら、それに依存した依存Signal値を更新していく、という処理の流れをとることである。入力がSignalではなく、かつInput Signalでは無いSignalは、発火する(実行してそのSignalノードの値を更新する)タイミングがない。
    • signal引数が変化しないなら呼び出し自体を省略する、といった最適化の事情に関連があるかもしれないかと一瞬思ったが、haskellのIOから類推すると、返り値さえSignalであれば省略されないということが保証できそうなので却下。
  • Signalの取り回しはまだ私には難しい。結果的にはリアクティブコードがmainに集約できている。
  • 標準のJSON処理はあまりにも面倒。ダイナミックにレコード構文として扱えるような機能はないものか。
  • Graphics.Input.Fieldは、右から左に書く言語のための方向指定とか、セレクションの設定とかが必須で難しすぎる。単に文字列が得られるような簡易インターフェースも欲しい。
  • share elmは日本語が表示できない。try-elmなら問題ないんだけど保存とフルスクリーン表示ができないためshare-elmを使用。
    • 複数ファイルから構成するアプリや、imageなどをアップロードする必要がある場合、非標準(elm-getで取得する)のモジュールを使用する場合など、share-elmやtry-elmでは対応できない。いずれもこれはスニペット共有・勉強用の簡易なものという位置付けだからである。
    • だから、本格的なアプリを公開する際には、github-pagesなどを使う必要があるだろう。もちろんDB連携とかをするなら、そういうバックエンドサーバも動作させる必要がある。
  • 「型さえ合えばまず動く」という感覚がわかってきた。でも型を合せるのが大変。その意味では、前にトップレベル定義の型アノテーションはかならずしも必要ない、とか書いたけど自明ではないプログラムや、プログラムを機能拡張する場合などを考えると実質的には必須になるという気がしてきた。
  • case式でマッチしないものがあるとJSレベルでエラーとなる。「マッチしない可能性があるcase式」はエラーではないにせよ、警告してくれないものか。

関連エントリ


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


すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

*1:マウスやキー入力やタイマーイベントなど、Elmプログラムの外にあるものを直接の入力元とするSignal。