モジュール

Elm にはコードベースをうまく拡大していくときに役に立つモジュールがあります。最も基本的には、モジュールはコードを複数のファイルへと分割できるようにします。

モジュールの定義

Elm のモジュールは、型を中心にして定義していくようにすると最もうまくいきます。List型に対するListモジュールがまさにそうなっています。そうなると、ブログサイトであれば、Post型を中心にしてモジュールを組み立てたくなる思います。たとえば次のようにモジュールを作ることができるでしょう。

module Post exposing (Post, estimatedReadTime, encode, decoder)

import Json.Decode as D
import Json.Encode as E


-- POST

type alias Post =
  { title : String
  , author : String
  , content : String
  }


-- READ TIME

estimatedReadTime : Post -> Float
estimatedReadTime post =
  toFloat (wordCount post) / 220

wordCount : Post -> Int
wordCount post =
  List.length (String.words post.content)


-- JSON

encode : Post -> E.Value
encode post =
  E.object
    [ ("title", E.string post.title)
    , ("author", E.string post.author)
    , ("content", E.string post.content)
    ]

decoder : D.Decoder Post
decoder =
  D.map3 Post
    (D.field "title" D.string)
    (D.field "author" D.string)
    (D.field "content" D.string)

ここで出てくる新しい構文は、最上部にあるmodule Post exposing (..)の行だけです。この行が意味しているのは、このモジュールがPostという名前であることと、ここに並んだいくつかの値だけがモジュールの外部から利用できるということです。ここに書かれているように、wordCount関数は Postモジュールの中でだけ利用可能になります。このように関数を隠すのは、Elm で最も重要なテクニックのひとつです!

Note: もしモジュールの宣言を追加するのを忘れてしまったら、Elm は代わりに次のようなものを使います。

module Main exposing (..)

>

これは初心者がひとつのファイルだけで作業するときに簡単にしてくれます。Elmを学び始めた最初の日からいきなりモジュールシステムに向き合うべきではありません!

モジュールを拡大する

アプリケーションがより複雑になっていくにつれて、モジュールにいろいろなものを追加していくことになります。The Life of a Fileで説明したように、Elmのモジュールが400行から1000行になるのは普通のことです。しかし複数のモジュールがあるとき、新しいコードをどこに追加するのかをどうやって決めたらいいでしょうか?

コードがどのようなものであるかに応じて、私は次のような経験則に従うことにしています。

  • コードがその場所でしか使われていないとき — もしロジックが一箇所だけに出現するのなら、トップレベルの補助関数として分割し、それを使用している箇所になるべく近いところに置きます。すぐ後ろに続く定義がブログ投稿のプレビューと関係していることを表すため、-- POST PREVIEWというようなコメントヘッダを使うこともあります。
  • 似たようなコードがあるとき — 投稿(Post)のプレビューを、サイトのホームページと投稿者のページの両方に表示したいとしましょう。サイトのホームページでは、その興味深い内容を強調したいので、長めの抜粋が欲しいです。それに対して投稿者のページでは、内容の幅を強調したいのでタイトルに注目したいです。これらはよく似てはいますが同じではありませんから、『その場所でしか使われていないとき』の経験則に戻りましょう。ただロジックを別々に書けばいいのです。
  • コードがまったく同じであるとき — たくさんのその場所でしか使われていないコードがあちこちに現れることになるでしょう。これは良いことです!しかしおそらくその中に、完全に同じロジックを含む定義があることに気づくでしょう。そのようなロジックは補助関数へと分割しましょう! もしそのすべての使用箇所がひとつのモジュールのなかにあるなら、これ以上何もする必要はありません。もし本当にそうしたいのなら、-- READ TIMEというようなコメントヘッダを置いてもいいでしょう。

これらの経験則はすべて、ひとつのファイルのなかに補助関数を作ることについてです。ある独自の型を中心としてたくさんの補助関数すべてがあるときだけ、新しいモジュールを作りたくなるでしょう。たとえば、Postモジュールを作っていない状態でPage.Authorモジュールを先に作りはじめて補助関数が山積みになった場合、新しくPostモジュールを作ると、探しているコードを見つけやすくなったり、コードを理解するのが簡単になるはずです。そうでないとしたら、わかりやすかったバージョンに戻しましょう。モジュールがたくさんあるほど良いというわけではありません!コードを単純かつわかりやすく保つ道を選びましょう。

まとめると、似ているコードは最初はその場所でし使われていないコードであると見なしましょう(たいていそういうコードは最終的にユーザインターフェイスにあります)。異なる定義のなかにまったく同じロジックを見かけたら、適切なコメントヘッダのついた補助関数を作りましょう。特定の型に対するたくさんの補助関数があるなら、それを新しいモジュールに分割することを検討してください。もし新たなモジュールがコードをわかりやすくしてくれるのなら、それは素晴らしいです!でもそうでないのなら、元に戻しましょう。多くのファイルがあるというのは、単純だとかわかりやすいということとは本質的に異なるのです。

Note: モジュールについて失敗する最もよくあるケースは、最初は『まったく同じ』であったものが、あとから『似ている』に変わったときです。ユーザインターフェイスではそれが特によく起こります! それぞれ異なる場合をぜんぶまとめて扱おうとして、引数を追加し、そして複雑な引数をさらに追加しては、つぎはぎだらけの関数を作ろうとする人もいます。より良い方法は、ふたつの『その場所でしか使われていない』という状況があることを受け入れ、それら両方の箇所にコードを複製することです。それから必要に応じてカスタマイズします。それから、ロジックのいずれかが結果的にまったく同じになったかどうかを見てください。もしそうなら、それを補助関数へと括りだしてください。長い関数は複数の小さい関数へと分割するべきで、長く複雑にしてしまうべきではありません!

モジュールの使用

コードはすべてsrc/ディレクトリの中に入れておくというのがElmの慣習です。これはelm.jsonの既定値でもあります。そのため、このPostモジュールはsrc/Post.elmという名前のファイルにする必要があるでしょう。これでこのモジュールをimportし、exposing節でモジュール外に公開するよう指定された値を使えるようになりました。インポートには4種類の方法があります。

import Post
-- Post.Post, Post.estimatedReadTime, Post.encode, Post.decoder

import Post as P
-- P.Post, P.estimatedReadTime, P.encode, P.decoder

import Post exposing (Post, estimatedReadTime)
-- Post, estimatedReadTime
-- Post.Post, Post.estimatedReadTime, Post.encode, Post.decoder

import Post as P exposing (Post, estimatedReadTime)
-- Post, estimatedReadTime
-- P.Post, P.estimatedReadTime, P.encode, P.decoder

exposingを使うのはほんのちょっとだけにしておくのをお勧めします。まったく使わないか、ひとつのインポートだけで使うのが理想的でしょう。そうしないと、読むときに型や関数がどのモジュールから来ているのかを把握するのが難しくなっていくことがあります。「待って、filterPostByはどこから来たんだっけ? 引数は何を取るんだっけ?」exposingを追加するにつれて、コードを読んでいくのはどんどん難しくなっていきます。私はimport Html exposing (..) をよく使いますが、それ以外ではまったく使いません。まずは基本的なimportを使い、もし特に長いモジュール名であるときはasを使うことをお勧めします!

results matching ""

    No results matching ""