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。

elmでやってみるシリーズ5: 逆ポーランド電卓

Elmでやってみるシリーズ5: 逆ポーランド電卓。

いわゆる一つの逆ポーランド記法(Reverse Polish Notation, RPN)電卓です。入力フォームをSignalに結びつけて入力として使う例になります。私は本物のRPN電卓を使ったことがないので本物とはたぶん動作が違います。

import Graphics.Input (input, button)

-- 電卓のボタン操作の種別
data Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter

-- GUIからの電卓に対する操作入力列を表現するシグナル。初期値はClear。
keys = input Clear

-- 画面を作る。
calculator (n, xs) =
   let btn = button keys.handle
       btnN n = btn (Number n) (show n)
   in flow down [
        flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ]
      , flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ]
      , flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ]
      , flow right [ btnN 0, btn Clear "Clear",  btn Enter "Enter",  btn Div "/" ]
      , if (n==0) then (asText xs) else (asText n)
    ]

-- 有限状態マシン(状態=入力エリアへの入力中の値とスタックのタプル)としての電卓のモデル化。
calc : Keys -> (Int, [Int]) -> (Int, [Int])
calc it (num, xs) = case it of
  Number n -> (num*10+n, xs)
  Enter -> (0, num::xs)
  Clear -> (0,[])
  _ -> if (length xs < 2)
         then (0, xs)
         else let top = head xs
                  second = head (tail xs)
                  rest = tail (tail xs)
              in case it of
                 Add -> (0, (second+top) :: rest)
                 Sub -> (0, (second-top) :: rest)
                 Mul -> (0, (second*top) :: rest)
                 Div -> (0, (second `div` top) :: rest)

-- foldpで、入力操作列シグナルに対して、「1回前の電卓状態に、入力操作を適用し、次状態を生成する」という関数をリフト適用する。
main=lift calculator (foldp calc (0,[]) keys.signal)

実行画面(操作可能)

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

気づいたこと

  • コロン(:)、ダブルコロン(::)はHaskellのそれとちょうど意味が逆になっている。既存のhaskell編集モードを流用したい場合、不便かも。
  • asパターンはない。asパターンは@ではなく、... as someのように記述。
  NG: let (x@{a,b}) = {a=1, b=2} in x
  OK: let ({a,b} as x) = {a=1, b=2} in x
  • case式でリストをパターンマッチで分解できない。具体的には「case (x:xs) of ...」などができず、(x:xs)の部分は単一の変数名しか指定できない。(x:xs)を指定してもエラーにならないので、Elmのバグではなかろうか。(本件は記述が変なのと詳細を覚えてないので削除)
  • Inputのhandleは、複数GUI部品からの入力を集約したSignalを作成するための間接参照点である。(という理解で正しいか?)
  • Elmに例外ってないみたいだが、たとえば(head [])するとJavaScriptでの実行時例外になって、プログラムは再開できなくなる(もちろんページ全体をリロードすれば操作は再開できるが)。そうならないようにチェックすれば良いのだが、そういうものか?
  • トップレベル定義の型アノテーション型推論させて表示するには、Elmコマンドの-pもしくは--print-typesでできる。しかしアノテーションはあまり必要な気がしない(逆にアノテーションが本体関数の変更に追随しないことで良くエラーになる)が、ドキュメント情報としてみると便利であろう。
  • foldlかfoldrで逆ポーランド電卓を作るのは、すごいH本の例にあったが、上のelmのはfoldpで同型。一般化すると、一連の入力シーケンスにともなって状態を変化させる処理は、適切な粒度のSignal+foldpの抽象化に対応するということか。ファイルからの入力文字ストリームはSignalにできそう。書き込みはどうか?ソケット通信やDBはどうか。

関連エントリ


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


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

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

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

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

追記: Elm 0.14対応

import Graphics.Input (..)
import Signal
import Signal(Channel)
import Graphics.Element (..)
import Text(..)
import List(..)

type Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter

keys : Channel Keys
keys = Signal.channel  Clear

calculator : (Int, List Int) -> Element
calculator (n, xs) =
   let btn x = button (Signal.send keys x)
       btnN n = btn (Number n) (toString n)
   in flow down [
        flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ]
      , flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ]
      , flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ]
      , flow right [ btnN 0, btn Clear "Clear",  btn Enter "Enter",  btn Div "/" ]
      , if (n==0) then (asText xs) else (asText n)
    ]

calc : Keys -> (Int, List Int) -> (Int,  List Int)
calc it (num, xs) = case it of
  Number n -> (num*10+n, xs)
  Enter -> (0, num::xs)
  Clear -> (0,[])
  _ -> if (length xs < 2)
         then (0, xs)
         else let top = head xs
                  second = head (tail xs)
                  rest = tail (tail xs)
              in case it of
                 Add -> (0, (second+top) :: rest)
                 Sub -> (0, (second-top) :: rest)
                 Mul -> (0, (second*top) :: rest)
                 Div -> (0, (second // top) :: rest)

main=Signal.map calculator (Signal.foldp calc (0,[]) (Signal.subscribe keys))

elmでやってみるシリーズ3: xeyesっぽい目が動く

elmでやってみるシリーズ3: xeyesっぽい目が動く。

import Mouse

-- 目の輪郭を書く
eyeframe x y = move (x,y) <| outlined { defaultLine | width <- 5 } <| oval 40 50

-- 目玉を書く
eyeball x y = move (x,y) <| filled black <| circle 5

-- 目(=目玉+目の輪郭)を一個分書く。tは目玉の見ている方向。
eye r x y t = [eyeframe x y, eyeball ((r * (cos t))+x) ((r * (sin t))+y)]

-- 目を2つ書く
eyes t = collage 200 200 <| (eye 15 -20 0 t)++(eye 15 20 0 t)

-- 「目の座標位置を計算する(=「マウス座標の原点から見た角度」をatanで求める)」という純粋関数にマウス座標をリフト適用。シグナル化された目玉の角度が返る。
mouseDirec = let f x y = (atan2 (100-(toFloat y)) ((toFloat x)-100))
           in f <~ Mouse.x ~ Mouse.y

-- 目を2つ書く、という純粋関数に、シグナル化された目玉の角度(mouseDirec)をリフト適用する。
main = eyes <~ mouseDirec


この簡潔さよ! ブラボー。Elm ブラボー。(さぼってますけどね。)

ブラウザ内で編集したり実行したい場合はこちらからどうぞ。

気づいたこと

  • SignalはElmのFRPのキモである。Signalを制するものはElmを制する。
  • mainの直下にSignalを使うReactive Codeを固めて、他はなるべくピュアにしたい、と思うじゃない。でもそれはたぶん無理な相談である。いや、やれるところはそうすれば良いと思うけど、「Signalを返す関数」はElmプログラミングにとって中核的であり最重要。それをどう組むべきかという問題からは逃げられない。
  • 「なるべくピュアにしたい」という理由は、liftの適用は演算子<~,~があろうともやっぱり可読性に難があるから。今のElmにはセクションがないし、演算子関数をliftすると演算子っぽくなくなるし、flip駆使しても限界がある。もっと言えばSignalはモナドではないし*1、仮にモナドであってもElmにDo記法は無い。で、考えたのですが、ベストプラクティスとしてのコーディングパターンは、上でもしているように、lift一発でできないところは以下のように関数ごとに純粋部とリアクテイブ部に分けることではないかと今のところ思っております*2。なお今のElmはwhere句は使えません。
func = let f x y ... = 純粋コード
      in  f <~ Signal X ~ Singal Y ... -- 必要なSignalを使用するリアクティブコード

関連エントリ


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

*1:Signalがモナドでなくアプリカティブなのは理由があって、「SignalのSignal」を禁止するためだそうな。つまりSignalにjoinは定義できない・してはならない。bind/flatMapも然り。

*2:fは無名関数でも良いかもしれない。letのセマンティクスが無名関数とその適用に差があるのか、たとえばElmにlet多相があるかどうかは不明。

elmでやってみるシリーズ2: ●を往復させる

import Window
import Debug (log)

-- 旧速度vと、現在位置xから、新しい速度を求める。(はじっこだったら速度を反転する)
newv : Int -> Int -> Int
newv v x = if (x<= -10 || 10<=x) then -v else v

-- 状態を表わす(現在位置、速度)のタプルに対して、次の状態をシグナル(fps 20)のタイミングで累積的に計算しなおすということを繰り返す。初期状態は(0,1)。結果得られるのは、(現在位置,現在の速度)を表現するシグナルである。
pos = foldp (\it (x,v) -> 
            let nv = (newv v (log "x" x))
            in (x+nv, nv)
            ) (0,1) (fps 20)

-- 指定位置x,yを中心に●を書く(半径20の円を書いて赤で塗り潰す)。
drawCircle : Float -> Float -> Form
drawCircle x y = move (x,y) <| filled red (circle 20)

-- 一連の背景線を書く
drawMatrix : [Form]
drawMatrix = map (\x -> drawLine <| x*10) [-10..10]

-- 背景線を1本書く
drawLine : Float -> Form
drawLine x = traced (solid blue) ( segment (x,-100) (x,100) )

-- 「一連の背景線と、●を書いたコラージュを生成する純粋関数f」に、現在位置(posタプルの第一要素)をシグナル化したまま取り出してリフト適用
main : Signal Element
main=let f = \w h p-> collage w h ([drawCircle ((toFloat p)*10) 10]++drawMatrix)
     in f <~ Window.width ~ Window.height ~ (fst <~ pos)


解説的な何か

  • foldpのpは、pastのpで、その意味は実際に「過去から現在までのシグナルの履歴値を畳み込む」です。しかし、foldp使用に伴なうオーダーは実行開始時刻から現在までの時間経過には依存せず、O(1)です。実際には、シグナルの更新1回ごとに1回呼び出される関数を登録するというものです。foldpの第一引数の関数の第二引数(上で言う(x,v))は前回のfoldpの返り値を保持しています。この値が、いわゆる状態を保持・更新するためのelmにおいての唯一の手段です。
  • Debug.logは、JavaScriptでいうConsole.logを呼び出します。正格評価だからその時点の値が普通に出ます。

続く。

関連エントリ


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

elmでやってみるシリーズ1: ●を動かす

elmでやってみるシリーズ1: ●を動かす。

import Window

pos : Signal Int
pos =let f = \tmp -> (tmp `mod` 30) - 10
        in f <~ foldp (\it acc-> acc + 1) 0 (fps 50)

drawCircle : Float -> Float -> Form
drawCircle x y = move (x,y) <| filled red (circle 20)

drawMatrix : [Form]
drawMatrix = map (\x -> drawLine <| x*10) [-10..10]

drawLine : Float -> Form
drawLine x = traced (solid blue) ( segment (x,-100) (x,100) )

main : Signal Element
main=let f = \w h p-> collage w h ([drawCircle ((toFloat p)*10) 10]++drawMatrix)
     in f <~ Window.width ~ Window.height ~ pos

続く。

感想

  • foldpは好き。
  • inputに対応するSignalってグローバル変数じゃね?まあ「マウスがクリックされる」とか「時間が経過する」という事実はグローバルなんだが、それによって引き起こされるイベントもそうなる…。プログラムからその値を能動的に制御できないから変数とは呼べない…のかな。でもユーザがフィールドに値を入力したり、ラジオボタンをクリックしたり、という操作によって設定項目をリアクティブに変更したその結果の、Signalである設定項目って、どう見てもグローバル変数だよね。まだよくわからない。

関連エントリ