ElmのSignalについての記事は沢山上がっていますので、今回はTaskについての記事を書きます。
Tasksについて
非同期処理とGUI
Elmには、非同期処理を上手く扱う仕組みとしてTasksというものがあります。ElmはフロントエンドGUIのための言語でありますが、GUIでは非同期処理は欠かすことができないでしょう。とある、重い処理のためにユーザの操作ができないとあれば、困る場合があるからです。
さて、非同期処理は基本的にコールバック地獄になりやすく、そのためJSではPromiseなど、さまざま言語でコールバック地獄をなんとかしようとする仕組みがあります。ElmのTaskもそうした、仕組みの一つでしょう。
Tasks
さて、Tasksは、Elm 0.15から導入されました。基本的には、HTTPのリクエストや、データベースを扱いたい時などの、非同期処理 を扱うための仕組みです。
簡単なソースコード例の紹介
いろいろと説明する前に、Tasksを使ったソースコードを出してみましょう。
Httpの通信するサンプルを書いてみます。elmを書く前に、nodeで簡単なjsonを返すだけのサーバーを作りました。
var http = require('http');
var server = http.createServer();
server.on('request', doRequest);
server.listen(8080);
function doRequest(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('{"msg":"Hello World"}');
res.end();
}
さて、このサーバーにリクエストを送るサンプルを書きます。依存するライブラリは以下の通りです。
"dependencies": {
"elm-lang/core": "2.1.0 <= v < 3.0.0",
"evancz/elm-html": "4.0.2 <= v < 5.0.0",
"evancz/elm-http": "3.0.0 <= v < 4.0.0"
}
さて、以下が簡単なサンプルになります。
import Http
import Html exposing (div, text)
import Html.Attributes
import Json.Decode as D exposing ((:=))
import Task exposing (..)
import Debug exposing (log)
main =
div [] [text "Hello World"]
helloWorldTask : Task Http.Error String
helloWorldTask =
Http.get (D.object1 identity ("msg" := D.string)) "http://localhost:8080"
port helloWorldRunner : Task String String
port helloWorldRunner =
let
errorLog error =
log "error" <|
case error of
Http.Timeout ->
"timeout"
Http.UnexpectedPayload msg ->
"unexpect payload : " ++ msg
Http.BadResponse code msg ->
"bad respose, code : " ++ (toString code) ++ " msg : " ++ msg
in
mapError errorLog << map (log "respose") <| helloWorldTask
まず、一つずつ簡単に、解説して行きます。
main
main =
div [] [text "Hello World"]
Taskとか関係はありませんが、とりあえず、elm-htmlで、helloworldを表示するだけのコードです。
Task型
helloWorldTaskについて見ていきましょう。まず型からです。
helloWorldTask : Task Http.Error String
Taskという型 が出てきました。Taskという型はなんらかの非同期処理の型です。
Taskは、2つの型引数を取ります。Task型は、なんらかの処理(task)を行なう型であり、その処理が失敗時(エラー時)に受け取る型と、成功時に受けとる型を想定します。なので、2つの型引数が考えられます。
type Task x a
基本的に、xはエラー時の値で、aは成功時の値を示します。なので、helloWorldTaskは、エラーとして、Http.Error型の値、成功時にString型の値を扱う、Task型となります。
helloWorldTaskの実装を見てみましょう。
helloWorldTask =
Http.get (D.object1 identity ("msg" := D.string)) "http://localhost:8080"
Http.getは、getメソッドのリクエストを送る関数です。第一引数にレスポンスをDecodeための、Decoder型を、そして、リクエストURLのString型を取ります。
処理としては、"http://localhost:8080"
にgetのリクエストし、レスポンスとして{"msg":"Hello World"}
という文字列が返されるため、それをDecodeする処理です。
port
さて次に, portです。
port helloWorldRunner : Task String String
先程、Taskを定義しましたが、定義しただけでは、Taskは動きません。Taskを動かすためには、portを通して定義しなければなりません。
また、 portと通して定義できる値は、TaskとSignal Taskです。実行タイミングを扱うのであれば、Signal Taskを定義しましょう。
成功時と失敗時の処理
さて、helloworldRunnerの実装を見て行きましょう。
port helloWorldRunner =
let
errorLog error =
log "error" <|
case error of
Http.Timeout ->
"timeout"
Http.UnexpectedPayload msg ->
"unexpect payload : " ++ msg
Http.BadResponse code msg ->
"bad respose, code : " ++ (toString code) ++ " msg : " ++ msg
in
mapError errorLog << map (log "respose") <| helloWorldTask
さて、まずは、letを使い、errorLogという、Http.Errorを、ログ表示する関数を書いています。
errorLog error =
log "error" <|
case error of
Http.Timeout ->
"timeout"
Http.UnexpectedPayload msg ->
"unexpect payload : " ++ msg
Http.BadResponse code msg ->
"bad respose, code : " ++ (toString code) ++ " msg : " ++ msg
次に、Task.mapErrorと、Task.mapを用いて、成功時と失敗時にそれぞれ適当なメッセージをログに残すようにしています。
mapError errorLog << map (log "respose") <| helloWorldTask
<<
などを使い、関数合成しています。
Taskの基本的な概要まとめ
以上が、Taskの基本的な概要となります。基本的にTask型を作り、portを通して定義することによって、Taskが動くようになります。
Taskの合成
Signal同時が合成できたように、Taskもまた、合成することができます。それを、また先程の例を修正し、説明してきます。
コード例の紹介
import Http
import Html exposing (div, text)
import Html.Attributes
import Json.Decode as D exposing ((:=))
import Task exposing (..)
import Debug exposing (log)
main =
Signal.map (\result -> div [] [text result]) myMailbox.signal
helloWorldTask : Task Http.Error String
helloWorldTask =
Http.get (D.object1 identity ("msg" := D.string)) "http://localhost:8080"
myMailbox : Signal.Mailbox String
myMailbox = Signal.mailbox ""
sendMe : String -> Task x ()
sendMe = Signal.send myMailbox.address
sendMeError : Http.Error -> Task x ()
sendMeError error =
sendMe <| case error of
Http.Timeout ->
"timeout"
Http.UnexpectedPayload msg ->
"unexpect payload : " ++ msg
Http.BadResponse code msg ->
"bad respose, code : " ++ (toString code) ++ " msg : " ++ msg
port helloWorldRunner : Task x ()
port helloWorldRunner =
helloWorldTask `andThen` sendMe
`onError` sendMeError
上のコードは、Mailboxが出てきていますが、これは後日また改めて記事にしたいと思っています。
さて、上記では、sendMe
とsendMeError
というTask x ()
を返す関数を作成しています。
sendMe : String -> Task x ()
sendMeError : Http.Error -> Task x ()
それらをport helloWorldRunner
にて、andThen
とonError
を用い合成していまうす。
port helloWorldRunner : Task x ()
port helloWorldRunner =
helloWorldTask `andThen` sendMe
`onError` sendMeError
ここで、andThen
はTaskが成功した時にするTaskを、onError
は失敗したときにするTaskを合成しています。上記では、helloWorldTask
が成功した時に、成功時の値を受けとりsendMe
で返されるTaskが実行され、エラー時に、失敗時の値を受けとりsendMeError
で返されるTaskが実行されるのです。
まとめ
以上で、Taskを使って、elmを非同期処理を書くための概要を説明しました。
まとめますと、
- Taskは非同期処理を行なう型。成功時と失敗時の型を持つ。
- portを通して定義されるTaskが実行される。
- Taskは、他のTaskを組み合わせることができる。
ということになります。