Elmで作るCLIアプリケーション

この記事は[Elmアドベントカレンダー](https://qiita.com/advent-calendar/2019/elm) 20日目の記事です。

みなさん、Elmを使ってアプリケーションを開発していると思います。Webアプリケーションを

実際、Elmは **Webフロントエンド** に特化したプログラミング言語であることに間違いは無いのでそれで間違い無いと思います。

ですが、実はElmにはユーザーインターフェイスのないヘッドレスプログラムを作成する機能が提供されています。実際、 `elm-test` のCLIはそれを使って実装されています。
今回はそれを紹介しようと思います。

## Platform.worker

普段Elmでアプリケーションを作るときmain関数で [Browser module](https://package.elm-lang.org/packages/elm/browser/latest/Browser) に入っている `sandbox`, `element`, `document`, `application` を使っていると思います。

ですが今回はCLIアプリケーションを作成するので [Platform.worker](https://package.elm-lang.org/packages/elm/core/latest/Platform#worker) を使います。

この関数の型を見てみると

```elm
worker :
    { init : flags -> ( model, Cmd msg )
    , update : msg -> model -> ( model, Cmd msg )
    , subscriptions : model -> Sub msg
    }
    -> Program flags model msg
```

となっているように `view` 関数が無いと分かります。
それ以外は普段のTEAと同じように使うことができます :raised_hands:

一つ注意してほしいところは、TEAは何かしらの `Msg` を受け取って `Model` を更新していくアーキテクチャなので `Msg` を発行しなければいけません（`Msg`が発行されないとアプリケーションが終わってしまいます！）。Webアプリケーションの場合は `view` から `Msg` が発行されるので問題ないのですが、CLIの場合だと `view` が無いので自分で `Msg` を発行する必要があります。方法としては、

- `init` 関数の `Cmd`
- JS側からportで値を送る

の２つが主な手段だと思います。

## CLIアプリケーションを作る

<img width="1440" alt="スクリーンショット 2019-12-11 6.34.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159238/693f1625-fe40-223c-8d5e-3db097397ac8.png">

アプリケーションの作り方は詳しくは紹介しません。主にElmでCLIツールを作る際のTipsのようなものを書いていきたいと思います。
実際に作ったCLIツールは [こちら](https://github.com/uzimaru0000/elm-github-cli) にあるので詳しいコードをみたい人は見てみてください。

また、JS部分の実装は省略するのでリポジトリを参考にしてください。`ports` でインターフェースは定義するのでそれにあった実装ならなんでもいいと思います。

### `elm/http` を使えるようにする

リポジトリを取得するためにHTTPリクエストを送るわけですが、 `elm/http` では `XMLHttpRequest` を利用しているためnode環境では動かないです。なので、使えるようにします。

nodeで `XMLHttpRequest` を使えるようにするために [xhr2](https://www.npmjs.com/package/xhr2)というpackageを利用します。
こちらを

```
$ npm i xhr2
```

したら `index.ts` 内に

```ts
global.XMLHttpRequest = require('xhr2');
```

と記述します。これで `new XMLHttpRequest()` が使えるようになるのでnode環境でも`elm/http`が使えるようになりました :raised_hands: 

### 入出力

<img width="1152" alt="スクリーンショット 2019-12-14 7.30.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159238/80a537a5-5bd0-1545-125c-22fb4de0ed5f.png">

CLIのライフサイクルとして、入力 → 何らかの処理 → 出力 → 入力 ・・・ というようなサイクルができると思います。
Elmでは入力、出力（`Debug.log`を使えば可能ですがそれは除いて）ができないのでJSに任せなければいけません。JSに任せるといえば `port` ですね

#### 出力

今回は、出力・入力をワンセットと考えます。Elm側から出すCmdは出力をお願いするものにしたいので

```elm
port output : String -> Cmd msg
```

という型にします。JS側のコードは

```js
app.ports.output.subscribe(str => {
    process.stdout.write(str);
    // 入力
});
```

JS側のコードを見るとわかるのですが、`output` をしたあとに入力を受け付けるようにします。

#### 入力

入力側では以下のようなデータを受け取るようにします

```elm
type alias KeyEvent =
    { sequence : String
    , ctrl : Bool
    , meta : Bool
    , shift : Bool
    }
```

キーが１つ押されるたびにデータをElm側に送るという形にします。
portではプリミティブな型以外は引数として取れないので

```elm
import Json.Decode as JD

port keyDown : (JD.Value -> msg) -> Sub msg
```

という形にしてDecodeするようにします。
JSの方は

```js
app.ports.output.subscribe(async str => {
    process.stdout.write(str);
    app.ports.keyDown.send(await keyDown());
});
```

`Promise` を返す `keyDown` 関数をよしなに作って、それをawaitした値をElm側に送るようにします。
当然、awaitして取得できる型は `KeyEvent` と一致するものです。

これで入出力ができるようになりました。

### 文字に色をつけたい

CLIで文字に色を付けるさい`\e[30mほげ\e[m`みたいな感じで書きますが、ちょっと面倒だったりするので便利moduleを作っておくといいと思います。

```elm
foreGroundColor : Color -> Style
foreGroundColor color =
    Style
        { color = color
        , ground = Fore
        }


backGroundColor : Color -> Style
backGroundColor color =
    Style
        { color = color
        , ground = Back
        }


text : List Style -> String -> String
text style str =
    styleString style ++ str ++ "\u{001B}[m"
```

こんな感じで `text [ foreGroundColor Red ] "hoge"` みたいにして書くことができます。

### コマンドライン引数

コマンドライン引数は `flags` で最初に渡してあげるのがいいと思います。
また、コマンドライン引数をパースする [パッケージ](https://package.elm-lang.org/packages/dillonkearns/elm-cli-options-parser/latest/) もあるので使ってみるといいかもしれません（僕は今回使わないアプリケーションを作ったので使いませんでした）

## まとめ

Elmに慣れている人だったらかなり簡単にCLIツールをつくれるのでは！？っという印象でした。実際、１時間位でこのくらいのアプリケーションだったら作れたのでぜひ作ってみてください。

環境構築や `Elm ↔ JS` のあたりがちょっと面倒だったのテンプレートなどがあったら便利だなーっとおもいました。

みなさんもCLIをElmで作って楽しいElmライフを！！