こわくないHaskell入門(初級)

  • 254
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

手続き型に慣れた人にもやさしい、こわくないHaskell入門記事です。
なぜHaskellを学ぶと良いか」も参考にしていただければ幸いです。

まえがき

Haskellと聞いて、何を思い浮かべますか?

  • モナド
  • 関数型
  • 遅延評価
  • 第4世代Intel Coreプロセッサ
  • アライグマ

いろいろ思い浮かべるかもしれませんが、Haskellがすばらしいのはモナドを利用しているからでも、遅延評価型の純粋関数型言語だからでもありません。
いろいろな「Haskellらしさ」が集まって、その結果Haskellにしかないすばらしい魅力を提供してくれます。
それは、決していままでのパラダイムと対立するものではなく、

  • 手続き型
  • 構造化プログラミング
  • オブジェクト指向

のようなこれまでの便利な道具をうまく抽象化しながら統合して作り上げられたものです。

  • PHP
  • javascript
  • C++
  • Java

などにあなたが費やしてきた時間は無駄にはなりません。
関数型だったり、モナドを使っているからと言って、怖いものではないのです。

しかし、世の中には「オブジェクト指向と関数型の宗教戦争」のような我々と異なる世界線でのできごとを持ちだして、我々を混乱させようとする闇の組織も存在します。
そういった背景もあってか、新しくHaskellを学ぶ人は

  • 関数型とはどういうことか
  • Haskellはなにがこれまでと違うのか

を知りたがっていることが多く、そのためか多くの入門書も既存の手続き型ベースな(もちろんオブジェクト指向も手続き型ベースです)プログラミング言語と異なる点を強調しています。

とはいえ、そろそろHaskellも門戸を開き、「にわか」の参入を許してもいい頃ではないでしょうか。
きっと、お行儀のいいHaskellなら、にわかが増えてもPHPみたいな悲惨なことにならないでしょう。
そこで、怖いお兄さんたちからマサカリが飛んでくるのを恐れず、

  • Haskellは怖くないよ
  • いままでと違わないよ

を根本においたHaskellの入門記事を書いてみようと思います。

Haskellと手続き型の関係については、あとで@ruiccさんのモナド入門以前あたりを読んでみると面白いかもしれません。

対象者

  • なんかもっと生産性の上がる言語を習得したい人
  • なんかHaskell入門しようとして挫折しかけた人
  • なんかマサカリ投げたい怖いお兄さんたち

結論

Haskellこわくないよ。
とっても便利だよ。
とっても学習障壁ひくいよ。

なのに、スクリプト言語みたいにサクサクかけて、コンパイル言語の実行時性能をもってるよ。
しかもたいていのありがちなバグはコンパイラさんが見つけて教えてくれるよ。
バグが入りにくいから金融系とかでも使われ始めてるらしいよ1

実行方法

Haskellはコンパイル言語とスクリプト言語のいいとこ取りをしています。
だからスクリプト言語のように気軽に実行して遊べます。

runghc foobar.hs arg1 arg2

もちろん、コンパイルして実行ファイルを作ることもできます。

ghc foobar.hs
./foobar arg1 arg2

でもHaskellと遊ぶにはGHCというコンパイラがインストールされた環境が必要です。
ググってください。
と言ってここで脱落者を増やしても意味がないので、Haskellで遊べる環境を用意しておきました。

上にオンラインエディタ、下にLinuxのターミナルがありますね?
下のターミナルにlsと打ち込んでみましょう。
下に表示されたファイルの中のmain.txtがHaskellのプログラムです。
ふつう、Haskellのファイルは.hsの拡張子をつけるのが流儀ですが、今回はのっぴきならない事情2があって.txtになっています。
気にしないでください。

vimを使い慣れている人はvimでmain.txtを開いてみましょう。
vimなんて使ったことない?
そんなあなたのために、上にエディタが用意されています。
エディタの左にファイル一覧がありますね?
main.txtをクリックすると、内容を編集できるようになります。
編集したら、その上の"Save and Run"ボタンをクリックして確定できます。

main.hs
main = do
  putStrLn "Hello Haskell"

試しに下のターミナルに

runghc main.txt

と打ってエンターを押してみてください。
エディタの上の緑の"Run"ボタンをクリックでも実行できます。

Hello Haskellと表示されたら、あなたもこれでHaskellプログラマーです。
やったね!

基礎

main

