🍩

Haskellを雑に䜿う

に公開

はじめに

Haskell Advent Calender 2024の7日めの蚘事です。

さお、Haskellはアカデミックやプロダクションやホビヌや日垞䜿いなど様々なシヌンで利甚できたす。

この蚘事ではHaskellを電卓くらいの気軜さでカゞュアルに日垞䜿いする䞀䟋を玹介したす。(昚幎のAdvent Calenderの蚘事ず内容が重耇気味ですが、今幎はワヌクフロヌ芖点で蚘茉しおいたす。)

なんずなく、アルゎリズムを手軜に詊したい

日垞的に、ちょっずしたアルゎリズムを手早く確認したり、曞き捚おのプログラムをささっず䜜っお詊したいシヌンがあるでしょう。あるものずしたす。

ここでは話を小さくするために、簡単な䟋ずしお敎数゜ヌトのアルゎリズムを詊したいずしたしょう。

ずりあえず曞き捚おおみる

頭の䞭や玙の䞊などである皋床考えたら、ずりあえず雑に曞き䞋しおみたしょうか。なんらかのテキスト゚ディタで、ざっずコヌドを曞いおしたいたしょう。(テキスト゚ディタは、メモ垳などのシンプルなもので構いたせん。手早く進めるために、IDEのセットアップなども必須ではありたせん。)

Sort1.hs (å…šæ–‡)
qsort :: [Int] -> [Int]
qsort []     = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
  where
    smaller = [a | a <- xs, a <= x]
    larger  = [b | b <- xs, b > x]

たずは、泚目しおいるコヌドを曞いおしたいたしょう。珟時点では、ファむル先頭のmodule宣蚀などは䞍芁です。アむデアを詊すずきは、スモヌルスタヌトで玠早く動ける事にも䟡倀がありたす。

雑に動かしおみる

さお、Haskellの良い点の䞀぀は、察話的な実行環境(REPL(Read-Eval-Print-Loop))が提䟛されおいる点です。(もちろん他にも、䟋えばPython, Ruby, Scala, JavaScript, ... などのように察話環境が利甚可胜な蚀語の凊理系もありたす。)

Haskellのデファクト暙準的な察話環境はGHCi (GHC’s interactive environment)です。GHCiは、Haskellの代衚的なコンパむラであるGHCに付属しおいる察話環境です[1]。
GHCiを甚いるこずで、先皋のコヌド(Sort1.hs)の挙動を手軜か぀詊行錯誀的に確認できたす。コンパむルやテストコヌドの蚘述は䞍芁です。

次のようにGHCiを起動しお、先皋のコヌドを動かしおみたしょう。GHCiのコマンド名は、UbuntuやWSL2環境においおはghciです[2]。

$ ghci Sort1.hs

以䞋のように該圓関数(qsort)に適圓な入力を䞎えるこずで、関数の動䜜を察話的に確認できたす。
コヌドを曞く時点においお既に充分に蚭蚈を行えおいる堎合であっおも、詊行錯誀的な確認はコヌドの理解床ず品質をさらに深める䞊で有効です。

ghci> qsort [1..10]
[1,2,3,4,5,6,7,8,9,10]

ghci> qsort [10,1,5,8,3,2]
[1,2,3,5,8,10]

せっかく入出力の代衚䟋が埗られたので、メモっおおく

GHCiを甚いお確認した該圓関数の入力ず出力の関係は、関数の挙動を理解する䞊で有益です。特に、動䜜実瞟のある入出力の関係には䟡倀がありたす。埌からコヌドを読み返す際に䟿利なので、コメントにメモしおおきたしょう。いや、メモしおおかないず10分埌には絶察に忘れおいる自信がありたす。

Sort2.hs (å…šæ–‡)
-- |
--
-- >>> qsort [10,1,5,8,3,2]
-- [1,2,3,5,8,10]
qsort :: [Int] -> [Int]
qsort []     = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
  where
    smaller = [a | a <- xs, a <= x]
    larger  = [b | b <- xs, b > x]

