Haskellらしさって?「型」と「関数」の基本を解説!【第二言語としてのHaskell】

第二言語としてHaskellを学ぶ道案内。開発環境の準備から、Haskellらしいプログラミングの考え方まで、Haskell-jpのigrepさんが丁寧に解説します。

Haskellらしさって?「型」と「関数」の基本を解説!【第二言語としてのHaskell】

こんにちは。Haskell-jpの山本悠滋です。
この記事では、すでにプログラミング経験のある方向けに、第二言語としてHaskellを学ぶ道案内をしていきます。 環境の準備や、自明なサンプルプログラムの紹介にとどまらず、Haskellらしいプログラミングの考え方も伝えていく予定です。

Haskellについて

Haskellというと、「関数型」というキーワードが思い浮かぶ方も多いと思います。 確かにHaskellは、すべての関数がカリー化されており、それらを組み合わせてプログラムを書いていく関数プログラミングがしやすい言語です。 しかしHaskellは、関数型言語であると同時に、厳密かつ柔軟な型システムを持つ静的型付き言語でもあります。 さらに、その強力な型によってプログラムの副作用までも管理できる仕組みを備えています。 これらの特徴を、バグが少なく堅牢でメンテナンス性の高いソフトウェア作りに活用できるのが、Haskellというプログラミング言語だといえるでしょう。

実際、Haskellは、信頼性とスピードが求められる複雑なシステムやプロジェクトで数多く採用されています。 Facebookにおけるシステム悪用対策の基盤であるSigmaや、 朝日ネットの認証サーバーは、それなりに大きな規模でのHaskellの実用例として有名です。 Tsuru Capitalをはじめ、金融業界でも利用されています。 Haskellでプログラムを書いているというと、よく「何に使えるの」と聞かれるのですが、汎用プログラミング言語なのでだいたいの用途には利用できるのです。

現在、Haskellで書いたプログラムを実行するときにもっともよく利用されているのは、GHC(Glasgow Haskell Compiler)というコンパイラです。 GHCでは、標準のHaskellをさらに便利に使えるように、さまざまな言語拡張も提供されています。

2017年7月22日にGHC 8.2.1がリリースされていますが、本記事で説明するHaskellプログラムはすべて、「Haskellの開発環境を整備する」のセクションで解説するツール「stack」でインストールできるGHC 8.0.2(執筆時点)で動作を確認しています。

Haskellの開発環境を整備する

Haskellの開発環境を整備する方法はいくつかありますが、今回は初めて環境を構築する方におすすめな、「stack」というツールを使用した方法を紹介します。 stackのインストールと設定方法はstackの公式サイトに一通り載っていますが、英語ということもあるので、この記事でも説明しておきます。

1Home - The Haskell Tool Stack2

LinuxやMacでインストールする

LinuxやMacでstackをインストールする方法は簡単です。 下記のようにcurlコマンドでダウンロードしたシェルスクリプトをそのまま実行すれば、使用しているOSを自動で検出して、インストールしてくれます。

$ curl -sSL https://get.haskellstack.org/ | sh

Windowsでインストールする

Windowsでstackをインストールする場合も基本的には単純で、公式のドキュメントの「Windows」セクションに用意されているリンクから適切なインストーラーをダウンロードし、実行しましょう。また、Chocolateyをお使いの方は、choco install haskell-stackでもインストールできます。

Windowsでstackをインストールする場合には、次の点に注意してください。

Windowsのユーザー名が日本語になっていると失敗する

Windowsユーザーの方が上記の手順でstackを用意する際、OSのユーザー名が日本語となっていると、GHCのインストール時にエラーになってしまう場合があるそうです。 特にWindows 8やWindows 10では、そうと気づかないうちにユーザー名が日本語になってしまっていることが多いので、確認しておきましょう(筆者も長年のWindowsユーザーですが、たまたまこの問題に出くわすことはありませんでした)

下記の記事などを参考に、新しくWindowsのユーザーを日本語以外で作り直して試してみるほうがいいかもしれません。

新しくWindowsのユーザーを作りたくない、という場合には、環境変数LOCALAPPDATAを変更して日本語を含まないパスに変えるという手もあります。

