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

レガシーコード生産ガイド

私に教えられることなら

Elmで相互再帰している型のモジュール化

個人的にElmやOCamlで好きなのが、型別にモジュールを作るという整理の仕方。

次のようにFoo型のためだけの関数をまとめておいて

module Foo exposing (..)

type alias Foo =
  { foo : Int
  }

init : Foo
init = { foo = 0 }

add : Int -> Foo -> Foo
add n x =
  { x | x.foo = x.foo + n }

別モジュールからインポートして、モジュール名と共に呼び出す。

module Main exposing (..)

import Foo

answer : Foo
answer = Foo.init |> Foo.add 42

多少冗長なんだけど、リファクタリング時に移しやすく、また書いてからある程度の期間を経てもすんなり読める。それと、命名規則addFoofooAddなどのようにブレることがなく、Foo.addと限定することができる。命名規則を考えたり覚えておかなくて良いので楽になる。

しかしちょっと問題もある。次のように相互再帰している型があるとき

type TypeA
  = B TypeB

type TypeB
  = A TypeA

TypeA、TypeBの型宣言を2つのモジュールに分けることはできない。相互再帰したimportは使えない。

解決策として

  1. どちらかに型変数を持たせて、使用モジュールから解決する
  2. 型だけのモジュールと、各型用の関数のモジュールに分ける

を思いついた。1はコードが複雑になりすぎるし、単に整理したいだけなのにプログラムの意味が増える(多相になる)のが気持ち悪く感じたのでやめた。

2はimport文が若干増えるけど、それ以外の問題は特に無さそう。

例えばファイル構成は以下のようになる

- Types.elm
- Types/
  - TypeA.elm
  - TypeB.elm

まず型のみを書いたTypes.elmを用意して

-- Types.elm
module Types exposing (..)

type typeA
  = B TypeB

type typeB
  = A TypeA

それぞれの型のための関数はまた別にモジュールを作る。

-- Types/TypeA.elm
module Types.TypeA exposing (..)

import Types exposing (..)
import Types.TypeB as TypeB

foo : TypeB -> TypeA
-- 以下略

相互再帰している型にまたがった関数を書きたい時は、

  1. importが循環しないなら、参照している側に
  2. 循環する場合は、両方をimportする上位のモジュールに

分けた方がいい。

例えば上の場合はTypes/TypeA.elmからTypes/TypeB.elmをimportしているけど、逆にTypeBの方からTypeAをimportしたくなったときは、多分整理の仕方が間違っているので2に従って上位モジュールに書く。

「できない」事によって、意味についてちゃんと考えてからの整理が強制されるのは良いと思った。

広告を非表示にする