Twitterを巡回していてわかったHaskell初心者が躓きやすいポイント8つ

  • 469
    Like
  • 2
    Comment

最近の趣味は「Haskellはいいぞ」と呟くかTwitter Search: Haskellを巡回して :heart: を押して回ることです :cop:
毎日巡回しているとHaskellに入門しようとするも細かいところに引っかかって前に進めないでいる人をちらほら見かけます。今回はそんな見回りの知見を活かしてHaskell初心者が躓きやすいポイントをまとめてみたいと思います。

1. 入門書は何がいいの?

それはもうすごいH本一択でしょう!…と言いたいところですが時々不満の声を聞くこともあります。確かにすごいH本こと『すごいHaskellたのしく学ぼう!』世界一わかりやすいHaskell入門書であることは間違いないと思いますが、逆に内容が平易すぎるため記述が冗長だと感じたり読み終わっても何か自分で作れるようになった気がしなかったりするかもしれません。なので僕は「プログラミングも初心者でHaskellから入門してみたい」という人にはすごいH本をおすすめしています :elephant: が、そうではなく「普段書いてるプログラミング言語はあるけど最近関数型言語とかよく聞くしHaskellにも入門してみたい」というような人には『関数プログラミング実践入門』をおすすめしています。関数プログラミングと広く銘打ってありますが中身はHaskellを中心に書かれているので安心して下さい :raised_hand: 。(ここに書いてるのはあくまで個人的な見解なので実際に本を購入するときは自己責任でお願いします :bow:

本なんて買うのめんどくさいからWebの記事で手っ取り早く入門したい :exclamation:
という人のためにいくつかオススメの入門記事を列挙したいと思います。

2. 開発環境って何を入れたらいい?

Haskellの代表的なコンパイラはGHCです。広く使われているパッケージマネージャはCabalです。そしてGHCのバージョン管理と各バージョンで動作するパッケージだけを集めて管理してくれる大変優秀なツールがStackです :exclamation: なので一番最初はStackを入れましょう。大事なことなのでもう一度いいます、Stackを入れましょう。時々GHCとCabalを直接インストールしていたりHaskell Platformの方をインストールしている人を見かけますが理由は色々あってやっぱりオススメなのはStackです。それなのになんでStackを入れるように書いてある本や記事が少ないのかというとStackは最近(確か2015年6月頃)出てきた新しいツールだからです。新しいツールですがあまりの便利さのために今やStackを使わないHaskellライフが考えられないほどです。

Mac OS Xでbrewを使ってる場合、Stackのインストールは

$ brew install haskell-stack

で終わりです。無事インストールが終了したら

$ stack setup

を実行しましょう。これは最初の一回だけで十分です。このコマンドでGHCやCabalのインストールが行われます。めでたくHaskellの開発環境の完成です :tada: 。その他のOSの場合はこちらを参照してください。

実はStackでGHCを入れた場合ghcコマンドは直接は入らずstackのサブコマンドとして実行する必要があります。

$ stack ghc

そのためghcghciを気軽に利用するため~/.bashrcなどに以下のエイリアスを追加しておくのがオススメです

alias ghc='stack ghc --'
alias ghci='stack ghci --'
alias runhaskell='stack runhaskell --'

3. 変数に再代入できなくてもプログラムが書けるの?

書けます

4. for文なくてもプログラムが書けるの?

書けます
 
 

もちろん書けますよ!!!
 
変数が全て不変(イミュータブル(再代入できない))であっても for文もwhile文も無くても不自由なくプログラムを書くことが出来ます :exclamation: このような印象を持もたれているのはHaskell製のソフトウェアをそんなに知らないことが原因だと思うのでいくつか紹介していきたいと思います。

  • lukasmartinelli/hwk - Haskellで書かれたawkのようなツールです。Haskellの文法が使えるので慣れていればこっちの方が記述量が少なくなることも多く重宝しています!
  • ShellCheck - その名の通りシェルスクリプトが正しいかどうかチェックしてくれるツールです!文法的に間違ってるところやもっとこうした方がいいというところを指摘してくれます。手書きのシェルスクリプトも事前にこういたツールにかけると安心して動かせますね!ブラウザ上ですぐに試すことが出来るのでぜひ遊んでみてください。
  • Pandoc - Pandocは様々な形式のドキュメントを別の形式のドキュメントに変換してくれるツールです。どれだけの種類のドキュメントに対応しているかは公式ページを見てみて下さい。トップページの下部にある図は圧巻です!
  • Monadius - グラディウスというゲームをHaskellで実装したものです!状態とループの塊であるゲームだってHaskellで書けちゃいます。公式サイトは日本語で書かれておりHaskellでゲームを作る時の知見も書かれているのでぜひ一読をオススメします!
  • facebook/Haxl - Facebookが開発しているリモートにあるデータへのアクセスを抽象化するライブラリです。N+1問題を解決してくれたり別のリソースには平行してアクセスしてくたりと便利なライブラリです
  • BlockApps STRATO - Haskellで書かれたEthereumベースのブロックチェーンです。Microsoft Azureで提供されていてすぐにクラウド上で試すことができます。世界の42の銀行が参加するR3Cevのブロックチェーン・コンソーシアムにも採用されていてこれからの金融市場を支える基盤技術になっていくでしょう

だから最初は戸惑っても挫けずに勉強してみてくださいね :smile:

5. 複数種類の値を返したい

ある時は文字列をまたある時は数値を返す関数を作りたい。でもHaskellは型が決まっちゃってるから書けない…
書けないなんてことはない!そういう時は Either String Int が使えます

safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "integer division by zero"
safeDiv n m = Right (n `div` m)

この様に文字列も数値も返すことが出来ます。さらに使うときは

main :: IO ()
main = do
    case safeDiv n m of
        Left error -> putStrLn error
        Right q    -> putStrLn $ "Result: " ++ show q

この様に型を意識して取り出せるので型安全なプログラムを書くことが出来ます。例の通りEitherは例外処理に用いられることが多いです。

今度は三種類以上の値を返したい場合を考えましょう。ここまで来たらそもそも設計がおかしいんじゃないかと疑いたくなりますがとにかくやってみましょう。Eitherを組み合わせてEither String (Either Bool Int) のようにすることも出来ますが、この場合は自分でデータ型を定義するのが得策でしょう。

data MyData = VBool Bool
            | VInt  Int
            | VStr  String

こんな感じ。関数は

myFunc :: String -> MyData
myFunc "true"  = VBool True
myFunc "false" = VBool False
myFunc "100"   = VInt 100
myFunc value   = VString value

こんな感じで定義します。使うときは

main :: IO ()
main = do
    case myFunc string of
        VBool b -> putStrLn $ "Bool: " ++ show b
        VInt  i -> putStrLn $ "Int: " ++ show i
        VStr  s -> putStrLn $ "String: " ++ s

こんな感じです。このような関数を書くことはあまりないと思うのですが、どうしても書きたくなった時はいずれ成長してから書き直しに戻ってくるところと覚えておいて一旦はここにあるような方法で書いておきましょう :pray:

6. 乱数の作り方がわからない

Haskellの乱数について調べるとStdGenとか出てきてナンダカヨクワカラナイってなると思います。ですが単純に乱数を生成するだけならIOモナドを使うのが手っ取り早いです!

import System.Random

main :: IO ()
main = do
    r <- randomIO :: IO Double
    print r

-- $ runhaskell Main.hs
-- 5.571578485008155e-2

簡単でしょう?ランダムな整数がほしい時は

import System.Random

main :: IO ()
main = do
    r <- randomIO :: IO Int
    print r

-- $ runhaskell Main.hs
-- -7817900208843160045

この様に型注釈を変えるだけでOK :ok_woman:
範囲を限定して生成したい時は

import System.Random

main :: IO ()
main = do
    r <- randomRIO (1,100) :: IO Int
    print r

-- $ runhaskell Main.hs
-- 46

randomRIOを使えば大丈夫です。この例では1から100までの間のランダムな整数を生成しています。
試しにモンテカルロ法で円周率を求めてみましょう!

import System.Random

-- 点を生成して四半円の中に入っている個数を数える関数
monte :: Int -> Double -> IO Double
monte 0 inside = pure inside
monte limit inside = do
    x <- randomRIO (0,1) :: IO Double
    y <- randomRIO (0,1) :: IO Double
    -- 四半円の中に入っているか?
    if x^2 + y^2 < 1
        then monte (limit-1) (inside+1)
        else monte (limit-1) inside

main :: IO ()
main = do
    inside <- monte 10000 0
    print $ 4 * inside / 10000

-- $ runhaskell Main.hs
-- 3.1384

最初のうちは乱数が欲しくなったらここにあるコードをコピペして使って下さい :ok_hand:

import System.Random

main :: IO ()
main = do
    -- 基本的な乱数生成
    r0 <- randomIO :: IO Int
    r1 <- randomIO :: IO Double
    r2 <- randomIO :: IO Bool
    print (r0, r1, r2)
    -- (6129061023494953312,0.35663084737564565,True)

    -- 範囲の決められた乱数生成
    r3 <- randomRIO (1,100) :: IO Int
    r4 <- randomRIO (0,1) :: IO Double
    print (r3, r4)
    -- (88,0.40205531778476833)

    -- ランダムな文字列生成
    let randomSamples :: [a] -> Int -> IO [a]
        randomSamples xs n = map (xs!!) <$> (sequence . replicate n $ randomRIO (0, length xs - 1))
    r5 <- randomSamples ['a'..'z'] 10
    r6 <- randomSamples (['a'..'z']++['A'..'Z']) 10
    r7 <- randomSamples (['a'..'z']++['A'..'Z']++['0'..'9']) 10
    print (r5, r6, r7)
    -- ("zpnmprtjoh","AVZIebceLK","cjFym4gmjR")

乱数に関して追記しました(20160326)

7. モナドは何だ

オブジェクト指向のプログラミング言語を初めて学んだ時、クラスとかオブジェクトといった概念に最初は慣れなかった記憶はありませんか?Haskellの場合はモナドで同じ現象が起こるのだと思います。モナドは新しい概念でとっつきにくいとは思いますが使えるようになるととても便利なものですよ!
一度別のパラダイムに慣れてしまった人にはどうもこのモナドが鬼門になるようで世の中には大量のモナドチュートリアルが出回っています。ここでは日本語で書かれていてわかりやすいモナドに関する記事を厳選していくつか紹介します。

Haskellでは入出力を伴うような"時と場合によって結果が変わる関数"はIOモナドを使って実現されています。なので"Hello World"を表示するプログラムですらモナドが現れるのでモナドから逃れることは出来ません。ですが一度使えるようになってしまえばモナドは非常に強力で便利な道具です。モナドを身につける一番効率のいい方法はモナドを使ってプログラムを書くことです。

main :: IO ()
main = do
    putStr "あなたの年齢を3で割った余りを教えて下さい: "
    m3 <- read <$> getLine :: IO Int

    putStr "あなたの年齢を5で割った余りを教えて下さい: "
    m5 <- read <$> getLine :: IO Int

    putStr "あなたの年齢を7で割った余りを教えて下さい: "
    m7 <- read <$> getLine :: IO Int

    let age = (m3 * 70 + m5 * 21 + m7 * 15) `mod` 105
    putStrLn $ "あなたの年齢は" ++ show age ++ "です!"

これは百五減算で年齢を当てるゲームです。最初のうちはこのような簡単なプログラムを書きまくってモナドやdo構文に慣れることが肝心です!

ところで「モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?」という有名なフレーズがあります。実はモナドはHaskellだけの概念ではなく数学の一分野である圏論に出てくる概念です。Haskellのモナドを調べていたらいつの間にか数学の話が出てきてわけがわからなくなるというようなことはよくあります。しかしHaskellのプログラムを書くだけであればモナドの数学的な背景を知る必要はありません。使い方さえわかってしまえば何も問題ありません。ただモナドは圏論の中でちゃんとした定義が与えられているということは重要です。もしモナドに対して勝手な解釈から議論が始まったとしても定義に戻って考えることで何が正しいのかを見失わずに済むでしょう。

8. 何か作りたいけどネタがない…

新しい言語を学ぶ時はとりあえず自分でなにか作ってみるのは大事ですね :exclamation:
まずは自分が作りたいと思えるものに出会うことが大切だと思うのでHaskellの素敵なライブラリを紹介していくことにします :smile:

簡単なWebサービスのアプリケーションサーバーをHaskellで書いてみるというのはどうでしょう?HaskellのWAFとして有名なのはYesodですが最初からこれに取り組むと泥沼にハマるので僕はオススメしません :no_good:
HaskellにはWAIというWebアプリのインターフェースがあります。PythonにおけるWSGIやRubyにおけるRackのようなものです。WAIに適合する標準的なサーバーであるwarpと組み合わせれば簡単なWebアプリを書くことが出来るので最初はここからはじめてみるといいと思います
stackを使ってhaskellで最小のWeb Applicationしてみる

WAI/Warpだけだとシンプル過ぎるという場合はSpockというWAFを使うのがオススメです。Spockはルーティングやセッション管理などWebアプリケーションを作る上で必要な機能を提供してくれます。
出典: Spock Tutorial

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Data.Monoid
import Web.Spock.Safe

main :: IO ()
main =
    runSpock 8080 $ spockT id $
    do get root $
           text "Hello World!"
       get ("hello" <//> var) $ \name ->
           text ("Hello " <> name <> "!")

BitTorrentのクライアントを一から実装してみるのはどうでしょう?以下の記事ではBencodeをパースするところから実装するのですが易しく書かれているのでとても勉強になると思います!
Creating a BitTorrent client in Haskell #1
Creating a BitTorrent client in Haskell #2
Creating a BitTorrent client in Haskell #3

glossを使うと図形の表示やユーザーからの入力を簡単に扱うことが出来ます。例えばglossでマンデルブロ集合を描画してみたものがあります
出典: Mandelbrot shift

Chartを使えば簡単にグラフを書くことが出来ます。バックエンドを入れ替えるだけで同じコードを使って画像ファイルに吐き出したりウィンドウに表示したり出来るのが特徴です!
出典: timbod7/haskell-chart wiki

SpreadsheetのUIでインタラクティブなGUIアプリを作ることが出来るtyped-spreadsheetというライブラリがあります。
Haskell-native spreadsheets
diagramsと連携してパラメータをいじるとすぐに図が変化するような環境も作れるのでかなり遊べると思います!
出典: Interactive and composable charts

OpenGLを使ってゲームを作ってみるというのはどうでしょう?
OpenGL Tutorial 1
OpenGL Tutorial 2
文献は多くはないですがかなりいい練習になると思いますよ!

まとめ

正直に言うとHaskellはもう10年以上もあるにかかわらずネット上の文献も少なく独学で習得するのは決して簡単ではない言語だと思います。特に日本語の記事となると極端に少ないので初心者の人にはかなり敷居が高く感じられるのではないでしょうか。しかし並行並列プログラミングが当たり前になり大規模な開発に耐えうる言語が求められるにつれHaskellの重要性はますます増してくると思います。そんな中Haskellを学ぼうとする人たちの転ばぬ先の杖となるようこの記事が少しでもお役に立てればいいなと思います。


この記事は第30回Haskellもくもく会@朝日ネットのもくもく時間を利用して書かれました。この勉強会は普段仕事でHaskellを書いている人からHaskellを始めようと思ってる初心者まで誰でも参加できるアットホームで明るい勉強会です。懇親会も大盛り上がりなので興味のある型はぜひ参加してみてくださいー :exclamation:


追記: 20160326

紹介したrandomは簡単でわかりやすいのですが現在はあまりオススメされてないようです。(参考: Haskellの乱数事情)
なので慣れてきたら別のライブラリを使ったほうが良いでしょう。
本文中に挙げた例をmwc-randomで書きなおしたものを載せておきます。

import System.Random.MWC

main :: IO ()
main = withSystemRandom . asGenIO $ \gen -> do
    -- 基本的な乱数生成
    r0 <- uniform gen :: IO Int
    r1 <- uniform gen :: IO Double
    r2 <- uniform gen :: IO Bool
    print (r0, r1, r2)
    -- (6129061023494953312,0.35663084737564565,True)

    -- 範囲の決められた乱数生成
    r3 <- uniformR (1,100) gen :: IO Int
    r4 <- uniformR (0,1)  gen :: IO Double
    print (r3, r4)
    -- (88,0.40205531778476833)

    -- ランダムな文字列生成
    let randomSamples :: [a] -> Int -> IO [a]
        randomSamples xs n = map (xs!!) <$> (sequence . replicate n $ uniformR (0, length xs - 1) gen)
    r5 <- randomSamples ['a'..'z'] 10
    r6 <- randomSamples (['a'..'z']++['A'..'Z']) 10
    r7 <- randomSamples (['a'..'z']++['A'..'Z']++['0'..'9']) 10
    print (r5, r6, r7)
    -- ("zpnmprtjoh","AVZIebceLK","cjFym4gmjR")