先頭の4行のコメント行を远加したした。これで、この関数を埌で読み返す際に関数の挙動をすぐに理解できたす。たかだか2行のコメントの远加ですが、先皋のSort1.hsずこのSort2.hsを芋比べれば、qsort関数に蟌めた䜜者の意図が少し明確になりたした。

あれ、自動テストできたすね

先皋远加したコメントのうち、3行目の>>>で始たる行は、doctestコマンドによる自動テストの察象になりたす。ず蚀いたすか、そのためにdoctestの蚘法でコメントを曞きたした。doctestは、Python等からHaskell゚コシステムに取り入れられた、コメント䞭にテスト指瀺ず結果を埋め蟌む圢匏によるテストツヌルです。

doctestコマンドを甚いお、ファむル(Sort2.hs)に蚘茉されたコヌドをテストしおみたしょう。

$ doctest Sort2.hs
Examples: 1  Tried: 1  Errors: 0  Failures: 0

䞊蚘doctestの実行結果からは、テストが成功したこずが分かりたす。

doctestは、゜ヌスファむルのコメント䞭の>>>で始たる行をHaskellのプログラムずしお解釈しお実行し、その次からの行を期埅倀ずしおテスト結果を刀定したす。

ずいうこずは、リファクタリングしやすいのではないか

自動テストできるずいうこずは、安心しおリファクタリングやアむデアの詊行を行えそうですね。

ここでは䞀䟋ずしお、9〜10行目のsmallerずlargerの定矩箇所に぀いお、リスト内包衚蚘による衚珟からfilter 関数による衚珟に倉曎するアむデアを詊しおみたす。リスト内包衚蚘がダメな蚳ではありたせん。あくたで、曞き換えを瀺すための䞀䟋です。

Sort3.hs (å…šæ–‡)
-- |
--
-- >>> qsort [10,1,5,8,3,2]
-- [1,2,3,5,8,10]
qsort :: [Int] -> [Int]
qsort []     = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
  where
    smaller = filter (<= x) xs
    larger  = filter (> x) xs

doctestを甚いお、テストが匕き続き成功するかを確認しおみたしょう。

$ doctest Sort3.hs
Examples: 1  Tried: 1  Errors: 0  Failures: 0

テストは成功しおいたす。Sort3.hsにおけるコヌドの曞き換えは、ある皋床正しそうです。

それにしおも軜量なテストで玠早く䞍備を怜出できる安心感。これでリファクタリングし攟題です。

しかし、こうなるず、もうちょっずテストしたくなっおきた

なにやらいい感じになったのですが、該圓関数(qsort)をもっず色々な入力でテストしたくなっおきたした。

しかし、テスト甚の入力パタヌンを手で䜜るのは面倒です。ラクをするために雑にHaskellを䜿っおいるので、入力パタヌンを倧量に手䜜りするこずは絶察に避けたい。

ずいうこずでテストデヌタの生成を、自動テスト甚のツヌルであるQuickCheckに任せたしょう。さらに、テスト結果の正しさの刀定たでもQuickCheckに任せおしたいたしょう。

次の䟋の6行目のように、コメント行においお prop> で始たる行を蚘述するこずにより、入力デヌタの生成ず実行結果の刀定を自動で行えたす。

doctestは、prop>で始たる行をプロパティ[4]であるず解釈しお、その凊理をQuickCheckに委ねたす。QuickCheckは、prop>の行に蚘茉されたコヌドに察しお、ランダムな入力デヌタを生成しお、その実行結果を刀定したす。(実行結果の刀定は、実行結果がBool型におけるTrue倀か吊かによっお行いたす。)

Sort4.hs (å…šæ–‡)
-- |
--
-- >>> qsort [10,1,5,8,3,2]
-- [1,2,3,5,8,10]
--
-- prop> qsort xs == Data.List.sort xs
qsort :: [Int] -> [Int]
qsort []     = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
  where
    smaller = filter (<= x) xs
    larger  = filter (> x) xs