stackはLOCALAPPDATAに書かれたディレクトリーのPrograms\stack以下にインストールしたGHCを置きますが、このLOCALAPPDATAに日本語のパスが含まれているとエラーになるようです。 LOCALAPPDATAは、デフォルトでWindowsのユーザーフォルダーのパス(つまり C:\Users\[ユーザー名]より下に作られるので、ユーザー名に日本語が含まれていると問題になります。

環境変数LOCALAPPDATAを変更し、日本語を含まないパスに変えればよさそうですが、 LOCALAPPDATAはstackのほかにもさまざまなアプリケーションが使用しているディレクトリーなので、変更の際は注意が必要です。 影響を最小限にとどめるために、バッチファイルを作り、環境変数PATHにおけるより優先度の高い位置にあるディレクトリーに置いてラップする、という手もあります。

以下は、LOCALAPPDATAC:\foobarに設定してstackを実行する場合の、ラップ用バッチファイルの例です。

@echo off
set LOCALAPPDATA=C:\foobar
[実際にstackがインストールされているパス]\stack %1 %2 %3 %4 %5 %6 %7 %8 %9

これをstack.batという名前で保存して、環境変数PATHの先頭のパスに配置すれば、stackコマンドを実行するときだけLOCALAPPDATAを変更することができます。

試しに使ってみましょう(Haskellで関数の定義と呼び出し)

ここまでの方法でstackをインストールできたら、早速動かしてみましょう。……とその前に、stackを使ってHaskellの最も有名なコンパイラー、GHCをインストールする必要があります。

stackは、たとえるなら、RubyのrbenvPythonのpyenvのように、処理系(HaskellであればGHC)のさまざまなバージョンを分離してインストールできるようにするためのものです {$annotation_1}。なので本当にHaskellでの開発をできるようにするためには、stack setupコマンドを利用して、GHCをインストールする必要があります。

やり方は簡単で、下記のようにstack setupコマンドを実行するだけです。

$ stack setup

しばらく待つと、GHCのインストールが完了します。完了したら、確認のためにGHCのバージョンを見てみましょう。 stackでインストールしたGHCを利用するには、stack ghcコマンドを使います。

$ stack ghc --version
Invalid option `--version'

Usage: stack.exe ghc [-- ARGS (e.g. stack ghc -- X.hs -o x)] ([--plain] |
                     [--[no-]ghc-package-path] [--[no-]stack-exe]
                     [--package ARG] [--rts-options RTSFLAG]) [--help]
  Run ghc

おっと、「--versionというオプションは無効だInvalid option」と言われてしまいました。ghcには--versionオプションがあるはずなのですが、これはどういうことでしょう?

これはstackの残念な仕様で、stackコマンド経由でghc--versionなどのオプションを渡そうとした場合、意図に反してstackコマンドが(正確には、stackコマンドのサブコマンドであるstack ghcが)--versionオプションを解釈してしまうことによるエラーです。 これを回避するには、--versionオプションより前に--を渡します。

stackコマンドが--versionオプションを解釈するのをやめさせたうえで、あらためて実行してみましょう。

$ stack ghc -- --version
The Glorious Glasgow Haskell Compilation System, version 8.0.2

ちゃんとGHCのバージョンが見えましたね! 「--を渡すことでそれ以降の引数をオプションとして解釈させない」というテクニックは、stackコマンドに限らず、オプションを解釈する大抵のコマンドで使用できるので、ぜひ覚えておいてください。

なお、上記の通り、今回は執筆時点でstack setupした場合にデフォルトでインストールされる、GHC 8.0.2を使用して説明します。 使用するバージョンによって表示される内容が異なる場合があります。あらかじめご了承ください。

対話環境GHCiを使ってみましょう

ここまでの方法でGHCのインストールが確認できたら、続いてGHC付属の対話環境(REPL)であるGHCiを使用してみましょう。 次のようにstack ghciコマンドを実行すると起動できます(出力結果は環境によって微妙に異なります)

$ stack ghci
Configuring GHCi with the following packages:
GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from C:\Users\user\AppData\Local\Temp\ghci4628\ghci-script
Prelude>

GHCiを起動したら、とりあえず電卓のように使ってみましょう。

Prelude> 1 + 1
2
-- 身長170cm, 体重60kgの人のBMI
Prelude> 60 / 1.70 ^ 2 -- 累乗にはキャレット「^」を使います。
20.761245674740486

ちゃんと計算できますね。 なお、「--」で始まる行はHaskellのコメントです。このようにGHCiのなかでも使えます。

Haskellでは関数をイコールで定義する

続いて、GHCiからHaskellのソースファイルを読んでみましょう。GHCiでは:lというコマンド:loadの省略形)で、引数に渡したHaskellのソースファイルを読み、その動作を確認できます:lはあくまでもGHCi専用のコマンドであり、Haskellの文法とは関係がありません。念のためご注意を)

以降、この記事では、「まずHaskellのソースファイルを書き、:lコマンドでそのファイルを読んで動作を確認する」という手順を繰り返すことで説明を進めていきます。ここでGHCiに慣れておきましょう。

手始めに、身長(height)と体重(weight)を受け取って、肥満度を表すBMI(Body Mass Index)を返す関数でも定義してみましょう。 下記の1行を記述したファイルをbmi.hsという名前で保存してください。

bmi height weight = weight / height ^ 2

この1行で、bmiという関数を定義しています。

せっかくなので、ここでHaskellにおける関数定義の文法を解説しておきましょう。 Haskellでは、次のような形式で関数を定義します。

関数名 仮引数名1 仮引数名2 ... 仮引数名N = 〔関数の本体〕

先ほどのbmi関数でいうと、bmiが関数名、heightweightが仮引数名です。 そして、イコール「=」より後ろの「weight / height ^ 2」の部分が〔関数の本体〕に該当します。仮引数のweightheightを使った計算式になっていることがわかりますね。

このようにHaskellでは、関数を定義する際も、まるで変数を定義するかのように=を使います。ちょっと変わっていますね。

また、戻り値を示すのにreturnのような構文を一切使っていない点にも注目してください。 Haskellには「return文」のようなものはなく、=以降に書いた式の結果がそのまま関数の戻り値となります。

3

Haskellの関数は簡単な構文で使える

さてさて説明はこのくらいにして、定義したbmi関数をGHCi上で使用してみましょう。 まずは、:l bmi.hsとして、bmi関数を書いたファイルを読み込みます。

Prelude> :l bmi.hs
[1 of 1] Compiling Main             ( bmi.hs, interpreted )
Ok, modules loaded: Main.

関数を定義したファイルを読み込めたら、次のようにbmi 身長 体重という形式で入力してbmi関数を実行できます。

*Main> bmi 1.7 60
20.761245674740486

ちゃんと実行できるようですね!

上記の通り、Haskellでは、関数呼び出しの際に丸カッコを使うこともないですし、複数の引数を区切るのにカンマを使うこともありません。 関数名と引数を、すべてスペースで区切って並べるだけです(関数の呼び出しで丸カッコを使うとしたら、結合の優先順位を示すためだけに使います)

Haskellに限らずとも、プログラミングでは関数呼び出しは非常に頻繁に書くものです。よく使うものが簡単な構文で使えるのはうれしいですよね!

4

最後に、GHCiを終了するときは:q:quitの省略形)と入力しましょう。

> :q
Leaving GHCi.

Haskellの基本的な型に親しもう(GHCiをもっと使いこなしつつ)

Haskellにおいて標準で使えるデータ型や、そのリテラルについて紹介します。

ついでに、もう少しGHCiと親しくなりましょう。 終了させた直後で恐縮ですが、もう一度stack ghciコマンドでGHCiを起動してください。

$ stack ghci
>

Bool型(ついでに「:t」コマンド)

Boolは、プログラミングでおなじみの論理値を表す型です。Haskellでは、真がTrue、偽がFalseで表されます (Pythonの真偽値のように、大文字で始まります)

> True
True

> False
False

GHCiには、:tコマンド:typeの省略形)という、式の型を確かめるためのコマンドが用意されています。 TrueFalseがBool型であることを、:tコマンドを使って確かめてみましょう。

> :t True
True :: Bool

> :t False
False :: Bool

:tコマンドを実行すると、式の後に続けて、:: 型の名前という形式で、対象の式の型が何かを教えてくれます。 上記の例は単純すぎてあまりありがたみがないですが、もっと複雑な式や、初めて使用する関数について調べるときには、:tコマンドで型を確認することがプログラムの理解を確実に促進してくれます。

5

論理積や論理和については、おなじみの&&||が使えます。

> True && False
> False
> True && True
> False
> True || False
> True
> False || False
> False

論理の否定も、多くのプログラミング言語でおなじみの!……と言いたいところですが、違います! Bool型の否定は、文字通りnotです。

> not True
False
> not False
True

個人的には、!が否定を表すことに違和感があるので、Haskellを学んで論理否定がnotであると知ったときは大変うれしかったです! !よりも視覚的に目立ちますしね!

関数型

Haskellのnotと、多くのプログラミング言語における!には、見た目以外にも大きな違いがあります。 こうした論理演算子は、多くのプログラミング言語では関数とされていませんが、Haskellではnotもまた関数なのです。

関数なので、notにも:tコマンドを使えます。実際に試してみましょう。

> :t not
not :: Bool -> Bool

型として、Bool -> Boolという文字列が返ってきました。 これは、「Bool(型の値)を受け取ってBool(型の値)を返す関数型」を表しています。 ->という記号が、型を表すのに使われるという点に、ちょっと面食らった方がいるかもしれません。

Haskellでは、いわゆる「関数型プログラミング言語」の多くと同じように、関数もファーストクラスオブジェクトとなっています。 つまり、関数も、Boolや文字列、整数などと同様に、変数に代入したり、関数の引数として渡したりすることができるのです。 実をいえば、bmi関数を定義するときに使った

bmi height weight = weight / height ^ 2

という構文も、関数オブジェクトをbmiという変数に代入する構文(の1つ)に過ぎません。

リスト型

リスト型は下記のようなリテラルで表されます。 :tコマンドで型を見ながら確かめてみましょう。

> :t [True, False, False]
[True, False, False] :: [Bool]

角括弧で囲った[Bool]という表記が、「Bool(の値)のリスト型」であることを表しています。 リストの長さに制限はありませんが、リストの要素はすべて同じ型でなければなりません。

リストの長さを知りたいときは、length関数を使いましょう。

> length [True, False, False]
3

リストを結合したいときは、++という演算子を使います。

> [True] ++ [True, False]
[True,True,False]

reverse関数を使うと、リストを逆順に並び替えることができます。

> reverse [True, True, False]
[False,True,True]

リストが空かどうか知りたいときは、nullという関数を使います。 ちょっと変な名前なのが悩ましいですね。

> null [False, False, True]
False
> null []
True

文字型・文字列型

Haskellでは、文字型と文字列型が厳密に分かれています。

まず、文字型の値は、下記のようにシングルクォート「'」で囲むことで表記します。

> :t 'a'
'a' :: Char

それに対して、文字列型の値は、ダブルクォート「"」で囲むことで表記します。

> :t "a"
"a" :: [Char]

:tの結果が[Char]となっていることからわかるとおり、Haskellの標準の文字列は実際には「文字のリスト」です。 なので、"a"['a']と等価です。

> ['a']
"a"

GHCi上でも、ダブルクォートで囲って表示されましたね。

上記のように:tコマンドでは[Char]と表示される文字列ですが、便宜のため、[Char]にはStringというおなじみの名前で、型の別名がついています。 例えば、何行にもまたがる文字列を受け取って1行ごとに分かれた文字列のリストへと変換する関数linesは、「Stringを受け取ってStringのリストを返す関数」として型付けされています。

> :t lines
lines :: String -> [String]

ちなみに、シングルクォートで文字列を書こうとすると、下記のようなエラーを出してくれます。

> 'abc'

<interactive>:27:1: error:
    • Syntax error on 'abc'
      Perhaps you intended to use TemplateHaskell or TemplateHaskellQuotes
    • In the Template Haskell quotation 'abc'

RubyやJavaScriptなどで、文字列をダブルクォートで書くかシングルクォートで書くかをめぐって議論になることもありますが、Haskellではそのような「自転車置場の議論(bike-shed discussion)(自転車置き場の屋根を何色に塗るかという、あまり実益のない議論)に悩まされずに済みますね。

(なお、エラーメッセージで触れているとおり、シングルクォートで始まる文字列はTemplate Haskellというコンパイル時プログラミングのために使用されることがあります。 Template Haskellについては今回は割愛します。)

さて、この文字列、実際には文字のリストなので、リストに使える関数はすべて文字列に対しても使えます。

結合したいときは ++ が使えますし、

> "foo" ++ "bar"
"foobar"

反転させたいときはreverse関数が使えます。

> reverse "abc"
"cba"
文字型・文字列型についてもう少し

ここで、いいお知らせと悪いお知らせがあります。

まずは、いいお知らせです。 Haskellの文字型は、内部的にはUnicodeの1文字として表現されるので、日本語の文字も普通に使えます! 記念に自分の名前を漢字やカタカナでGHCiに打ち込んでみましょう!

> "山本悠滋"
"\23665\26412\24736\28363"

おっと……、何やら符号化された形で出力されてしまいましたね。

これが、Haskellの文字型と文字列型についての悪いお知らせです。 GHCiで文字を表示すると、日本語で使われる文字は、上記のようなエスケープシーケンスを使った特別な文字リテラルで表示されてしまうのです(これについての詳細は「Real World Haskell」の付録が簡潔にまとまっています)

ちゃんと日本語として読める状態で表示させたい場合には、後述するputStrLn関数を使うのが一番簡単でしょう。

> putStrLn "\23665\26412\24736\28363"
山本悠滋
> putStrLn "山本悠滋"
山本悠滋

あるいは、unicode-showパッケージを使うという手もあります。 下記のコマンドでunicode-showというパッケージを入れた上で、

$ stack install unicode-show

GHCiの設定ファイル~/.ghci(WindowsではC:\Users\<ユーザー名>\.ghciに以下の内容を追加してください。

import qualified Text.Show.Unicode
:set -interactive-print=Text.Show.Unicode.uprint

そのうえでstackからGHCiを再起動して、あらためて文字列リテラルで日本語を入力してみましょう。

$ stack ghci
> "山本悠滋"
"山本悠滋"

今度は無事に筆者の名前が表示されました!

数値型と型クラスについて簡単に

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く