uehaj's blog

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

Elmにおけるimportの使い方

Elmにおけるインポートの方法を説明する。本説明が対応するElmのバージョンはElm 0.14かそれ以降、ただしインポートの仕様は今後大きく変更される可能性がある(今後についての関連情報)。

他モジュールで定義され、エクスポートされた識別子(型コンストラクタ、値コンストラクタ、型エイリアス、定数・関数など)は、インポートすることで始めて使用可能となる。Elmのimportは、この点でJavaのimportと異なっている。Javaではimport宣言をしてもしなくてもFQCNを指定すればクラスパスにあるすべての識別子を参照可能であるが、Elmでは、インポートの明示的な宣言をしない限り、他モジュールの識別子を利用できない(ただし、いくつかのモジュール・識別子はデフォルトでインポートされている。後述)。

インポートの宣言をする方法は大きく分けて2つある。

  • Qualified Import
  • Open Import

(Open Importはunqualified importと呼ばれることもあるようだ)。いずれもソースファイル冒頭(モジュール宣言のすぐ後)に記述する。以降、それぞれについて説明する。

Qualified Import

一般形式:

  1. import モジュール名A
  2. import モジュール階層.モジュール階層.モジュール名B
  3. import モジュール名 as 短縮識別子C
  4. import モジュール階層.モジュール階層.モジュール名 as 短縮識別子D

説明:

Qualified Importは、モジュール指定もしくは短縮識別子をプリフィックスとして、他モジュールからexportされた識別子を利用する。たとえば、上記のようにimportを行ったとき、以下のように識別子をアクセスできる。

  1. モジュール名A.識別子
  2. モジュール階層.モジュール階層.モジュール名B.識別子
  3. 短縮識別子C.識別子
  4. 短縮識別子D.識別子

3,4の「短縮識別子」はこの記事を書く際に便宜上付けた名前であり正式名称は不明。 短縮識別子は短かい必要はないが1文字が多く使われる(Signal→Sなど)。4.のように階層のある名前例えば「Graphics.Element.Field」に「Field」という短縮識別子を付けるなどもできる。

短縮識別子は同じものを複数回指定できないので、Qualified Importでは識別子の衝突が発生しない。(モジュール名があらかじめ衝突していない前提であるが。)

中置演算子のインポートは、Qualified Importではできない。後述のOpen Importを使用する必要がある。

例1:

import Text

main=Text.plainText "abc"

例2:

import Text
import Graphics.Element

main=Graphics.Element.flow Graphics.Element.right [ Text.asText "ABC" ]

例3:

import Text as T
import Graphics.Element as Element

main=Element.flow Element.right [ T.asText "ABC" ]

例4

import Text(asText)
import Signal as S -- 短縮識別子S
import List as L -- 短縮識別子L
import Signal(..)

main =  S.map asText <| constant ( L.map (\x -> x+1)   [1,2,3]  )

Open Import

一般形式:

  1. import モジュール名A(..)
  2. import モジュール階層.モジュール階層.モジュール名B(..)
  3. import モジュール名C(識別子,識別子,識別子…)
  4. import モジュール階層.モジュール階層.モジュール名D(識別子,識別子,識別子…)

説明:

Open Importは、指定したモジュールで定義・エクスポートされた識別子をすべて(1,2のケース)あるいは列挙したもの(3,4のケース)を、importする側の名前空間に取り込んで、前置指定も無く利用できるようにする。識別子の衝突が発生する可能性があり、その可能性は、1,2>3.4である。

例5

import Text(..)

main=plainText "abc"

例6

import Text(..)
import Graphics.Element(..)

main=flow right [ asText "ABC" ]

例7

import Text(asText)
import List((::)) -- 中置演算子::のOpen Import
import Signal
import Signal(constant) -- Qualified ImportとOpen Importを組合わせる

main=(Signal.map) asText (constant (2::[3,4,5]))

型のOpen Importについて

判別共用体型(Tagged Union, ADT)は、「型コンストラクタだけをimportして値コンストラクタをインポートしない」「指定した値コンストラクタだけをインポートする」などが可能である。

Qualified ImportとOpen Importの使い分け

