Dev日記

プログラミング学習のまとめなど

関数型よく知らない人のelm入門(3) - type alias詳解

毎日ちょっとずつではありますがelmを触ってます。

今回は type alias についてです。

結構クセが強くハマったので自分なりの理解をまとめていきます。

type alias

色んな言語で採用されている同様のものと同じように、 elm においても「型に別名を付ける」のが type alias です。

type alias UserId = Int

getUserNameForId UserId -> Maybe String

こんな感じで使えば「目的」が明確になり、引数や戻り値として分かりやすくなる…といいたいところですが、 elm上では、 type alias で別名宣言しても元の型と同じとしてみなされます

コードを見た方が早いかと。次のような感じです。

intValue : Int
intValue = 3

--普通にビルドが通り動く
getUserForId intValue

こういう宣言をした場合、別の型とみなしてコンパイルを通さない言語もありますが、 elm は通します。 ここ次で大事なのでしっかり把握してください。elmは別名の型は元の型と区別しません。

よって書いておいて何ですが、上記のような使い方はビルドエラー検出としては使えません。(ただし関数の引数・戻り値に使うことで可読性はあがります)


構造体

そこで使われるのが 構造体 として使い方、JavaでいうならEntity-classとしての使い方です。複数のデータを一括して保持するもの、ですね。

type alias Student = 
  { id : Int
  , name : String
  }

myStudent : Student
myStudent = Student 10 "Taro"

--IDと名前を連結する
joinData : Student -> String
joinData s = (toString s.id) ++ ":" ++ s.name

joinData myStudent -- "10:Taro"

こんな感じ。生成部分の表記が特殊ですが、ようは Student という Int と String を受け取る関数があるようなイメージですね。


ここまでは問題ないと思います。では次に同じ情報を保持する Teacher クラスを作ってみましょう。

--Student同様のデータを保持
type alias Teacher = {id: Int, name: String}

myTeacher : Teacher
myTeacher = Teacher 100 "Mike"

ここまではいいと思います。 では次のコードはどうなると思いますか?

--Studentのidとnameを連結する
getStudentData: Student -> String
getStudentData s = (toString s.id) ++ ":" ++ s.name


--それにTeachを渡す
getStudentDatas myTeacher

ビルドエラー?それとも値を出力する?

正解は「値を出力する」です。 100:Mike と表示されます。

このへん、普通の言語の感覚だと苦しみます。だって型が違うのに受け付けて動いているのですから。まるで動的型付け言語のような挙動です。

そこで思いだしてほしいのが最初の UserIdの例

そこで書きましたとおり、type alias はあくまで別名で、元の型が一緒なら区別しない ということです。

つまり、 Student と Teacher は内部的には同一のものとして扱われます。なんとなくこの辺はJaavscriptっぽいですね。

なので、特に型を記述しなくても呼び出せます

getStudentData {id=5, name="Hoge"}  --"5:Hoge"

ここ注意してください。


ただし、構造体の要素が違う場合はビルドが通りません。少ない場合や型が違う場合もダメですし、「要素が多い」場合もビルドエラーです。

--同じように id/nameはあるが一個多い
type alias Master = {id: Int, name:String, level: Int}

myMaster : Master
myMaster = Master 99 "Yoda" 999

getStudentData myMaster -- ビルドエラー

javascriptだと問題なく動くパターンですが、elmではビルドを通しません。MasterとStudentでは、元の型構造が異なるためです。

よって「type aliasとして別々でも、同じ型として扱われる」ことで困ることはあまりないと思います。


値の更新

これに関係してくるのが、 構造体の値の更新 です。

elmでは以下のようにして値を更新します。i

student = Student 1 "Taro"
student2 = {student | id = 2}

このようにすると、name は Taro のまま、 id のみ 2 になります。もちろんelmではこの辺の副作用がないので、 student 自体には影響を与えません。

java的にいうなら、「studentのcloneを作り、そのcloneの値を変更している」イメージです。

ここで問題。次のコードはビルドが通るでしょうか?

student = Student 1 "Taro"

--id が Int でなくString
student2 = {student | id = "2"} 

正解は… ビルドが通ります。実行時もエラーになりません

実はこの場合、 Student ではなく別の新しい型、 {id:String, name:String} が生成されて、student2はこの型になります。

よって次のコードはビルドが通りません。

getStudentData : Student -> String

student = Student 1 "Taro"
student2 = {student | id = "2"} 

getStudentData student2  -- ビルドエラー!

また、「元の構造体にない変数名」はビルドエラーです。

type alias Student = {id : Int, name : String}

student = Student 1 "Taro"

student2 = {student | level = 99} --levelがないのでビルドエラー

トリッキーな挙動ですね。代入する変数の型は違ってもいいのに、名前が違うのはダメという。

しかしこれが問題かというと、まず問題になりません。少なくとも自分はまだ経験していないです。

値更新時は意図しない型になりますが、その更新した値を使おうとするとどこかでビルドエラーになるからです。(上記の例でいう joinData)

なので普通問題にはならないと思います。たとえ型宣言部分を書き替えても、です。(どこかでビルドエラーになるはず)


子要素の更新

また厄介な点として、 構造体ツリーの子要素をダイレクトに更新できません 。以下のような本と出版社の情報を格納する構造体を考えてみましょう。

--出版社と書籍情報
type alias Publisher = { name : String, address : String}
type alias Book = { title : String, publisher : Publisher}

--インスタンス生成
book = Book "Secret" (Publisher "Kado" "Tokyo")

では出版社が引っ越したとします。book を更新しましょう。(※もちろんelmでは値の更新はできないので、値を変更した別インスタンス生成になります)

--ビルドエラー。直接子要素は更新できない
book2 ={book.publisher | address = "Osaka"}

--このパターンもダメ
book3 = { book | publisher = { book.publisher | address = "Osaka" } }

よって、このようなときは以下のように一度要素をバラすしかありません。

publisher1 = book.publisher
publisher2 = { publisher1 | address = "Osaka" }

book2 = { book | publisher = publisher2 }

book2.publisher.address -- Osaka

一旦子要素自体を変数に格納し、それをベースに値を更新。さらにそれを使って Book を更新します。

この程度ならいいですが、階層が深くなると相当面倒でしょうね….

Modelの設計時に注意してください。


まとめ

注意点をまとめますと

  • type alias で別名にしても、元の型が一緒ならelmは区別しない
  • 値更新時、違う型をいれてしまうと別の型としてあつかわれる
  • 一気に子要素を更新できない

この3点に注意しておけばまず困らないと思います。


クセが強いのは事実ですが、本当に学習が楽しい言語ですね。クライアントサイドがメインな都合で Haskell はイマイチ作りたいものがなく挫折したのですが、elmはフロントエンドを組めるので楽しいです。

また、ほとんどの場合でビルドさえ通れば問題なく動くのはちょっとした快感です(css等の表示ミスはありますが、ロジックはまず一発で動く)

フロントエンドの経験がある方前提ですが、Haskellのとっかかりとしてもオススメ。 Haskellほどエレガントではない ですが、理解しづらい部分がそぎ落とされているので入門的な意味では十分かと思います。