API でデータを取得して表示させる処理を PHP と Elm で比較してみた


## 比較しようと思った動機

同僚と Elm の話になり、 Elm はコンパイルさえ通れば事実上実行時エラーはないという話と、実装コスト(特に初期実装)はめちゃ高いという話をした。Elm は純粋関数型言語なので前者は理解してもらえたようだが、後者の性質について実際に物を見せて比較した方がわかりやすいと思い PHP と比較してみようと思った。

## やること

http://localhost:3333/text.json から、

```json
{ "id": 1, "name": "hoge" }
```

という JSON を受け取って、

```html
<div>
Id: 1
<br>
Name: hoge
</div>
```

と HTML で表示させることを考える。

## PHP での実装

```php
<?php
$json = file_get_contents('http://localhost:3333/text.json');
$user = json_decode($json, true);

echo '<div>';
echo 'Id: ' . $user['id'];
echo '<br>';
echo 'Name: ' . $user['name'];
echo '</div>';

```

PHP だと上記のように 10 行も書かずに実装できる。

## Elm での実装

```elm
module Main exposing (main)

import Browser
import Html
import Json.Decode
import Http
import String

main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }   

type alias User = 
    { id : Int 
    , name : String
    }   

type alias Model =
    { user : User
    }   

type Msg 
    = GetUser (Result Http.Error User)

init : () -> ( Model, Cmd Msg )
init _ = 
    let 
        cmd = Http.get
            { url = "http://localhost:3333/text.json"
            , expect = Http.expectJson GetUser decodeUser
            }   
    in  
    ( Model ( User 0 "" ), cmd )
        
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GetUser result ->
            case result of
                Ok user ->
                    ( { model | user = user }, Cmd.none )
                Err err ->
                    ( model, Cmd.none )

view : Model -> Html.Html Msg
view model =
    Html.div []
        [ Html.text ( "Id: " ++ String.fromInt model.user.id )
        , Html.br [] []
        , Html.text ( "Name: " ++ model.user.name )
        ]

decodeUser : Json.Decode.Decoder User
decodeUser =
    Json.Decode.map2 User
        (Json.Decode.field "id" Json.Decode.int)
        (Json.Decode.field "name" Json.Decode.string)

```

PHP と比べてコード量が import や空行を除いても 5 倍程になっている。しかもコンパイル通すのに 25 回ほど修正した。

## 考察

### 期待通りの JSON データが得られなかった時

PHP の方は、例えば name 属性がなかったときは `$user['name']` で Notice が出て、とりあえず空文字列が表示される。

```
<div>Id: 1<br>PHP Notice:  Undefined index: name in test.php on line 8
Name: </div>
```

一方 Elm だと update 関数の `Err err` の処理に入って初期値が表示される(ように実装してある)。その初期値は設定しないとコンパイルが通らないので、必ず設定されている。もちろん何のエラーも出ない。

### API のエラー

わざとサーバーを止めて、各々 API 通信をしてみる。
PHP の方は下記のようなエラーが出る。そして id と name ともに空となる。

```
PHP Warning:  file_get_contents(http://localhost:3333/text.json): failed to open stream: Connection refused in test.php on line 2
<div>Id: <br>Name: </div>
```

一方 Elm の方は上記と同じように update 関数の `Err err` の処理に入って、同じように初期値が表示される。

## おまけ: エラーの種類 (Elm)

update 関数の `Err err` の `err` には下記の 5 パターンが入る。つまり想定されているエラーは 5 種類である。一つ目の想定していない JSON データは `BadBody` エラーであり、二つ目のサーバーが起動していない場合は `BadUrl` エラーとなっている。

```
type Error
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus Int
    | BadBody String
```

参考: https://package.elm-lang.org/packages/elm/http/latest/Http#Error


今回はどのエラーに対しても初期値を表示させるという実装にしたがエラーごとに値を変えることができる。

```elm
-- 略
Err err ->
    case err of
        BadUrl _ ->
            ( { model | user = { id = 100, name = "aaa" } }, Cmd.none )
        BadBody _ ->
            ( { model | user = { id = 999, name = "zzz" } }, Cmd.none )

```

しかし、上記ではコンパイルが通らない。それは他の 3 つのエラー (`Timeout`, `NetworkError`, `BadStatus`) について実装していないからである。もちろんその 3 つ対して case 文を書いてもいいが、その他のエラーとして扱いたい場合は `_` を使うことで簡単に描ける。

```elm
-- 略
Err err ->
    case err of
        BadUrl _ ->
            ( { model | user = { id = 100, name = "aaa" } }, Cmd.none )
        BadBody _ ->
            ( { model | user = { id = 999, name = "zzz" } }, Cmd.none )
        _ ->
            ( model, Cmd.none )
```

上記のコードでコンパイルは通るようになる。

## まとめ

Elm は学習コストや初期コストが高いが、コンパイルさえ通せば実行時エラーは起きないというめちゃすごいメリットを持っている。また、その性質はリファクタ時にも大いに有効になる。そのため、長い目で見れば Elm も技術選定の時に考慮してもいい言語なのかもしれないと思った。