簡潔さの程度でいうと以下の順である(上の方ほど字数が少ない)。

  1. 識別子を指定せずにOpen Import
  2. 識別子を指定してOpen Inport
  3. Qualified importで短縮識別子使用
  4. Qualified importで短縮識別子使用しない

mapなどいかにも重なりそうな名前があるので、書き捨てコード以外は3か4、最悪でも2を選んでおくのが安全ではあるが、衝突は滅多に無い気もするな…はて。出現頻度が高いものは簡潔に書きたいので、importするモジュールごとに、そして書く内容によってそれぞれ判断したい気もする。elm-htmlとか既存のコードでどうやってるかを参考にするのも良いのではないかと。

デフォルトのimport

任意のElmプログラムで、以下が指定されているのと同じ動作をする。 (参考の下の方 )

  • import Basics (..)
  • import List ( List )
  • import Maybe ( Maybe( Just, Nothing ) )
  • import Result ( Result( Ok, Err ) )
  • import Signal ( Signal )

Listの::や、Signalの<~, ~が(Elm 0.14では少なくとも)デフォルトインポートされていないことに注意。前はElementとかSignal(..)などもデフォルトインポートされてたんだけどな…(´・ω・`)。

おまけ:

割と最近出た書籍「Seven More Languages in Seven Weeks: Languages That Are Shaping the Future」にはElmの章があるようです。ぽちっとな。

Seven More Languages in Seven Weeks: Languages That Are Shaping the Future
Bruce A. Tate Frederic Daoud Ian Dees Jack Moffitt
Pragmatic Bookshelf
売り上げランキング: 9,168

関連リンク

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

実装から学ぶ型クラス…Groovyで型クラスを実現することを例にして

これは2014年のG*アドベントカレンダーの第23日目の記事のつもりでしたが、12時すぎてしまいましたorz。
f:id:uehaj:20141224031845p:plain:right:w200

HaskellScalaやRustには型クラスという言語機能があり、個人的感想として、理解が難しかったものの一つです。いわく、インターフェースのようなもの。いわく、オープンクラスのようなもの、など。
わからなければ作ってみるのが一番です。なのでGroovyで型クラスを実装してみました。
ソースはこちら
ただし実用的なものではなく、学習用です。また実装したのは概念のコア部分のみで、言語によって詳細は異なることに注意ください。

型クラスとは何か

型クラスとは、多相型・ジェネリクス型の型引数(仮型引数)に対して、「ある型に対して可能な操作の集合」を、制約として与え、またそれらの操作が可能であるという保証を、「クラスの継承関係」とは無縁の方法で与えるものです。

別の言い方で言うと、「クラスにデータを従属させる」「メソッドとデータを一体として管理する」「代入可能性を、クラス継承関係で与える」というようなOOPの考えかたの侵襲を拒否し、クラス継承を使用せずに、型引数を制約する方法です。

型クラスが無い場合、たとえばGroovyにおいてメソッドジェネリクスを使用した以下のコード

static<T> void runSomething(T a) {
   a.run()
}

を、正しいコードとして静的型チェックを通すためには、

static<T implements Runnable> void runSomething(T a) {
   a.run()
}

のように「TはRunnableを継承している」と指定します。Runnableという操作の集合による制約を、引数aの型Tに対して行ない、それによってaに対して特定の操作ができるという保証もしています。
これはこれで良いのですが、型クラスが必要になる動機は、ジェネリクス型変数の制約はしたいのだが、このようなクラス継承の使用を避けたいということです。

クラス継承による制約の何が嫌なのか

なぜ避けたいかといえば、

  • クラス継承がないから(Haskellの場合)
  • 多相型変数Tを「<T extends XXX>」のように制限するためには、TがXXXのサブクラスか実装クラスとして宣言されている必要がある。もし、渡したい引数のクラスがもともとそのように定義されていない場合、
    • Tとして渡すクラスのソースを変更してimplements XXXを追加したりメソッドを追加する。しかし使用用途によって都度クラス定義が変化させたり、追記していかなければならないのが嫌
    • Tとして渡すクラスを継承し、XXXを実装するクラスを定義して、新規インスタンス作ってメンバをコピーして渡す。でもコピーでは意味が変わってしまうので嫌(ラッパーと同程度、もしくはそれ以上に嫌)。Scalaのnew with Traitも同様。
    • Tとして渡すクラスから継承させることがなんらかの理由(プリミティブ型であったり、finalクラスである場合)で不可能だったり、嫌であるなら、以下の何れかの方法でラッパークラスを使う:
      • XXXを継承するクラスのメンバーとしてTを定義する。
      • Groovyのトレイトの動的proxy。
    • →でもラッパーを導入するとアイデンティティが破壊されるのでいやだ。オーバーヘッドが嫌だ。
  • 関数型プログラミングスタイルの一流派としてあり得る方針としての「データと関数の分離」に反するので嫌なのだ
  • 「継承不要」「ラッパー不要」というのはパラメトリック多相*1で普通にできていた話なのに、制約を付けようとした時点で必要になるというのは嫌だ。なるべく同じようにやりたい
  • なんでもクラスにするという基本方針がそもそもの間違いで、どう覆い隠そうとしてもシンプルさが損なわれる(本質的批判)
  • オブジェクトが保持するvtableへのリファレンスなどのメモリコスト、vtableを介在したメソッド呼び出しの実行時コストを避けたい*2。また、データ型のメモリイメージ表現のC言語などとの互換性を常に保持したい*3
  • 仮想関数による多態性イラネ。ていうより、原語では同じ単語で多相性とかぶってるかぶってる。
  • 操作のレシーバーという概念は非対称でいやだ。レシーバーと他の引数、戻り値の型に非対称性があり、数学的概念の記述などでの一般性を阻害する

など。まるでイヤイヤ期の幼児のような、嫌だ嫌だ、の連発です。まあ嫌なのだからしょうがない。私もこう列挙されると嫌な気になってきます。

型クラスの仕組みの4段階

型クラスの仕組みを以下の4段階で考え、Groovyで実装していきます。

  • (1)型クラスを定義する
  • (2)型クラスのインスタンス
  • (3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する
  • (4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す

それぞれに付いてモノイド型クラスのコードを例にして解説します。
ちなみに、今回サンプルとしては以下の型クラスを作っています。

型クラス

interface Monoid<T> {
interface Functor<F> {
interface Applicative<A> extends Functor<A> {
interface Monad<M> extends Applicative<M> {
interface Show<T> {
trait Eq<T> { // One of eq or neq, or both should be overriden.
trait Ord<T> extends Eq<T> {

インスタンス
class OptionalMonoid<T> implements Monoid<Optional<T>> {
class ListFunctor implements Functor<List> {
class OptionalFunctor implements Functor<Optional> {
class ListApplicative extends ListFunctor implements Applicative<List> {
class ListMonad extends ListApplicative implements Monad<List> {
class IntShow implements Show<Integer> {
class StringShow implements Show<String> {
class ListShow<T> implements Show<List<T>> {
class IntEq implements Eq<Integer> {
class IntOrd implements Ord<Integer> {

(1)型クラスを定義する

モノイド型クラスを定義します。型引数に適用する制約を定義するものです。

@TypeChecked
interface Monoid<T> {
    def T mappend(T t1, T t2);
    def T mempty();
}

このようにします。Tではなくdef Tなのは、なぜかそうしないとコンパイルが通らないから。
見るとわかるように普通のインターフェースです。ここでインターフェースにする理由は、継承が使えるからです。例えば、Monad extends Functorのように型クラスを継承することができます。

これは先程の「クラス継承を不要としたい」に反するように思えますが、実際にはthisからのオフセットによるインスタンス変数の参照やアクセス、thisを介した仮想テーブル(vtable)とその呼び出しの機構は完全に不要で、メソッド置き場として使っているにすぎません。なので、ここでクラス継承を使用するのは便宜的なもので、本来なら、staticメソッドにして、staticメソッドを継承階層をたどって探索してくれる仕組みをAST変換で作り込めば良いところです*4が、手をぬきます(Scalaではobjectにするところ)。Monoidはデータメンバを持たない(インターフェースなので持てませんが、classで実装したとしても持たせない)ことに注意ください。

ここでやっていることをまとめると、

  • 操作の集合をクラスのインスタンスメソッドとして定義する。このクラスをジェネリクス型引数の制約として使用する。これが型クラスである。このクラスから継承したいかなるクラスにおいても、データをインスタンス変数として定義することはない(本質的)
  • ここで定義したメソッドの間では相互に呼び出しあうことができる
  • メソッドの本体を別に定義しても良い。Groovyのtraitが便利に使えるだろう。

(2)型クラスのインスタンス

型クラスのインスタンス化とは、制約を与えたいデータ型が、ある型クラス制約を満たす、という宣言であり、満たせるようにするための操作を補充します。
以下では、String型がMonoid型クラスの制約を満たすように、「StringのMonoid型クラスのインスタンス」を作成します。

def instance_Monoid_java$lang$String_ = new Monoid<String>() {
    @Override String mappend(String i1, String i2) {
        i1+i2
    }
    @Override String mempty() {
        ""
    }
}

型クラスのインスタンスを保持する変数名を、「instance_Monoid_java$lang$String_ 」という決められた規約に従うものにしておきます。これはスコープの探索をサボるためです。変数名がインスタンスを決め、同名のインスタンスがあれば、内側の物が勝ちます*5。この変数名の変数をstatic importすることも可能であることも期待します。

ここまでは、何の変哲もない標準のGroovyの機能で実現することです。

(3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する

さて、準備した型クラスMonoidの制約を使って、多相関数を定義してみます。
以下のようにします。

@TypeChecked
class Functions {
    static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) }
    static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() }
}

引数「Monoid<T> dict=Parameter.IMPLICIT)」が本実装における型クラス制約の宣言であり、意味は、mappendの返り値と、引数で使用する型変数Tは、Monoidという型クラスの制約を満たす必要がある、ということです。

dictは型クラスMonoid型の、型クラスのインスタンスが渡ってくる引数ですが、こちら側から見ると関数の名前空間だと思ってください。引数にあたえられた名前空間に転送します*6複数の型クラスが引数に与えられたら区別して振り分けます*7
IMPLICITは以下のように定義してあるただの定数で、AST操作の際のマーカーとして機能します。

class Parameter {
    static final Object IMPLICIT = null
}

ちなみにここで、mappend,memptyが型クラスインスタンスの同名メソッドを呼び出しているだけの関数なのは、無駄に感じるかもしれません。その意味では、ちょっと例がわるくて、Monoid型クラス制約のついた型引数を使った、任意の静的関数が定義できます。ここでmappendやemptyをstatic関数として定義してるのは、利便のため、ユーティティ関数としてこれらをstatic空間にexportしていると思ってください。Haskellだと、インスタンス宣言で、staticな型クラスインスタンスがモジュール内グローバルに存在を開始し、関数呼び出しはそのモジュール内グローバルな空間からマッチする名前が呼ばれることになるんですよね。このGroovy型クラス実装は、Scalaでも同様だと思いますが、名前空間として機能する型クラスインスタンスを明示的に扱うので、1つクッションが入る場合があるということです。善し悪しはあるでしょうが、Haskellの方がシンプルなのは確実です。

(4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す

ここからが、そしてここのみが、山場です。:)

先程定義した関数を呼び出します。以下のようにします。

import static Functions.*
 :
@TypeChecked(extensions='ImplicitParamTransformer.groovy')
  :
        String s = mempty()
        assert s == ""
        assert mappend("a","b") == "ab"

このコードが静的型チェックのもとでコンパイル・実行できます。

ソースコードの表面上は、関数定義での仮引数Monoid<T> dictに対応する実引数は与えていませんが、暗黙に型クラスのインスタンスが補充されて呼び出されます。これをやってるのが今回作ったImplicitParamTransformerであり、カスタム型チェッカ(型チェッカ拡張)でありながら、型チェッカであることを踏み越えて、以下を実行します。

  • ジェネリクス型を使っている静的メソッド呼び出しにおいて、その静的メソッド定義で「IMPLICIT」をパラメータ初期値で使用しているなら、そのパラメータに、ジェネリクス型から定まる変数名を挿入する。呼び出しが、「mappend("a","b")」であるなら、mappendの定義が
    static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) }

であるので、実引数の型から型引数の実際の型を推論・解決させて、Tにjava.lang.Stringを束縛して、IMPLICITに対応する引数を、Monoid<String>型のinstand_Monoid_java$lang$Stringという名前の変数の参照に変換します。ついでに、ごく限定されたターゲット型推論を、「String s = mempty()」の形のとき実行し、

    static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() }

では、戻り値が代入される変数の型がStringであることから、Mooid<T>の型がMonoid<String>であると推論させ、変数名を決定します。

最終的には、これらのmappend,memptyは以下の呼び出しと等価になり実行されます。

String s = mempty(instance_Monoid_java$lang$String_ )
assert s == ""
assert mappend("a","b",instance_Monoid_java$lang$String_ ) == "ab"

要はScalaのimplicitパラメータの機能を実行していることになります。Scalaの場合は変数名は無意味で型だけで探索するわけですが、特定の型を持った変数定義の探索が面倒なので変数名に型情報をエンコーディングしてスコープ解決させています。

直感的な説明

型クラスのざっくりイメージ的な理解の仕方としては、クラスベースOOPでは、データ型にvtableを組み込み不可分にしていたのを、ひっぺがして、動的な束縛機能を除去して、本当にそれが必要になる最後のタイミング、つまり多相関数呼び出しの時点まで結合を持ち越して、そしてその時点で多相型を解決の枠組みでインスタンスを選定し、別引数で渡し、関数の名前空間として使うということです。

他にやるべきこと

あとはいろんなシンタックスシュガーを導入できると思いますが、学習用としてはここらで打ち止めておきます。

問題点

不都合がありまして、モジュール化ができてません。たとえばEqクラスとかOrdクラスとかそれぞれを別クラスにしたかったのですが、一つの大きな誤算として、多相型のメソッドをstatic importしてもジェネリクス情報が得られません。

class Functions {
    static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) }
      :

を、

import static Functions.mappend

すると、引数や返り値のジェネリクス情報が呼び出しコード上の処理段階で不明になります。なので(4)でのジェネリクスの推論ができません。解決策はなんでしょうね。クラスファイルに残っているジェネリクス情報をリフレクションとかでたどるんでしょうかね。たいへんなのでこれ以上の追求は個人的にはあきらめます。

おまけ: Higher-kind Generics

Groovyでは、型引数をとる型構築子を型引数に(いわゆる高階型変数(Higher-kind Generics))したときに期待する動作を行います。たとえば、今回以下のようなコードが書けています。

@TypeChecked
interface Applicative<A> extends Functor<A> {
    public <T> A<T> pure(T t)
    public <T,R> A<R> ap(A<Function<T,R>> func, A<T> a) // <*> :: f (a -> b) -> f a -> f b 
}

ここでAは引数を取れる型引数であり、たとえばMaybeやListなどを与えることができます。これはJavaジェネリクスでは(少なくともJava8では)できないことです 。これができることによって得られる抽象化能力は、非常に大きいものです。モナドはこれがあるから型クラスになり得るのです。

とはいえ、これをもってGroovyでHigher-kind Genericsができる、と言ってよいかどうかは、微妙です。というのも、しなければならないことは「期待する動作を行う」だけではないからです。具体的には、上記を継承するインスタンスで、上記のジェネリクス条件を満たさないオーバーライド定義したときに、型エラーになること、が必要だと思っています。実際試してみると、Groovy 2.4-beta-xでは期待したようにエラーにはならないケースがあるようです。

エラーチェックが正しく出来ないなら、型チェックがなされない=動いてしまう、という結果になるのは明らかです。綿密に調べてもしバグならバグ報告したいところですが、できておりません。
ということで結論は保留です。今のところわかりません。

まとめ

  • 型クラスの基本は実装がわかれば理解できる
    • ただし言語ごとの実装戦略や、シンタックスの差異は当然あるのでそこは別途。
  • 型クラスの構成部品はOOPから流用可能だが、コンセプト的に根本的に相入れないからこそ、型クラスが存在する
  • 型クラス=FPではない。その実現に寄与する周辺機能にすぎない。但し静的型および多相型に強く結びついた重要で強力な機能である。FPにおける型クラスは、OOPにおけるクラス継承に比するべき概念である(クラス継承はOOPに必須ではない、ということとも同型)。
  • Groovyはコンパイラやランタイムに手を入れずに、カスタム型チェッカやAST変換でかなりのことができる
  • 静的型や多相型に関してもある程度のことができる。ただし、静的型チェッカの多相型処理を読み込むのは覚悟がいる。
  • Groovy2.xの静的型チェッカの多相型サポートは、激しく進歩している。Javaのそれより強いのは間違いない。
  • カスタム型チェッカ(型チェッカ拡張)は、フルのAST変換を書くのにくらべて非常に便利。ただし実行フェイズや、ハンドラを書けるイベントが決まっているので目的とする処理に適合しない可能性がある。

おまけ2:アドホック多相

アドホック多相という言葉は使いませんでしたが、これはこういうことなんじゃないかと思っています(オレオレ理解)。。

  • パラメトリック多相: 処理対象のデータ型の特定をしないで処理を記述できるような、処理記述の柔軟性。ただし、処理対象となるデータに対して実施できるのは、ポインタ操作やバイトコピーなど、内容や構造に依存しないような操作のみに限る。典型的にはコンテナへの出し入れ対象として扱うだけ、とか。
  • アドホック多相: 目的は同様だが、パラメトリック多相では達成できない、「データの内容に依存した処理」を可能とするために、データと独立した「操作の集合」を事前に定義し、それを関数呼び出し時のデータ引数を渡す時に、引数データの付随情報として与える。(その際に操作の集合の選定を多相型の型推論で元にして自動的に実行する。)

これらの概念は、実装方式とは独立である。たとえばパラメトリック多相についてポインタで実現するケースもあると思うが、型毎に関数実体を適切なタイミングで生成して使っても良いであろう。

*1:パラメトリック多相とは、型引数の制約が無い場合、つまりコンテナに入れる、出す、といった、データ固有の操作を行わない引数を多相にするときに使える。

*2:可能かどうかは実装による。

*3:余談になるが、多言語間呼び出しでは、OOPが長期的には無価値であることが実証されてきたと言うしかない。CORBA/RMIを見よ。SOAPを見よ。WebAPIはメソッドを持たないJSONを返す

*4:@InheritConstructorはすでに似たようなことをしているのでそれを真似すればよい。

*5:Scalaとの相違点:確かScalaでは衝突をコンパイル時エラーにするんじゃなかったかな。

*6:この振り分けは、Haskellなどのイレイジャではないジェネリクス環境で理想的な条件下であれば、静的に決定できるとされている。未確認だが。

*7:Groovyのwith句を使っても良い。

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

この記事は「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設計は慎重にやれ、ということか。

elmでやってみるシリーズ8: 赤いドットが回っているかのように見えるけど実は直進運動な錯視

Elmでやってみるシリーズ8: 赤いドットが回っているかのように見えるけど実は直進運動な錯視

少し前に、「このくるくる回る白いドット、実は真っ直ぐ往復してるだけなんだぜ」という記事がありましたが、これをElmで再現してみようというのが今回のネタ。
(追記 id:waman さんが数学解析をされています→回転しているようにみえる白いドットは単振動している - 倭算数理研究所 。このネタ自体もwamanさんのTwitterで知ったんですけどね。)

以下は画面キャプチャ(静止画)。
f:id:uehaj:20140727232140p:plainf:id:uehaj:20140727232055p:plain

コードは以下のとおり。

fps_=96 -- 画面更新頻度
pi2 = 2*pi -- 2π
deg30 = pi2 / 360 * 30 -- 30°をラジアン単位に変換

-- 1秒間にfps_回、1増加するシグナル値。
ticks : Signal number
ticks = foldp (\it acc->acc+1) 0 (fps fps_)

-- ticksから派生させて、1秒間に一回転に対応する角度値のシグナルを計算。また、元シグナルticksに対してdミリセカンド分、発生するのが遅れるシグナルにする
sig : Float -> Signal Float
sig d = let f x = (x / fps_) * pi2
        in f  <~ (delay (d * millisecond) ticks)

-- 座標(x,y)を原点を中心に左にtだけ回転させた座標を得る
rotate_ : Float -> (Float, Float) -> (Float, Float)
rotate_ t (x,y) = (x*(cos t)-y*(sin t),  x*(sin t)+y*(cos t))

-- 時間に追随するラジアン単位角度tに対するsin tの位置を、特定角度xで全体を傾けてプロットする
dot : Float -> Float -> Form
dot t x =  move (rotate_ x (100 * (sin t), 0))  (filled red (circle 3))

-- 「tick値と、角度を変えたdotを6つはっつけたコラージュ(400x200)を作る純粋関数disp」に、dotの位置を表わすシグナル値sigを6つ作ってリフト適用。これらのシグナル値への引数としてそれぞれディレイ値を適切に設定することで、「遅れ」による位相差が生じ、「回転する」かのように見えることになる。
main : Signal Element
main = let disp t p1 p2 p3 p4 p5 p6 = flow down
                [ asText t
                , collage 400 200
                                  [ dot p1 <| deg30*0
                                  , dot p2 <| deg30*1
                                  , dot p3 <| deg30*2
                                  , dot p4 <| deg30*3
                                  , dot p5 <| deg30*4
                                  , dot p6 <| deg30*5
                                  ]]
       in disp <~ ticks ~ sig 0 ~ sig 100 ~ sig 200 ~ sig 300 ~ sig 400 ~ sig 500

実行画面

上記のソースを変更した上で実行したい場合はこちらからどうぞ。ブラウザ内で実行できます。フル画面表示はこちらから。fps_値を増やすとなめらかになりますが、ブラウザ負荷がたぶん上がります。

気づいたこと・解説

  • Signal.delay関数で元のticksのSignalを「時間的にずらしたシグナル」を作れる(sig dではついでにラジアン単位の角度にしている)
  • おそらくJavaScriptのonTimerイベントの精度の関係で微妙な誤差が出るのですが、なんか有機生命みたいなゆらぎが出てておもろい。
    • 角度でずらせば、正確にずらせるんですが、FRPの面白さを堪能するために、時間でずらしてます。
    • hatena blog記事中のインラインフレーム中で実行させてると、また遅延が違うので動きが違う。味わい深い。
    • 補助線を引いてみると以下のとおり。

関連エントリ


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

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

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

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

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

プログラミングHaskell

プログラミングHaskell

elmでやってみるシリーズ7: elm-htmlでTwitter Bootstrapを適用

Elmでやってみるシリーズ7: elm-htmlでTwitter Bootstrapを適用。

つい先日、「Blazing Fast HTML」と銘打って、elm-htmlライブラリが公開されました。これはElmでDOMツリーを構築・更新するための低レベルライブラリであり、Virtual DOMという技術を使っているので非常に画面更新速度が速いそうです。SPA(Single Page Application)ではDOMの更新速度が重要になりますが、Elmは純粋関数型・イミュータブルデータなのでそのことを利用してさらに効率良く実装できるそうな。

従来、ElmはCSSとの連携はあまり重視されておらず、「ElmはCanvasを使ったアニメーションが得意」とされてきましたが、現代的な見た目のHTMLベースのアプリも自在に開発できるようになる、という道筋の第一歩なわけです。まだ未成熟ですがね。

個人的にTwitter BootstrapなどのCSSフレームワークとElmとの連携に興味があったので、今回試してみました。

やったことは以下のとおり。

  1. elm-getでelm-htmlをインストール
    • elm-get install evancz/elm-html 0.1.2
  2. HtmlTest.elmを作る(後述)
  3. 以下のように--only-jsオプションを付けてelmコードをコンパイル。なお、--only-jsを使用する場合、呼び出すためにモジュール名が必要なので冒頭でmodule宣言が必要になる。
    • elm --make --only-js HtmlTest.elm
  4. Twitter Bootstrapを使用するindex.htmlをこちらの「Embed in HTML」を見て適当につくる。index.htmlの内容はこちらを表示してビューソースしてみてください。
  5. index.htmlとHtmlTest.jsをgithub pagesにpush。

処理の内容としては、マウスのX座標、Y座標の過去10個分をテーブルにして表示するというもの。

コード(HtmlTest.elm)は以下のとおり。

module HtmlTest where -- --js-onlyをする場合モジュール宣言は必須

import Html (..)
import Mouse

-- マウス座標データを一行分の<tr>に変換
data2line : (Int,Int) -> Html
data2line (x,y) = node "tr" [] []
         [ node "td" [] [] [text <| show x]
         , node "td" [] [] [text <| show y]
         ]

-- テーブルを作る
tbl : [(Int,Int)] -> Html
tbl dat = node "table" ["className" := "table table-striped table-bordered table-condensed"] []
      [ node "thead" [] [] [
          node "tr" [] []
              [ node "th" [] [] [text "Mouse X"]
              , node "th" [] [] [text "Mouse Y"]
            ]
          ]
      , node "tbody" [] []
         (map data2line dat)
      ]

-- アンカータグによるリンクを作るユーティリティ関数
linkTo txt url = node "a" ["href":=url] [] [text txt]
-- ボタンのように装飾をしたリンクを作るユーティリティ関数
buttonLinkTo txt url = node "a" ["href":=url,"className":="btn btn-primary btn-lg"] [] [text txt]


-- 画面を作る
body : [(Int,Int)] -> Html
body dat = node "div" ["className":="navbar navbar-default navbar-fixed-top"] ["padding-top":= px 10] [
         node "div" ["className":="container"] [] [
           node "div" ["className":="jumbotron"] []
             [ node "h1" [] [] [ text "Elm/Twitter Bootstrap" ]
             , node "p" [] [] [ "Elm-html" `linkTo` "https://github.com/evancz/elm-html"
                              , text "で"
                              , "Twitter bootstrap" `linkTo` "http://getbootstrap.com/" 
                              , text "連携しています。"]
             , node "p" [] []
             [ "もっと学ぼう" `buttonLinkTo` "http://elm-lang.org/"
             , "ソースコード" `buttonLinkTo` "HtmlTest.elm"
             ]
           ]
           , tbl dat
         ]
       ]

-- 画面を表示する
display : [(Int, Int)] -> Element
display list = body list |> toElement 200 200

-- マウス座標のシグナル値(x,y)をliftして与えて画面を表示する
main : Signal Element
main = display <~ foldp (\it acc -> take 10 (it :: acc)) [] Mouse.position

実行画面(操作可能)

全画面で実行する場合はこちら
プロジェクト全体はこちらのgh-pagesブランチからどうぞ。

気づいたこと

  • elm-htmlはGroovyのマークアップビルダーみたいなもの。
  • elm-htmlの出力はHtml(DOM)であり、それを変更する手段は提供されていない。なのでコード的には毎回全体を何も考えずに宣言的に生成する。しかし、Elmが完全に純粋であることも利用して、Vitual DOMを通じて、実DOMに対して最低限の差分のみが効率良く適用されるという話。
  • elm-htmlの記述はHTMLと一対一対応で、冗長度が高いが、elm-htmlの位置付けは、より高機能でより抽象度の高いライブラリ作成の基盤になるための低レベルライブラリ、というものなのでこれはこれで良い。
  • elm-htmlはいわゆる非標準ライブラリであり、しかも--only-jsでHTMLと連携させるので、share-elmなどでは公開できない。なのでgithub-pagesを使用して公開してみた。
  • --only-jsを前提とするとelm-serverでホットリロードできない。もしくはやりかたがわからない。
  • elm-htmlのHtmlとGraphics.Input(.Fields)との連携・関係はまだ理解できていない。elm-htmlだけでhandleを使えるから、こっちで閉じてやるんだろうか。そうじゃないとしたらElementやfieldをHtmlに入れる仕組みが必要なはずだが。
  • elm-htmlではcssも要素の属性も基本的には文字列でしかない。本来なら強い型付けをして欲しいものですが、将来開発されるであろうelm-html上に構築される何かに期待。
  • 合成と共有部分の切り出し、共有化、抽象化というプログラミングの本質的な強力さが現状でも享受できる。html,js,cssを使っていたWebアプリ開発暗黒時代の夜明け
  • Elmで外付けスタイルシートを定義することはできない。本来はElmがLESSやSassの代替になって欲しいものである。*1
  • CSSクラスの指定は「"className":=」、floatは「cssFloat」とするらしい。このような違いはJavaScriptでDOMをいじる場合と同様らしい。

関連エントリ


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



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

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

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

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

*1:と思ったが、そもそも、DOMを直接いじってスタイルが変更できるのだから、外付けスタイルシートなどはまったくいらなくなる…かとも思ったが、JSでトラバースしてスタイルつけるより、クラス名だけつけてブラウザに任せた方が速いと思うので、やっぱり何かしら必要かも。よくわからない。

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))