Haskellにはmain関数があります。
C++とかJavaとかと一緒ですね。ほら、なにも変わらない。

このmain関数が、プログラム実行時に最初に呼ばれます。
文法は

main.hs
main = do
  なんか
  ここに
  命令を
  いっぱい
  かくよ

です。
よく{}とかで関数の「ひとかたまり」を明示する言語がありますが、Haskellは行頭のスペースによるインデントでプログラムの「ひとかたまり」を示します。
だって、{}とかにしたら、いちおう動くけど、インデントぐっちゃぐっちゃで読みにくいコード書くやついるじゃないですか。
ああいううんこ製造機を撲滅するために、インデントに意味を持たせています。
おせっかいだけど許してください。

コメント

プログラムにコメントを書かないやつは何をやらせてもダメです。
僕もよくコメントを書くのをサボります。ごめんなさい。

comment.hs
-- こめんとだよ
{- 
  複数行の
  こめんとだよ
-}

実は、Haskellにはpythonのpydocとかdoctestみたいな仕組みがあって、
プログラムの中に特殊な形式のコメントとして関数の説明を書くといい感じにドキュメント化してくれたり(haddock)、
プログラムの中に関数が満たす性質をちょろっと書いておくと自動的にテストしてくれたり(doctest, Quickcheck)してくれる便利なものがあります。

もうちょっとお勉強してから、あとでその便利機能を使ってみましょう。

標準入出力

サンプルを実行してみましょう。
getLine:と出力されたらなにか適当に文字を打ってEnterを押してください。
同じ内容が下に反復されます。
続いてgetContents:と出力されたら、また適当に文字を打ってEnterを押してみてください。
今度もまた同じ内容が繰り返されました。
でも、引き続きなにか打てるみたいです。
何回か遊んでみましょう。
飽きたらCtrl+Dで終了です。Ctrl+Dがうまく効かない時はCtrl+Shift+Dを押すといいかもしれません。

stdio.hs
main = do
  putStrLn "getLine:"     -- 標準出力に文字列を出力
  l <- getLine            -- 一行の標準入力を定数`l`に代入
  putStrLn l              -- 文字列の入った定数`l`の内容を標準出力に出力

  putStrLn "getContents:"  -- 標準出力に文字列を出力
  c <- getContents        -- Ctrl+Dが押されるまでの複数行の標準入力を定数`c`に代入
  putStrLn c              -- 文字列の入った定数`c`の内容を標準出力に出力

なんだか<-とかいう見慣れない記号がありますが、
とりあえず、入力を受け取る系の関数から値をもらうときには<-という矢印みたいなのを使うと覚えておきましょう。
また、文字列を標準出力する関数putStrLnに、関数にありがちな引数を囲む()がついていないですね。
あんなものは飾りです。
いえ、()がないのはカリー化という超絶便利な機能によるものですが、いまは気にしないでください。
(@igrep さんご指摘ありがとうございます)
いえ、()がないのはカリー化という超絶便利な機能を使いやすくする効果があるのですが、いまは気にしないでください。

ファイル入出力

サンプル

file.hs
main = do
  i <- readFile "input.txt" -- "input.txt"というファイルの中身を文字列として代入
  putStrLn i                -- 標準出力に出力してみる
  writeFile "output.txt" i  -- "output.txt"というファイルに`i`の内容を書き込む

writeFileは引数を2つとる関数です。複数の引数をとる関数は引数をスペース区切りであとに書くだけです。
コンマはいりません。
runghc main.txtを実行したあとにcat output.txtをしてちゃんと内容が書き込まれたか確かめてみてください。

定数

すでに標準入出力のところにでてきました。定数です。
言語によっては変数の宣言時にconstとかfinalとかくっつけて宣言するあれです。
つまり、ほとんど変数みたいに使えるけど、値を代入するチャンスが最初の一回しか与えられない変数の劣化版みたいなやつです。
だったら変数を使えって?
いままでの人生で、変数に変な値を代入し直しちゃう系のバグを何回書いたか数えてください。
そんなバグ書いた覚えがないなら、あなたは超絶すごいプログラマか、
あるいは自分のミスの原因をすぐに忘れちゃううんこ製造機です。

