Haskellの型入門

Posted by Takuya Noguchi on November 30, 2017 · 2 mins read

NaClの野口です。

最近Haskellを勉強し始めました。
勉強を始めてまだ日が浅いですが、面白い概念を多く学ぶことが出来たと感じています。

本記事ではHaskellの型について自分が理解したことを簡単にまとめます。
Haskellを動かす環境の構築についても記述しますので、読者の方も実際にコードを動かしてみてください。

環境

本記事内ではGHCiという対話形式でHaskellのコードを実行できるツールを使用します。
また実行環境のOSはUbuntuであることを前提として記載しています。

ではさっそく以下のコマンドを実行してGHCiをインストールしましょう。
Ubuntu以外のOSを使用している場合は、ドキュメントを参照してください。

上記コマンドを実行すると様々なツールがインストールされますが、本記事内ではGHCiのみ使用します。

実行方法

HaskellのコードをGHCiで実行する方法について記載します。
まず以下のサンプルコードをtest.hsという名前で作業用ディレクトリに保存してください。

後で詳しく解説しますが、上記コードでは引数2つを加算する関数(add2)の定義を行っています。

次にサンプルコードを保存したディレクトリ内で以下のコマンドを実行し、GHCiを起動します。

GHCiの起動に成功した場合は、プロンプト(Prelude>)が表示されて入力待ちの状態になります。
ではサンプルコードをGHCiにロードしてみましょう。

:loadのように「:」が付いているものはGHCiのコマンドです。
上記のようなメッセージが表示され、プロンプト(*Main>)が表示されればロード成功です。

<no location info>: can't find file: ./test.hs のようなメッセージが表示された場合は、
GHCiを起動したディレクトリ内にサンプルコードが存在することをもう一度確認してください。

サンプルコードのロードに成功後、関数(add2)が実行できることを確認します。
関数の引数は カンマで区切る必要がない ことに注意してください。

関数に2つの引数を渡して実行すると、それらを加算した値が返ってくることが確認できました。

関数の型宣言

まずHaskellには様々な型があります。
例えば整数を表すInt型、文字を表すChar型、文字列を表すString型などです。
他にも知りたい方は基本的なデータ型 - ウォークスルー Haskellを参照してください。

では関数の型宣言についての説明に移ります。
関数の型宣言とは「関数の引数と戻り値がどのような型であるかを宣言する」ことです。
先程実行したサンプルコードで関数の型宣言を行っている箇所を以下に示します。

日本語で説明すると、「add2はInt型の値を2つ入力し、Int型の値を1つ出力する関数」となります。
最後の型が戻り値、それ以外は引数の型を順番に記述していると考えてください。

つまり引数が3つになった場合の関数(add3)の型宣言は以下のようになるということです。

またGHCiには関数の型を調べるための便利なコマンドがあります。

型変数、型クラス

型変数、型クラスという概念について説明する前に、関数(add2)の挙動を詳しく見てみます。
引数に整数を渡したときは問題なく動いています。

では引数に小数を渡してみましょう。

引数に小数(2.0)を渡したことが原因でエラーが発生しました。
このままでは小数の足し算が出来ないので修正したいと思います。

まずInt型を小数を許容する型(Float型)に変更する方法から試してみましょう。

GHCiに変更をロードして確認してみます。

問題なく計算できていますね。
では2つの引数が整数の場合はどうなるのでしょうか。

結果が小数になってしまいましたね。
2つの引数が整数の場合は整数が戻り値になって欲しいです。

上記を解決するためには型変数という概念を使用します。
型変数について詳しい説明をする前に、型変数を使ったバージョンのコード示します。

上記コードのaが 型変数 です。
サンプルでは型変数名にaを使っていますが、b, cや複数文字を使っても問題ありません。

この型変数は任意の型が入ることを示しています。
ただし同じ名前の型変数の型は、全て同じであることに注意してください。

では変更したコードをGHCiにロードしてみましょう。

・・・エラーが発生してロードに失敗していますね。
エラーメッセージを読むと、ここまで意識していなかった「+」関数が関係しているようです。

エラーを解決するために、まず「+」関数の型を調べてみましょう。
ここでは(+)のように丸括弧で囲む必要があることに注意してください。

Num a => は初めて見る書き方ですが、これは型変数aに対して型クラス制約を行っています。
上記の一文を理解するためには、「型クラス」と「型クラス制約」という2つの概念について知る必要があります。
ただし以降の説明で出てくる「クラス、メソッド、インスタンス」という用語は、オブジェクト指向言語の文脈で使用する時と意味が異なる ことに注意してください。

ではまず1つ目の型クラスについて説明します。
型クラスとは「複数の型に共通する操作(メソッド)をまとめたもの」で、型クラスに属する 型(値ではない) をその型クラスのインスタンスと呼びます。

「+」関数の型宣言ではNumが型クラスに該当します。
ここで型クラス自体は型ではないことに注意してください。
つまり型宣言の際に Num -> Num -> Num のように記述することは出来ません。

上記を理解したところで型クラスNumがどのように定義されているか、GHCiのコマンドを使って調べてみましょう。
--の右側はコメントであることに注意してください。

「+」、「-」、「*」などがメソッド、instanceキーワードを使って定義している、Word、Integer、Intなどがインスタンスです。

次に2つ目の型クラス制約について説明します。
日本語だと「型クラス制約のある型変数の型は、その型クラスのインスタンスでなければならない」と説明できます。
型変数をそのまま使うと任意の型を許容してしまうため、型クラス制約を型変数にかける必要があるということです。

上記のことを理解すると、先程のエラーメッセージの内容も理解することができます。
以下に箇条書きで記載すると、

  1. add2の入力が「+」関数の入力、「+」関数の出力がadd2の出力である
  2. 「+」関数の型変数は型クラスNumによる制約を受けている
  • Word、Integer、Int、Float、Double を許容
  1. ただしサンプルコードの実装だとadd2の型変数の型が型クラスNumのインスタンス以外の場合がある
  • Char、String など
  1. add2が型クラスNumのインスタンス以外の型を受けとっても実行に失敗する
  2. 上記よりadd2の型変数にも型クラスNumによる制約をかけるべき

と理解できます。
型に関するバグを実行前に防ぐことができるのは嬉しいですね。

ではサンプルコードを型クラス制約を追加したバージョンに変更してみましょう。

では変更したコードをGHCiにロードしてみましょう。

整数同士の加算も期待通りの結果が返ってきていますね。

おわりに

書きはじめると型に関する簡単な説明だけで1つの記事になってしまいました。

Haskellにはカリー化、遅延評価、モナドなど興味深い概念が数多く存在しています。
どれも面白い概念なので、興味を持った方はぜひ調べてみてください。

参考サイト