Haskellでいってみよう

日曜プログラマにも満たないレベルでもHaskellで何かソフトウェアを作りたい!

開発環境の整備とプロジェクトディレクトリ

ちゃんとしたプロダクトを作るにあたり、まずは開発環境を整えないといけない。 ソース管理は当然なので、今ならgithubが妥当だろう。早速ユーザ登録して、 新規リポジトリを開始する。この辺の詳しいところは、以下の書籍を参考にした。

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)

コラボレーションするようなモノではないので少々オーバースペックな感じはするが、 自宅でバージョン管理システムを運用するのは大変面倒くさいのでやはりクラウド サービスが楽だ。

新しく、"picfinder"というリポジトリを作ったので、これをローカルにcloneしてくる。

$ git clone https://github.com/eijian/picfinder.git

簡単そうなところで、指定したディレクトリ内に存在するJPEGファイルの中で、同じ 画像であるものを探し出してレポートしてくれるプログラムを作ってみようと思う。 さて、Haskellでビルドやテストを繰り返し行うには、Cabalというのを使うと良さそう。 この辺は以下の書籍を参考にした。

まずは初期化をする必要があるらしい。

$ cabal init
Package name? [default: picfinder] picfinder
Package version? [default: 0.1.0.0] 0.1.0.0
Please choose a license:
  :
   8) BSD3
  :
Your choice? [default: (none)] 8

とりあえずプロダクト名、初期バージョン、そしてライセンスを選ぶ。 Cabal(かHaskell界)のしきたりなのか、バージョンは4つの数字で表す ようにする、と先の[関数プログラミング…]に書いてある。まずは、 0.1.0.0で始めておく。

名前とかを入力した後、今回のプログラムは画像関係なのでカテゴリは Graphicsを、また実行プログラムとしておく。

Project category:
  :
  10) Graphics
  :
Your choice? [default: (none)] 10
What does the package build:
   1) Library
   2) Executable
Your choice? 2

あとはmainが入るソースファイル名やHaskellのバージョンなどを聞かれたが よくわからないのでデフォルトのままで。とりあえず最後まで進んだので良しとする。

Generating LICENSE...
Warning: LICENSE already exists, backing up old version in LICENSE.save0
Generating Setup.hs...
Generating picfinder.cabal...

You may want to edit the .cabal file and add a Description field.

あと、ソースツリーをどうするか考えないといけないがルートにバラバラいろんな ファイルやディレクトリが散らかっているのは嫌いなので、ソースとテストと文書用に 3つだけ作った。トップディレクトリは次のようになった。

.git                      <= gitが勝手に作った?
.gitignore                <= github登録時に指定
LICENSE                   <= githubで登録時に選択すると作られるがCabalが作った方を採用
README.md                 <= githubで登録した時に作られた
Setup.hs                  <= Cabalが生成
doc/
picfinder.cabal           <= Cabalが生成
src/
test/

次回からは少しずつプログラムを作ってみる。

簡易cat (2)

ファイル内容を表示するだけでは寂しいので、コマンドラインに オプションを追加してみる。ややこしいのは大変なので、先頭に 行番号を表示する -n モドキだけ。

-n オプションがある場合は各行の表示をする時に番号を付加する。 行番号はいわゆる「無限リスト」を各行とzipで組にしてみた。

Cat4.hs
-- 
-- Cat4
-- 

module Main (
  main
) where

import System.Environment

main :: IO ()
main = do
  xs <- getArgs
  putFiles $ checkArgs xs

checkArgs :: [String] -> (Bool, [String])
checkArgs [] = (False, [])
checkArgs (x:xs)
  | x == "-n" = (True, xs)
  | otherwise = (False, (x:xs))

putFiles :: (Bool, [String]) -> IO ()
putFiles (_, []) = putStr ""
putFiles (b, (x:xs)) = do
  cs <- readFile x
  putStr (decorate b cs)
  putFiles (b, xs)

decorate :: Bool -> String -> String
decorate False cs = cs
decorate True cs = unlines $ map tr (zip [1..] $ lines cs)

tr :: (Int, String) -> String
tr (n, l) = (show n) ++ "\t" ++ l

単に最初の引数が"-n"かどうかをチェックしているだけなのに 結構邪魔くさいことになっている。 doブロック内でできることできないことについて、下記ではまった。

putFiles (b, (x:xs)) = do
  cs <- readFile x
  putStr (decorate b cs)

putStrのところ、今回は短いのでいいが、一旦変数にバインドしようとして 単に代入文を書いたらコンパイルエラーになった。

  cs' = decorate b cs
  putStr cs'

しょうがないのでputStr一行にまとめてしまったが、あとで試したらletを 使えば大丈夫そう。

  let cs' = decorate b cs
  putStr cs'