const.hs
main = do
  let foo = "うんこ"  -- 普通の代入(文字列)
  let bar = 3.4       -- 普通の代入(小数)
  let baz = True      -- 普通の代入(真偽値)
  c <- getContents    -- 標準入力をうけとるときの書式はこっち
  putStrLn foo        -- "うんこ"って表示する
  putStrLn (show bar) -- `show`は文字列以外のものを文字列に変換する関数
  print baz           -- `show`してから`putStrLn`する便利関数`print`もあるよ

標準入力が絡むときだけ、文法に注意してください。

変数

もちろん、変数だって使えます。
だれだよ。Haskellは変数の副作用を「絶対にゆるさない」とか言ったやつ。

var.hs
import Data.IORef   -- 変数を使うときは最初にこれを書く

main = do 
  v <- newIORef 0   -- 新しい変数`v`の内容を`0`で初期化
  c <- readIORef v  -- 変数`v`の内容を定数`c`に代入
  print c           -- `v`の中身`0`が表示される
  writeIORef v (c + 1)  -- `c`の値に`+ 1`した値を変数`v`に代入
  c2 <- readIORef v -- 変数`v`の内容を定数`c'`に代入
  print c2          -- `1`と表示される

ファイル入出力と似てますね!
変数を意味する特殊なファイルに対して読み書きをしていると思うと、なんだか統一感があってかっこ良くないですか?

「いちいち定数に代入して使うのがめんどくさい」って?
大丈夫。
「高階関数」と「カリー化」「ラムダ式」あたりを使えるようになれば、modifyIORefという便利な関数を手に入れることができます。
それに、Haskellに詳しくなって「変数とかマジ使わねぇし」って言っているあなたが私のウルトラハッピーな脳内お花畑にはすでにいますよ。

関数

関数には2種類あります。

  • 入出力系の関数を内部で使っている関数と
  • そういうのを使っていない関数

です。
前者は関数の引数に対して関数の返り値が一意に定まらないため、本当に意図した動作をしているのかテストしにくいという特性があります。
だって、標準入力を使ってたりしたら、引数だけじゃなくて、ユーザがターミナルになんて打つかによって関数の結果が変わっちゃうじゃないですか。
一方で後者は、なにか適当に引数を与えてその結果を見れば、いつでも同じ値を返してくれるため、とてもテストしやすいです。
今後、前者を「純粋でない関数」、後者を「純粋な関数」と呼びます。

サンプル

純粋でない関数(入出力系の関数を使う関数)の定義

前者はmain関数の形式で定義します。
引数がある場合は関数名の後に半角スペース区切りで仮の名前をつけます。
また、返り値がある場合はreturnを使ってその値を返します。

io.hs
関数名 引数名1 引数名2 = do
  ここに
  なんか
  処理を
  書くよ
  return なんか

この関数を呼び出すときは、入力系の関数の使い方と同じく、<-やじるしを使います。
もちろん、この新しく定義したnotPureも入力系の関数なので、notPureを呼び出す関数も同じ形式で定義します。

io.hs
main = do
  name <- getName "☆"
  putStrLn name

getName str = do
  putStrLn "姓: "
  lastName <- getLine
  putStrLn "名: "
  firstName <- getLine
  return (lastName ++ str ++ firstName)

純粋な関数(入出力系の関数を使わない関数)の定義

テストなどのしやすさから、できるだけこっちのタイプの関数を定義して使うようにしましょう。

pure.hs
関数名 引数1 引数2 = 局所定数1とか局所定数2を使った式
 where
  局所定数1 = なんか
  局所定数2 = なんか

whereの下に定義した定数はこの関数の内部でのみ使うことができます。
もうちょっと具体的な例を見てみましょう。

sample.hs
main = do
  let message = howOldAreYou "田村ゆかり" 17
  putStrLn message

howOldAreYou name age = nameSan ++ ageSai
 where
  nameSan = name ++ "さん"
  ageSai  = show age ++ "歳"

使うときは、show関数を使ってる時みたいに、letで定数に代入します。
こんな単純な例だとありがたみがわかりにくいですが、制御構文やパターンマッチングを勉強するとありがたさに打ち震えるようになります。

リスト

このあたりで配列について触れましょう。
Haskellでは実際にはあまり「配列」を使いません。
配列みたいな「単方向リスト」と呼ばれるものを使います。
でも、とりあえずは配列だと思っておいても構いません。
あとでググりましょう。