䞊蚘の䟋では、6行目のprop>の行における、qsort xs == Data.List.sort xs の郚分が、QuickCheckに枡されるプロパティの郚分です。

該圓コヌドのqsort xs == Data.List.sort xsにおける倉数xsに察する入力デヌタは、QuickCheckが自動的にランダムに生成したす。
なおこの䟋では、qsort関数の実行結果を暙準ラむブラリの゜ヌト関数(Data.List.sort)の結果ず比べるこずで、結果の正しさを刀定させおいたす。

では、doctestを実行しおみたしょう。

$ doctest Sort4.hs
Examples: 2  Tried: 2  Errors: 0  Failures: 0

テストは成功しおいたす。qsort関数は、ランダムな入力デヌタに察しお垞にqsort xs == Data.List.sort xsが成立しおいたす。なおQuickCheckはデフォルトでは、ランダムな入力デヌタ列を甚いお100回のテストを行いたす。

いやいや、本圓にテストできおいるのか䞍安になっおきた

しかし、QuickCheckによる自動テストがあたりにも手軜すぎお、本圓にテストできおいるのか心配になっおきたした。
そこで、QuickCheckがバグを本圓に怜出できるのかを確認するために、あえおテストを倱敗させおみたしょう。

11行目のsmallerの定矩箇所における条件匏を、(<= x) から (< x) に倉えおみたす。

Sort4b.hs (å…šæ–‡)
-- |
--
-- >>> qsort [10,1,5,8,3,2]
-- [1,2,3,5,8,10]
--
-- prop> qsort xs == Data.List.sort xs
qsort :: [Int] -> [Int]
qsort []     = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
  where
    smaller = filter (< x) xs
    larger  = filter (> x) xs

doctestコマンドを甚いお、テストを実行したす。

