読者です 読者をやめる 読者になる 読者になる

uehaj's blog

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

Elmでやってみるシリーズ14:ライブラリを公開する

elm frp Haskell

この記事は「Elm Advent Calendar 2014」の23日目として書きました。


f:id:uehaj:20141223193216j:plain:right:h320

今回は、作成したElmのライブラリをElmコミュニティライブラリに公開してみます。公開するブツは以前こっそりと作成してすでに登録していた「IntRange」というもので、たいしたものじゃございません*1。今回Elm 0.14に対応させた上で、elm-packageコマンドの登録手順を整理してみます。

プロジェクトを作る

何はともあれ、公開したいプロジェクトを作ります。
ディレクトリを作ってそこにcdしてelm-packageを実行。

$ mkdir IntRange
$ cd IntRange
$ elm-package install
Some new packages are needed. Here is the upgrade plan.

  Install:
    elm-lang/core 1.0.0

Do you approve of this plan? (y/n) y
Downloading elm-lang/core
Packages configured successfully!

これで以下のファイルが作られているはずです。

$ ls -la
total 8
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 ./
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 ../
-rw-r--r--  1 uehaj  staff  330 12 23 09:34 elm-package.json
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 elm-stuff/

Elmソースコードを作る

ライブラリ設計ガイド」を参考にしてがんばって作ります。このガイドで個人的に気になった内容を箇条書きにすると、

  • モジュールに定義した関数において、主要な処理対象のデータは関数の最後の引数にせよ。合成や部分適用で便利だからですね。
  • 中置演算子を避けよ。誰を非難するわけではないですが、おいそこらの言語! 聞いとけ。演算子乱用すんでね。検索できないし読めねー、プレフィックスで出自モジュールを明示することもできない。やめろ。もしやりたければ慣れた人向けのものとしてAPIの最後に置くか、別モジュール(ホゲ.Infix)に分けろ。
  • 抽象的すぎるAPIはよしとけ。汎用性が重要なのは認めるが、それは道具であって、設計上のゴールではなく、実際のユーザ便益が本当にあるのかどうか考え。その抽象性が有用な具体例を示せ。

ちなみにgitの使用が大前提です。バージョニングもgitを元にしています。githubが必須かどうかは不明。

elm-package.jsonを編集

JSONファイルを編集します。以下のような内容です。

version セマンティックバージョニングに基づいたバージョン番号が、ソース変更から自動的に設定されるので手動で修正する必要がない。セマンティックバージョニングについては後述。
summary 概要。修正してもサイトが更新されないケースがあるので、周到に決める必要がある。
repository このソースを格納しているgithubリポジトリURL。
license BSD3が推奨のようでデフォルトである。
souce-directory ソースコードの格納フォルダ。デフォルトは"."だがsrcとかを作っていればそこに。
exposed-modules 公開するモジュール名のリスト。手動設定が必要。
dependencies 依存ライブラリ。elm-package installで自動的に追加されるが手動で編集しても良い。

ドキュメントコメント

APIリファレンスの内容になるのでこちらのガイドに従って作ります。

ドキュメントコメントは、elm-docコマンドでJSONファイルが生成されます。このJSONは、package.elm-lang.org上でHTMLにレンダリングされるのですが、現状ではローカルで容易にプレビューする方法がないようです*2

$ elm-doc IntRange.elm
{
  "name": "IntRange",
  "comment": "The library provides fold*/map* to the range of numbers without consuming memory.\n\nIntRange.fold*/map* can be used as a replacement of List.fold*/map* on a list of consecutive Int values.\n\nUsing those method reduces memory when iterate on a list which have vast numbers of element and don't use the list after iterate.\n\nFor example,\n\n          import IntRange (to)\n          import IntRange\n          Import List\n\n          IntRange.foldl (+) 0 (0 `to` 100000000) -- Can be calculated without consuming less memory.\n          List.foldl (+) 0 [0..100000000] -- Require memory for the list which length is 100000000.\n\nBoth of List.foldl and IntRange.foldl don't consume call stack, but List.foldl allocate memory for the list whose length is 100000000. In contrast, IntRange.fold requires relatively less memory. It can be used like counter variable of loop.\n\n# Create IntRange\n@docs to\n\n# Iteration\n@docs foldl, foldr, map, map2\n\n# Convert\n@docs toList",
  "aliases": [],

  略

タグを切る

gitのタグを切っておきます。もしタグをつけておかないとelm-publishの際にエラーになり、タグを切れというメッセージが表示されます。

    git tag -a 1.0.0 -m "release version 1.0.0"
    git push origin 1.0.0

publish!

以下を実行することで、http://package.elm-lang.org/サイトにelm-package.jsonおよびelm-stuff/documentation.jsonの内容がPOSTされ、コミュニティライブラリとして登録されます。ちなみに、削除等の自動的な方法はおそらくない(個別に頼むしかない)と思うので、慎重にどうぞ。

$ elm-package publish
Verifying uehaj/IntRange 1.0.0 ...
This package has never been published before. Here's how things work:

  * Versions all have exactly three parts: MAJOR.MINOR.PATCH

  * All packages start with initial version 1.0.0

  * Versions are incremented based on how the API changes:

        PATCH - the API is the same, no risk of breaking code
        MINOR - values have been added, existing values are unchanged
        MAJOR - existing values have been changed or removed

  * I will bump versions for you, automatically enforcing these rules

The version number in elm-package.json is correct so you are all set!
Success!

バージョンアップ

さて、なんらかの理由でソースコードなどを編集した後で、バージョンアップしたものを登録する際には、「elm-package bump」を実行します。

$ elm-package bump
Based on your new API, this should be a PATCH change (1.0.0 => 1.0.1)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.0 => 1.0.1) in elm-package.json? (y/n) y
Version changed to 1.0.1.