list.hs
main = do
  let ls = [1..4]
  print ls          -- [1,2,3,4]
  let ls2 = [1,3..8]
  print ls2         -- [1,3,5,7]
  let ls3 = ["foo", "bar", "baz"]
  print ls3         -- ["foo", "bar", "baz"]
  print (ls3 !! 1)  -- "bar"

基本的には[]で囲んでコンマで区切るだけです。
数字のリスト用の特殊な記法があって、..を使うと、そのあいだの数字を補ってくれます。

for

Haskellだってfor文を使えます。
ちょっとだけ特殊な記法ですが、まずはこの形で覚えてしまいましょう。
foreach文と言ったほうが分かる方も多いかもしれません。

for.hs
import Control.Monad    -- for文を使うときに必要
import Data.IORef       -- 変数を使うときに必要

main = do
  printList [1..5]    -- 1から5まで標準出力
  s <- getSum [6..10] -- 6から10までの総和を求める
  print s

printList ls = do
  forM_ ls $ \i -> do  -- リスト内の各要素`i`について
    print i

getSum ls = do
  s <- newIORef 0         -- 総和を保存するための変数を初期化
  forM_ ls $ \i -> do      -- リスト内の各要素`i`について
    c <- readIORef s
    writeIORef s (c + i)  -- 総和を更新
  ret <- readIORef s      -- 最終的な総和を取得して
  return ret              -- 返り値にする

printListgetSumfor文を使っています。
残念ながらfor文は純粋でない関数内でしか使えません。
これらのfor文の使い方は、バグの原因になるため、あまりよくない使い方です。
よりよい反復の方法は次回以降に紹介します。

if

if文はよくある形です。
for文は純粋でない関数内でしか使えませんでしたが、if文は純粋な関数の中でも使えます。

if.hs
main = do
  putStrLn "挨拶といえば?: "
  greeting <- getLine
  answerToGreeting greeting

  putStrLn "なんか数字: " 
  num <- getLine
  putStrLn (checkNum num)

-- 純粋でない関数内の`if`
-- `then`, `else`のあとに`do`をつける
answerToGreeting greeting = do
  if greeting == "Hi"
    then do
      putStrLn "You speak English, don't you?"
    else do
      putStrLn "英語でおk"

-- 純粋な関数内の`if`
-- `then`, `else`のあとに`do`をつけない
checkNum num = 
  if num == "0" 
    then "ゼロ" 
    else "非ゼロ"

case

case.hs
main = do
  putStrLn "number: "
  num <- getLine
  printInEnglish num
  putStrLn (numInEnglish num)

printInEnglish num = do
  case num of
    "1" -> do
      putStrLn "one"
    "2" -> do
      putStrLn "two"
    "3" -> do
      putStrLn "three"
    _   -> do
      putStrLn "I don't know"

numInEnglish num =
  case num of
    "1" -> "one"
    "2" -> "two"
    "3" -> "three"
    _   -> "I don't know"

上から順番に見ていって、一番最初にマッチした行のやじるし->以下が評価されます。
_はすべての値にマッチするので、最後の行に書くことで、それまでにマッチしなかった値にマッチさせることができます。
逆に一番上に書いたりすると、1でも2でも常に_のやじるし以降の値が評価されてしまいます。

FizzBuzz

最後にこれまでの総復習を兼ねてFizzBuzzを書いてみましょう。
標準入力で数字nを受け取って、1からnまで

  • 3の倍数のときは"Fizz"
  • 5の倍数のときは"Buzz"
  • 15の倍数の時は"FizzBuzz"

と表示します。

fizzBuzz.hs
import Control.Monad

main = do
  putStrLn "いくつまで?: "
  numStr <- getLine       -- 文字列として数値を受け取る
  let num = read numStr   -- `read`は文字列を数値に変換する関数
  fizzBuzz num

fizzBuzz num = do
  forM_ [1..num] $ \i -> do
    putStrLn (show i ++ ": " ++ toFizzBuzz i)

toFizzBuzz num =
  case mod num 15 of    -- `mod`はあまりをもとめる関数
    0 -> "FizzBuzz"
    3 -> "Fizz"
    5 -> "Buzz"
    6 -> "Fizz"
    9 -> "Fizz"
    10 -> "Buzz"
    12 -> "Fizz"
    _ -> ""

次回

書きました
もうすぐリライトして、よりこわくない内容にします。

宣伝

ARoW


  1. 要出典 

  2. runnable.comが本来Haskell対応していないのに無理やりHaskell対応させてるから