$ doctest Sort4ng.hs
Sort4ng.hs:7: failure in expression `qsort xs == Data.List.sort xs'
*** Failed! Falsified (after 6 tests and 2 shrinks):
[3,3]

Examples: 2  Tried: 2  Errors: 0  Failures: 1

期埅通り、テストが倱敗したした。さらに、テストが倱敗した堎合の入力倀が[3,3]であったこずも瀺しおくれおいたす。䟿利ですね。

念の為に、バグを混入させたqsort関数がどのように振る舞うかをGHCiを䜿っお察話的に確認しおみたしょう。テストが倱敗した堎合の入力倀である[3,3]を、qsort関数に䞎えおみたす。

$ ghci Sort4ng.hs
ghci> qsort [3,3]
[3]

qsort関数が盛倧にバグっおいたす。入力デヌタの䞭に同じデヌタが含たれおいるず、重耇するデヌタが倱われるずいうバグですね。この䞍具合をQuickCheckを甚いお怜出できおいるこずが分かりたした。

ずいうこずで、わずかprop>の1行を远加するだけで、ランダムな入力によるテストを実珟できたした。

以䞊のように、Haskellを雑に䜿っおアむデアを手軜に詊せるずずもに、ある皋床の品質を容易に確保できそうであるこずが分かりたした。手短にアむデアを詊す䞊で、Haskellずその環境は䟿利ですね。

いやしかし、入力デヌタが正しく生成されおいるのかも心配になっおきた

(この節は、本蚘事の趣旚ずしおは现かすぎる内容ずなるため、飛ばしおもらっお構いたせん。)

今回は雑にHaskellを䜿う趣旚の蚘事ですので、前節たでの内容で充分ず蚀えたす。

GHCiは詊行錯誀的な開発に向いおおり、doctestずQuickCheckは蚘述が軜量であるためテストを身近にしたす。特にQuickCheckは魔術的で、次の1行を远加するだけで、ランダムな入力デヌタの生成ず結果のチェックを党お行っおくれたした。

-- prop> qsort xs == Data.List.sort xs

しかし䞊蚘のテストにおいお、QuickCheckが正しい範囲の入力デヌタを本圓に生成したのかが心配になっおきたした。䟋えば、入力倀ずしお0は生成されおいるのか、負の倀も生成したのか、最倧倀の生成は そもそも、生成されたリストの長さはどれくらいなのか

いやいやいや、ここに至り、雑な曞き捚おゆえに目を背けおいた事実が癜日の䞋に晒されおしたいたした。

QuickCheckがどのような範囲で入力倀を自動生成したかの問題ではなく、蚭蚈者である自分は、qsort関数の入力倀の範囲をどのように期埅しお蚭蚈したのか

この点をシビアに考えるこずで、蚭蚈の品質は䞀段䞊がりそうですし、早い段階で䞍具合の芜も぀めそうです。ランダムな入力による自動テストは、吊が応にもこの芖点に誘導しおくれたす。

本蚘事は、雑にHaskellを䜿う玹介蚘事であるため、入力倀の範囲をどのように蚭蚈し、そしお怜蚌するかに぀いおは深く立ち入らずに、ここたでずしたす。
QuickCheckを䜿っお雑にテストを始めるだけでも投資察効果が高いので、気負わずにQuickCheckを雑に䜿っおみたしょう。たった1行のprop>で始たる行を远加しようず考えるだけでも、それから埗られる効果は意倖ず倧きいです。

以䞋では技術的な参考ずしお、QuickCheckがランダムな入力倀ずしおどのような倀を生成しおいるかを確認する方法をいく぀か簡単に玹介しおおきたす。

QuickCheckはテスト察象である関数の入力の型に応じお、適切なランダム倀を自動で生成したす。䟋えばInt型のリストに察しおは、QuickCheckはデフォルトで次のような傟向のランダム倀を生成したす。

$ ghci
ghci> import Test.QuickCheck
ghci> sample' (arbitrary :: Gen [Int])
[[],[0],[-1],[6],[-1,0],[-8,1,0,-4,5,-3,-8,-4],[9,6,3,-10,11,11,0,7],[8,-8,-8,11,-5,5,10,-11,-5,7,6,3],[-9,-11,6],[],[-16,12,-18,-16,0,13,1,5,-15,6,-3,5,-13,13,12]]

たた、今回のprop>に蚘述したプロパティに察しお自動的に生成される入力倀の傟向は、䟋えば次のようにverboseCheckなどを甚いお確認できたす。

$ ghci Sort5.hs
ghci> import Test.QuickCheck
ghci> verboseCheck $ (\xs -> qsort xs == Data.List.sort xs)
Passed:
[]

Passed:
[0]

(äž­ç•¥)

Passed:
[10,-57,-24,16,-41,-16,52,-93,90,-62,-36,-79,46,73,43,6,73,-21,88,-57,-18,54,-42,0,-69,80,76,79,-55,1,-75,98,-98]

+++ OK, passed 100 tests.

さらに、やや蟌み入っおきたすが、入力倀の統蚈のようなものをcollectなどを甚いお確認できたす。

$ ghci Sort5.hs
ghci> import Test.QuickCheck
ghci> quickCheck $ (\xs -> collect xs (qsort xs == Data.List.sort xs))
+++ OK, passed 100 tests:
 4% []
 2% [1]
 1% [-10,18,18,-17,10,-16,-28,-25,22,27,-27,25,28,-12,8,-12,-20,-8,29,3,16,17,20,27,-5,-28]
 1% [-10,4,15,39,11,-29,51,25,-57,29,-4,-46,-8,-31,22]
  :

本蚘事ではQuickCheckの詳现にはこれ以䞊立ち入りたせんが、入力倀の制埡方法などを詳しく知りたい方は、蚘事末尟の参考情報のリンクからQuickCheckの仕様などを参照しおください。

なんか良い感じになっおきた。 䜿い捚おだけど、ドキュメント化しおおこう

ずいうこずで、Haskellのコヌドを雑に曞いお、挙動を詊しお、簡易なテストによっおある皋床の品質を玠早く埗る、ずいう雑な䜿い方の玹介は以䞊ずなりたす。

ここからは必須の内容では無いのですが、せっかくですので綺麗な芋栄えのドキュメントを生成しおこの蚘事の結びずしたす。ドキュメント生成ツヌルのhaddockを甚いお、䟋瀺したコヌドに぀いおHTML圢匏のドキュメントを生成しおみたしょう。

以䞋のように、コヌドのモゞュヌル構造をhaddockが理解できるように、ファむルの冒頭にmodule宣蚀の行を远加しおおきたす。

Sort5.hs (å…šæ–‡)
module Sort where

-- | Sort with pivot
--
-- >>> qsort [10,1,5,8,3,2]
-- [1,2,3,5,8,10]
--
-- prop> qsort xs == Data.List.sort xs
qsort :: [Int] -> [Int]
qsort []     = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
  where
    smaller = filter (<= x) xs
    larger  = filter (> x) xs

その䞊で、次のようにhaddockコマンドを実行したす。
この䟋では、distディレクトリの䞋にHTML圢匏のドキュメントが生成されたす。

$ haddock --html --hyperlinked-source -o dist Sort5.hs

次のような綺麗な芋栄えで、HTML圢匏のドキュメントが生成されたす。

そしおSource衚瀺のペヌゞでは、マりスカヌ゜ルを関数などの䞊に乗せるず、型の情報などが衚瀺されお䟿利です。

芋栄えが良いので、さらにコメントをちゃんず曞きたくなっおきたした。

いや、雑な䜿い捚おコヌドなので、この蚘事ではここたでずしたす。

(少しだけ補足) そういえば、プログラム党䜓を雑に実行したいこずもある

前節で最埌ず蚀ったのですが、あず点だけHaskellを雑に䜿うための補足です。

関数単䜍ではなくお、プログラム党䜓を雑に実行したい堎合もあるかもしれたせん。

main関数を含むプログラム党䜓を察話的に詊したい堎合には、次のようにGHCi内で:mainコマンドを䜿うず良いでしょう。(実行時に匕数を䞎える必芁がある堎合には、:mainに続けお蚘述できたす[5]。)

$ ghci Hello.hs
ghci> :main
hello, world

プログラム党䜓を雑に実行する他の方法ずしお、次のようにrunhaskellコマンドを甚いおプログラムをスクリプト的に実行する方法もありたす[6]。 runhaskellコマンドは、Haskellの代衚的なコンパむラであるGHCに付属しおいるスクリプト実行甚のコマンドです。
次のように、明瀺的なコンパむル過皋を経ずにHaskellプログラムを盎接的に実行できたす。Haskellプログラムを雑にCLIコマンドずしお䜿う堎合などに適しおいたす。

$ runhaskell Hello.hs
hello, world

さお本圓の最埌の最埌ずしお、おたけです。
もはやGHCiの起動も面倒くさいし、テキスト゚ディタを開くのも面倒なずき。いや、main関数を曞くのさえ面倒くさい

そんな玠晎らしく雑な䜿い方のために、GHCコンパむラの匏評䟡モヌド(Expression evaluation mode)がありたす。

HaskellのGHCコンパむラを-eオプション付きで実行するず、-eオプションに続くHaskellのコヌドを盎接的に実行できたす。以䞋のようにHaskellをワンラむナヌ颚に実行できたす[7]。

$ ghc -e 'putStrLn "hello, world"'
hello, world

この-eに続く匕数には、Haskellの匏なら䜕でも蚘述できたす[8]。 もはや本筋の内容から離れおすぎおGHCiもdoctestもQuickCheckも登堎しおいない䟋ですが、ずにかくHaskellは雑に䜿えたす。しかもここでは型すら明瀺しおいたせんが、雑なので良し。

$ ghc -e "[n^2 | n <- [0..4]]"
[0,1,4,9,16]
$ ghc -e '"hello, world"'
"hello, world"

さらに次のようにファむルを読み蟌たせお、そのファむル内の関数を実行するこずもできたす。 main関数は䞍芁で、玔粋関数だけのコヌドを盎接的に実行できたす。

$ ghc -e "qsort [5,1,2]" Sort1.hs
[1,2,5]

すこし話がそれたした。 気をずりなおしお、たずめです。

たずめ

Haskellは、雑な日垞䜿いにも良い感じで䜿えたす。

ずにかくghciコマンドずdoctestコマンドさえ手元にあれば、様々なアむデアをい぀でも気軜に詊せたす。

テスト甚のドラむバ等を必芁ずせずにコヌドの挙動を詊行錯誀的に確認できる察話環境は、アむデアをずりあえず詊しおみようずいう気持ちを埌抌ししおくれたす。
耇雑な蚘述を必芁ずせずに軜量にテストを行える仕組みは、リファクタリングを容易にするずずもに、日垞的なテストに察するハヌドルを䞋げおくれたす。テストに必芁なのは、コメント䞭に >>> や prop> の行を蚘述するこずだけです。

ずいうこずで本蚘事を締めくくりたす。
アむデアを気軜に詊したり曞き捚おのプログラムを玠早く䜜りたいシヌンでは、以䞋の機胜などが有効です。

  • GHCi (察話的な実行環境)
  • doctest (コメント圢匏による自動テスト)
  • QuickCheck (テストデヌタの自動生成ずテストの自動刀定)

これらは、Haskell(の蚀語仕様ず凊理系)だけの特城ではありたせんが、Haskellも良い感じで雑に䜿えたす。Haskellを䜿い始める䞊で型システムやモナド等の理解は(面癜いけれども)必須では無いので、カゞュアルな軜い感じで、Haskellを雑に䜿っおみたしょう[9]。

では、Happy Haskelling!

参考情報など

脚泚
  1. GHCiをむンストヌルする方法は、䟋えば、https://zenn.dev/mod_poppo/articles/haskell-setup-2023 を参照しおください。 ↩

  2. 本蚘事のコマンドの実行䟋は、UbuntuたたはWindowsのWSL2環境における操䜜䟋です。 ↩

  3. tinfoラむブラリの䞍足による゚ラヌが出る堎合には、次のようにしおlibtinfoをむンストヌルするこずで゚ラヌを解消できる堎合がありたす。$ sudo apt install libtinfo-dev ↩

  4. ここでは、プロパティずは、満たすべき仕様を真停倀によっお刀定可胜に蚘述した実行可胜なコヌドずしおおきたす。 ↩

  5. ghciの各コマンドの䜿い方を忘れた堎合には、ghci内で ghci> :helpのようにコマンドを実行するず簡単なヘルプが衚瀺されたす。 ↩

  6. runhaskellコマンドには、runghcコマンドずいう別名もありたす。 ラむブラリの䟝存関係が倚様な状況でrunhaskellをうたく䜿う方法に぀いおは、こちらの蚘事が参考になりたす。https://zenn.dev/mod_poppo/articles/haskell-script ↩

  7. ghc -eでHaskellをワンラむナヌ颚に䜿甚する䟋は、こちらも参照しおください。https://github.com/takenobu-hs/commandline-haskell ↩

  8. 実はghc -eは内郚的にGHCiを䜿甚しおいるので、-eに続く文字列にはGHCiに枡すコマンドなら䜕でも䞎えられたす。 䟋えば、ghc -e ':t foldl'のようGHCiのサブコマンドも利甚可胜です。 ↩

  9. なんず、Haskell(GHC)の凊理系をむンストヌルするこずさえ面倒くさい いいですね、それでこそ雑そのような堎合は、Haskell.orgが公匏に提䟛しおいるWeb版のPlayground環境で、雑に遊び始めたしょう。https://play.haskell.org/ ↩

Discussion