一方で where句ではエラーになった。csがスコープにないと怒られたので where句の中に"cs <- readFile x"を書いてもエラーになってしまう。

  putStr cs'
  where
    cs' = decorate b cs

letとwhereの違いを確認しておこう。

次回は使えそうなプログラムを作ってみる。

簡易cat - コマンドライン引数

はじめに、このブログはHaskellの入門記事でも何でもないので、その辺りはご期待には添えないのであらかじめお断りしておくことにする。単に、私がHaskellで何かしらソフトウェアを作るということのモチベーションを維持するためであり、またその過程を記録するものである。記述が大雑把なのもご容赦願いたい。ただせっかく稚拙で恥ずかしいソースを晒すので、願わくば「高位のHaskell使い」の方からHaskellらしからぬところ、より的確な関数/ライブラリなど、改善につながるご助言をいただければ幸いである。

さて、実用的なアプリケーションを作るにはコマンドライン引数を扱えないと話にならない(と考えた)ので、その辺を確認しておく。サンプルとして超簡易的なcatコマンドを作成する。

STEP 1

引数に指定したファイルを単に表示するだけのcatを作ってみる。まずは、最初の引数のファイルだけを表示する仕様とする。二番目以降の引数は無視。ちなみに、これではcatとは名ばかりである…。

Cat1.hs
module Main (
  main
) where

import System.Environment

main :: IO ()
main = do
  (x:xs) <- getArgs
  putFile x

putFile :: String -> IO ()
putFile f = do
  cs <- readFile f
  putStr cs

実行してみる。

$ ghc -o cat1 Cat1.hs
$ ./cat1 Cat1.hs
module Main (
  :
  putStr cs

一応動いた。getArgsはリストを返すので最初の要素だけを取り出してみた。ファイルの読み出しと標準出力への書き出しはとても簡単。readFileで読んでputStrするだけ。ちなみにこの辺のテキスト処理的な簡易コマンドの書き方については、この本を参考にした。関数プログラミングの解説書として云々という評価はあるが、私的にはHaskellで実用アプリを作るという事ではなかなかいいアプローチで説明もわかりやすいと思う。ただしAmazonではもう新品は売ってないのかな?

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

STEP 2

次に複数のファイル名を与えてそれらを順に表示させる事にする。これで一応catを名乗ることができる? 複数のファイル名はリストで渡されるので、リストの各要素に対し同じ処理を実行するには map というのがいいらしい。なので先ほどのプログラムにちょっとmapを付けてみる。

Cat2.hs
module Main (
  main
) where

import System.Environment

main :: IO ()
main = do
  xs <- getArgs
  map putFile xs

putFile :: String -> IO ()
putFile f = do
  cs <- readFile f
  putStr cs

コンパイルすると…

$ ghc -o cat2 Cat2a
  :
Cat2a.hs:14:3:
    Couldn't match type ‘[]’ with ‘IO’
    Expected type: IO ()
      Actual type: [()]
  :

怒られた。確かに map の型は「(a -> b) -> [a] -> [b]」なのでmainの型と合わない。さすがに"純粋な" map をここで使う事に無理があるのか?あきらめて再帰関数に変えてみた。

Cat3.hs
module Main (
  main
) where

import System.Environment

main :: IO ()
main = do
  xs <- getArgs
  putFiles xs

putFiles :: [String] -> IO ()
putFiles [] = do
  putStr ""
putFiles (x:xs) = do
  putFile x
  putFiles xs

putFile :: String -> IO ()
putFile f = do
  cs <- readFile f
  putStr cs

うまく行った!(実行結果は割愛)

ちなみに最初は再帰の底(putFiles [] = do のところ)を書いていなかったので実行時に怒られた。この辺、慣れが必要か。でも 'putStr ""' という処理が適切かどうかはわからない。

続きは次回。

まずは "Hello, World!" から

実用的なプログラムを作ることが目的なので、実行ファイルを
作成できなくては始まらない。Haskellの本だと対話環境を使った
リスト操作や再起などの説明が多いが、そうではなくてまずは
コンパイルしてみる。お決まりの"Hello, World!"でも書いてみる。

(Greeting.hs)

main = putStrLn "Hello, World!"

Rubyほど短くないが、かなり簡潔に書ける方だと思う。Javaだと
いろいろ修飾が大変、というのもHaskellに流れてきた理由の一つ。

コンパイルする。

$ ghc Greeting

これで、"Greeting"という実行ファイルができる。簡単だ。
出力ファイルの名前指定(-o オプション)や最適化(-O, -O2)も
OK。(前は"--make"をつけていたが今は要らないらしい)

http://d.hatena.ne.jp/kazu-yamamoto/20140206/1391666962

$ ghc -o gre -O2 Greeting

大きなプログラムになるとモジュール分割とか出てくるので、
その辺も書いておく。mainの型も。

(Greeting2.hs)

module Main (
  main
) where

main :: IO ()
main = putStrLn "Hello, World!"

最初、モジュール名は何でもいいと思ってJavaみたいにファイル名と
同じにしてたら失敗した。ghcは「Mainモジュールの"main"」がないと
実行ファイルを作ってくれないみたい。最初の例ではモジュール名を
書かなかったので勝手にMainとして処置してくれたらしい。
Mainの後ろの括弧内に外部へ公開したい名前を羅列するそうだ
カプセル化みたいに?)。

