関数の型

elm/coreelm/htmlなどのパッケージを見ると、間違いなく複数の矢印を持つ関数が見つかるでしょう。例を示します:

String.repeat : Int -> String -> String
String.join : String -> List String -> String

なぜこんなに矢印があるのでしょうか?何が起こっているのでしょうか?!

隠れた括弧

省略されている全ての括弧を見るとより分かりやすくなります。例えばString.repeatの型は以下のように書いても問題ありません:

String.repeat : Int -> (String -> String)

これはIntを受け取って 新たな 関数を作る関数です。動いているところを見てみましょう:

[ { "input": "String.repeat", "value": "\u001b[36m<function>\u001b[0m", "type_": "Int -> String -> String" }, { "input": "String.repeat 4", "value": "\u001b[36m<function>\u001b[0m", "type_": "String -> String" }, { "input": "String.repeat 4 \"ha\"", "value": "\u001b[93m\"hahahaha\"\u001b[0m", "type_": "String" }, { "input": "String.join", "value": "\u001b[36m<function>\u001b[0m", "type_": "String -> List String -> String" }, { "input": "String.join \"|\"", "value": "\u001b[36m<function>\u001b[0m", "type_": "List String -> String" }, { "input": "String.join \"|\" [\"red\",\"yellow\",\"green\"]", "value": "\u001b[93m\"red|yellow|green\"\u001b[0m", "type_": "String" } ]

つまり概念的には全ての関数は1つの引数を受け取ります。 関数は新たな1引数の関数を返すかもしれません。これを繰り返すと、どこかで関数を返すのを止めるでしょう。

実際に起こっていることを明示するために常に括弧をつけることはできますが、複数の引数を持つ関数などではとても扱いづらくなってしまいます。これは4 * 2 + 5 * 3(4 * 2) + (5 * 3)とは書かないのと同じことです。型における括弧の省略ルールを新たに学ぶことは少し手間かもしれませんが、一般的に価値のあることです。

ですが、そもそもこの特徴の意味は何なのでしょうか?どうして(Int, String) -> Stringのように引数を一度に渡してしまわないのでしょうか?

部分適用

ElmにおいてList.map関数はごく当たり前に使われます:

List.map : (a -> b) -> List a -> List b

これは関数とリストの2つを引数に取ります。そしてリストの全ての要素に関数を適用したリストを返します。いくつか例を見てみましょう:

  • List.map String.reverse ["part","are"] == ["trap","era"]
  • List.map String.length ["part","are"] == [4,3]

さて、String.repeat 4String -> String型であったことを覚えていますか?よって以下のように使うことができます:

  • List.map (String.repeat 2) ["ha","choo"] == ["haha","choochoo"]

(String.repeat 2)String -> String型の関数ですから、直接使うことができます。(\str -> String.repeat 2 str)とする必要はありません。

Elmにはエコシステム全体においてデータ構造は常に最後の引数であるという慣習があります。これは、通常、関数がこの使い方を念頭において設計されることを意味し、かなり一般的なテクニックです。

ここで部分適用は過度に使用される可能性があるということを覚えておくことが大切です。部分適用は時に便利で簡潔ですが、節度を持って使うことがベストです。つまり 少しでさえ 複雑になった場合にはトップレベルにヘルパー関数を定義することを常に勧めます。こうすることで関数は名前を持ち、引数も名前を持ちそして新たに追加されたヘルパー関数をテストすることが容易になります。先ほどの例においては、この関数を作ることに当たります:

-- List.map reduplicate ["ha","choo"]

reduplicate : String -> String
reduplicate string =
  String.repeat 2 string

上のケースはとてもシンプルです。しかし(1)reduplicationとして知られる言語学の現象に興味を持っていることがより明確になり、そして(2) reduplicateに新たなロジックを追加するのが非常に簡単になります。もしかしたらshm-reduplicationをどこかでサポートしたくなるかもしれないでしょう?

言い換えると、もし部分適用が長くなっているなら、ヘルパー関数を作るということです。そしてもし部分適用が複数行にまたがるならば、絶対に トップレベルのヘルパー関数に変えるべきです。このアドバイスは無名関数を使うときにも当てはまります。

Note: もしもこのアドバイスに従った結果、最終的に"あまりにも多くの"関数を定義してしまった場合、続く5個や10個の関数の概要を示すために関数に-- REDUPLICATIONのようなコメントをつけることを推奨します。以前の例で-- UPDATE-- VIEWなどのコメントがついたものを示しましたが、これは私の全てのコードで使っている一般的なテクニックです。アドバイスに従ったらファイルがあまりに長くなっていくのではと心配しているなら、The Life of a Fileを見ることをおすすめします!

パイプライン

Elmにも部分適用を利用するパイプ演算子があります。例えば、sanitize関数を入力を整数に変換する関数として定義されているとします:

-- BEFORE
sanitize : String -> Maybe Int
sanitize input =
  String.toInt (String.trim input)

上を以下のように書き直すことができます:

-- AFTER
sanitize : String -> Maybe Int
sanitize input =
  input
    |> String.trim
    |> String.toInt

つまり上の"パイプライン"ではString.trimに入力を渡して、そしてその結果をString.toIntに入力として渡しています。

パイプラインは多くの人が好むように、"左から右へ"読むことを可能にするため、コードをすっきりさせることができます。 しかしパイプラインは過度に使われる可能性があります。3、4つのパイプラインとなった場合トップレベルにヘルパー関数を書いた方がコードがより簡潔になる場合が多くあります。トップレベルに定義することで変換自体に関数名が付き、その引数にも名前が付けられ、型注釈も書くことになります。つまりソースコード自体がその意味するところを雄弁に語るようになり、チームメイトや将来の自分自身は感謝することでしょう!更にロジックをテストすることもより簡単になります。

Note: 私は個人的にはむしろBEFOREのほうが好きですが、これはおそらくパイプが無い言語で関数型プログラミングを学んだからでしょう!

results matching ""

    No results matching ""