elm-packageは、ソースコードの公開APIを解析して、適切なセマンティックバージョニングに基づいたバージョン番号を付与をしてくれるのです。セマンティックバージョニングとは、詳しくはしりませんが、そのモジュールを利用する他のコード側から見て、バージョンアップしてよいかわるいかなどが判断できるような基準でのバージョン番号の付与ルールのようです。

セマンティクバージョンは「MAJOR.MINOR.PATCH」の3つの番号の組であり、1.0.0から始まり、APIの変更の種別によって

  • PATCH - APIとしては同一であり、互換性を破壊するリスクは無い
  • MINOR - 追加的な修正であり既存部には変更がない
  • MAJOR - 既存部分が修正されているか削除されている

という基準で増加させていきます。elm-packageでは、もちろん意味的な修正内容までを自動的に判断しているわけではないので、コンパイルが通るかどうか、という基準で考えれば良いと思われます。メジャーバージョンアップすると既存コードがコンパイルできる保証がなくなるということです*3

上記では、package.jsonをいじったぐらいなので、APIの追加も無いとみなされて、マイナーバージョンのみ増加しています。

ためしに一個、モジュールからの公開関数(toList)を削除してみると、

elm-package bump
Based on your new API, this should be a MAJOR change (1.0.1 => 2.0.0)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.1 => 2.0.0) in elm-package.json? (y/n)

のようにメジャーバージョンアップとみなされます。
elm-package diffで以下のように理由を知ることができます。

$ elm-package diff
Comparing uehaj/IntRange 1.0.0 to local changes...
This is a MAJOR change.

ーーーー Changes to module IntRange - MAJOR ーーーー

    Removed:
        toList : IntRange -> List Int

公開するシンボルを一個増やしてみると、

$ elm-package bump
Based on your new API, this should be a MINOR change (1.0.1 => 1.1.0)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.1 => 1.1.0) in elm-package.json? (y/n)

$ elm-package diff
Comparing uehaj/IntRange 1.0.0 to local changes...
This is a MINOR change.

ーーーー Changes to module IntRange - MINOR ーーーー

    Added:
        add : Int

たしかにマイナーバージョンアップになります。

まとめ、もしくは本題

Elmは、言語の存在目的/ミッションが非常にしっかりとした言語です。以下に私の考えるところのElmの存在意義を書いてみます。

一つは、「フロントエンド・プログラミングについてゼロの状態から考え直す」です。開発者である Evan Czaplicki氏へのインタビュー記事「【翻訳】フロントエンドのプログラミング言語をゼロから設計した理由 | POSTDを参照ください。

もう一つは、純粋関数型言語の産業界への普及です。Evan氏もしくはコミュニティの目的意識が非常にはっきりしていて、Elmに関する言語設計上の選択肢においては、「一般のプログラマが業務で使用できるようになる」ことを常に優先しています。

たとえば用語法について。ADTはADTと呼ばずUnion Typeと呼びます、elmのメーリングリストではHaksellでそうだから、という理由に流されず、常にゼロベースで議論がなされています。Elmにモナドの導入がなされるとしても、Elmにおいてモナドと呼ばれることはないでしょう。

Haskell文化圏は、「歴史的経緯」もしくは「背景となる数学理論」に基づいた名前が導入され永遠に使い続けられる傾向がある気がします。その種の用語に、誰もが最初は苦労したはずです。しかし、いったん慣れると、よしあしや合理性は別にしてそのわかりにくい名前を使い続けるのが通例です。ADT? Algebraic(代数的)って、どこがどういう側面について代数的なんだ? 直和型、直積型が、加算や乗算といった「代数的操作」に対応するからだとぉ! なんでとりたてて加算と減算だけを問題にする?減算・除算はどうなってるんだ??? とかみな思いますよね(2015/1/3削除)(「F-始代数」に由来するそうです(始代数 - Wikipedia)。型スキーマの都度具現化を意味するキーワードがなぜforallでなければならないかを納得できる理由に出あったことも私はありません。

もちろんHaskellはそれで良いです。目的が普及ではないからです。間接的に機能が他の「実用」言語に波及していけばそれが言語ミッションの成功です。あるいは、Haskellがそうであることは「Haskellが成功しすぎることの罠」に落ちないために必要だったのかもしれません。

Elmは別のレベルに目標を置きました。それが成功するかどうかはわかりませんが、自分個人も共感するので、それに寄与したいなあと思っています。プログラマと人類の幸福に寄与するために!
良いクリスマスを!

タングラムエディタ

間に合いませんでしたっ!

参考

関連エントリ


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

*1:Elm 0.13でList.indexedMapが追加されたので、明らかに存在意義が減じゃ。意味あるとしたらfoldl/foldrぐらいか。

*2:もしやるなら、elm-packageサイトをローカルに立てて、elm-package送信先を書き変えてコンパイルしなおして実行すればできるかもしれない。あるいはelm-docコマンドで生成したjsonを適当にレンダリングした方が早いかも。

*3:でもこの基準だとバージョン番号のインフレがおきそうですね。公開するシンボルは極小にするなど、API設計は慎重にやれ、ということか。