最後に、画面から名前を入力させてあいさつさせてみる。

(Greeting3.hs)

module Main (
  main
) where

main :: IO ()
main = do
  putStrLn "What's your name? "
  name <- getLine
  putStrLn ("Hello, " ++ name ++ ". Nice to meet you!")

実行するとこんな感じ。

$ ghc -o gre Greeting3
  :
$ ./gre
What's your name?
Taro                              <= 入力
Hello, Taro. Nice to meet you!

できた。

Haskellコンパイラを入れる

どうやらHaskellコンパイラとしてはGHCがメジャーということらしい。もろもろライブラリも使いたいのでHaskell Platformを入れるのがよいそうで。自宅はMacで、パッケージ管理はHomebrewを使っている。

$ brew install haskell-platform
Warning: haskell-platform-2013.2.0.0 already installed

ああ、前に分けもわからず入れたんだった。ということでHomebrewの更新もやってしまおう。

$ brew update
Cloning into '/usr/local/Library/Taps/homebrew-dupes'...
   :
==> Deleted Formulae
   :
catdoc		    haskell-platform	mpio		    texmacs
   :

あれ?もう一度インストールを試すと

$ brew install haskell-platform
Error: No available formula for haskell-platform
We no longer package haskell-platform. Consider installing ghc
and cabal-install instead:
  brew install ghc cabal-install
    :

どうも"haskell-platform"という名前ではパッケージをインストールできなくなってしまっている。。。
指示通り、ghcとcabal-installを入れてみる。

$ brew install ghc cabal-install
Error: ghc-7.6.3 already installed
To install this version, first `brew unlink ghc'
==> Installing dependencies for cabal-install: gmp, ghc
==> Installing cabal-install dependency: gmp
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/gmp-6.0.0a.
######################################################################## 100.0%
==> Pouring gmp-6.0.0a.mavericks.bottle.tar.gz
🍺  /usr/local/Cellar/gmp/6.0.0a: 15 files, 3.2M
==> Installing cabal-install dependency: ghc
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/ghc-7.8.3.m
######################################################################## 100.0%
==> Pouring ghc-7.8.3.mavericks.bottle.1.tar.gz
🍺  /usr/local/Cellar/ghc/7.8.3: 5742 files, 724M
==> Installing cabal-install
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/cabal-insta
######################################################################## 100.0%
==> Pouring cabal-install-1.20.0.3_1.mavericks.bottle.1.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d
Error: The `brew link` step did not complete successfully
The formula built, but is not symlinked into /usr/local
Could not symlink bin/cabal
Target /usr/local/bin/cabal
is a symlink belonging to haskell-platform. You can unlink it:
  brew unlink haskell-platform

To force the link and overwrite all conflicting files:
  brew link --overwrite cabal-install

To list all files that would be deleted:
  brew link --overwrite --dry-run cabal-install

Possible conflicting files are:
/usr/local/bin/cabal -> /usr/local/Cellar/haskell-platform/2013.2.0.0/bin/cabal
==> Summary
🍺  /usr/local/Cellar/cabal-install/1.20.0.3_1: 5 files, 18M
$ ghc -v
Glasgow Haskell Compiler, Version 7.8.3, stage 2 booted by GHC version 7.8.3
    :

何とか入ったらしい。でも、本当にこの手順で良かったのか?

何をしようか?

このところ流行っていそうな関数プログラミングとやらに興味が湧いてきて雑誌記事やwebを読みまくってる。どうも個人的にはHaskellの言語特性もさることながら簡潔な文法にえらく関心してしまい、何とかHaskellでプログラムを書けるようになれないか、とむずむずしてきた。

というわけで、日曜プログラマと呼ぶのもおこがましいレベルであるけどもまっとうなソフトウェアを作れるよう、ちまちました部品やサンプルを書きなぐりながら少しずつレベルアップを図りたい次第。とにかく書いてみないと上達もなにもないので、始めてみよう。