練習問題(つづき)

「入門Haskell」のp.31から二つ目。

② 単語のカウント方法として,`Theory of Everything’のようにバッククォートとシングルクォートで囲まれたパートを,1つの単語として表現するように wordsCount を拡張しなさい。

オリジナルの wordsCount。

wordsCount str = outWords str
    where outWords [] = 0
          outWords (c:cs)
              | isAlphaNum c = 1 + inWords cs
              | otherwise = outWords cs
          inWords [] = 0
          inWords (c:cs)
              | isAlphaNum c = inWords cs
              | otherwise = outWords cs

拡張版。

wordsCount str = outWords str
    where outWords [] = 0
          outWords (c:cs)
              | c == '`' = 1 + inQuote cs
              | isAlphaNum c = 1 + inWords cs
              | otherwise = outWords cs
          inWords [] = 0
          inWords (c:cs)
              | c == '`' = 1 + inQuote cs
              | isAlphaNum c = inWords cs
              | otherwise = outWords cs
          inQuote [] = 0
          inQuote (c:cs)
              | c == '\'' = outWords cs
              | otherwise = inQuote cs

結果。

*Main> wordsCount "`Theory of Everything' by Greg Egan"
4

練習問題

「入門Haskell」のp.31から。
まずは一つ目。linesCountの拡張

① linesCountでは,空白行が続く場合にどんどんカウントしてきます。これを空白行(何もない行)を無視するように拡張しなさい。

オリジナルのlinesCount。

linesCount [] = 0
linesCount ('\n':cs) = 1 + linesCount cs
linesCount (c:cs) = linesCount cs

結果。1st line,2nd line … は空行を無視した行番号で,1st line と2nd lineの間に空行がある。

*Main> linesCount "1st line\n\n2nd line\n3rd line\n"
4

で,拡張版。

linesCount lines = atHead lines
    where atHead []        = 0
          atHead ('\n':cs) = atHead cs
          atHead (c:cs)    = 1 + inLine cs
          inLine []        = 0
          inLine ('\n':cs) = atHead cs
          inLine (c:cs)    = inLine cs

結果。

*Main> linesCount "1st line\n\n2nd line\n3rd line\n"
3

偶数のリストいろいろ

定義。

even1 = [2,4..]
even2 = filter (\x -> x `mod` 2 == 0) [1..]
even3 = filter (\x -> x `mod` 2 /= 1) [1..]
even4 = zipWith (+) [1..] [1..]
even5 = map (\x -> x * 2) [1..]
even6 = 2:(map (2+) even6)
even7 = map (\x -> sum $ replicate x 2) [1..]
even8 = scanl (\x y -> x + y) 2 $ repeat 2
even9 = scanl1 (\x y -> x + y) $ repeat 2
even10 = map (\x -> sum $ take x $ repeat 2) [1..]
even11 = map (\x -> length $ replicate (x*2) "Haskell") [1..]
even12 = iterate (2+) 2

結果。

*Main> take 20 $ even1
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even2
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even3
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even4
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even5
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even6
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even7
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even8
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even9
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even10
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even11
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]
*Main> take 20 $ even12
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40]

リストを操作する関数(その3)

foldl と foldr はちょっとわかりにくい。なのでこんなのを書いてみた。
括弧内の数字は引数を表している。0 が初期値で 1, 2, 3… がリストの各要素。内側の括弧から順に評価される。
foldl はこう。

*Main> foldl (\x y -> "(" ++ x ++ "," ++ y ++ ")" ) "0" ["1","2","3","4","5"]
"(((((0,1),2),3),4),5)"

で foldr はこう。

*Main> foldr (\x y -> "(" ++ x ++ "," ++ y ++ ")" ) "0" ["1","2","3","4","5"]
"(1,(2,(3,(4,(5,0)))))"

何となくわかりやすい……?

ガード節

パターンマッチングの後,引数の具体的な値を条件に分岐したい場合には次のように書く。
| と = の間の分岐条件の部分をガード節という。
条件は上から順にチェックされる,でいいのかな。otherwise は「その他の場合」。

swapCase は文字列の中の大文字と小文字を入れ替える。アルファベットでないならそのまま。

import Data.Char

swapCase []     = []
swapCase (c:cs)
    | isUpper c = (toLower c):(swapCase cs)
    | isLower c = (toUpper c):(swapCase cs)
    | otherwise = c:(swapCase cs)
*Main> swapCase "Haskell"
"hASKELL"
*Main> swapCase "03^-c,aa"
"03^-C,AA"

局所的な定義と where 節

ある関数の内部で別の関数が評価されるとき,もしその内部の関数がプログラムのほかのどの部分でも評価されないなら--言い換えればその場でしか使われないなら,その関数を局所的な定義にすることができる。局所的に定義された関数はプログラムのほかの部分からは見えなくなる。
局所的な定義はインデントを下げて where と書いたあとに続けて通常の関数定義と同じように書く。
where 以降を where 節という。

というわけで Capitalize をこうしてみた(関数名は先頭に大文字を使えないから capitalize)。

import Data.Char

capitalize []     = []
capitalize (c:cs) = (toUpper c):(lower cs)
    where lower []     = []
          lower (c:cs) = (toLower c):(lower cs)

先頭の文字だけ大文字にして,2文字目以降は局所的に定義された lower を適用している。where 以降が lower の定義。

実行結果。

*Main> capitalize "haskell"
"Haskell"

追記:
map を使えば lower なんて関数を定義しなくてもすむ。

capitalize []     = []
capitalize (c:cs) = (toUpper c):(map toLower cs)
*Main> capitalize "haskell"
"Haskell"

capitalizE

Capitalize ならぬ capitalizE。字面どおり最後の文字だけを大文字に変える。
……いや,名前はともかく(っていうか,こんな動作に名前がついてるとは思えないが)。
toUpper や toLower を使うには Data.Char モジュールを import する。

import Data.Char

capitalizE []     = []
capitalizE [c]    = (toUpper c):[]
capitalizE (c:cs) = (toLower c):(capitalizE cs)

実行結果。

*Main> capitalizE "Haskell"
"haskelL"

OK。

で,Capitalize のほう。こうやったら全部大文字になった。

capitalize []     = []
capitalize (c:cs) = (toUpper c):(capitalize cs)
*Main> capitalize "haskell"
"HASKELL"

”リストの先頭”は常にあるんだから当然だよな。

join

要素と要素の間ごとに新しい要素を挟み込む。

join s []     = []
join s (c:cs) = c:s:(join s cs)

実行結果。

*Main> join '-' "abc"
"a-b-c-"

あれ?

そうか,一番最後の要素も c:cs にマッチングしてるんだな。てことは”最後の要素”を表すパターンがあればいいのか。これでどうだ。

join s []     = []
join s (c:[]) = c:[]
join s (c:cs) = c:s:(join s cs)

実行。

*Main> join '-' "abc"
"a-b-c"

OK。

関数の定義とパターンマッチング

「入門Haskell」から文字列の文字数を数える関数。

bytesCount []     = 0
bytesCount (c:cs) = 1 + bytesCount cs

関数名 bytesCount につづく引数の部分がパターンを表していて,受け取った引数のパターンによって関数本体の内容が変わる。
1行目の [] は空のリスト,c:cs は先頭の要素とそれ以降の要素に分けられることを表している。だからこの関数を評価すると,引数が空リストなら 0,空でないなら(先頭とそれ以降に分けられるから)先頭の文字数 1 に以降の文字数を足したもの,となる。
パターンがいくつもある場合には定義をいくつも書けばいい。パターンは上から順にチェックされる。

リストの操作は基本的に先頭から順に,再帰的にすればいいようだ。

ちょっと練習。すべての要素を足し合わせる。

mySum []     = 0
mySum (c:cs) = c + mySum cs

実行結果。

*Main> mySum [1,2,3,4,5,6,7,8,9,10]
55
*Main> mySum []
0

href

via. http://d.hatena.ne.jp/nskj77/20060401

コマンドラインから使えるリファレンス。Refe の Haskell 版。バージョンは 0.3.1。

http://i.loveruby.net/w/href.html

インストールは README.jp に書いてあるとおりにすれば簡単……とはいかない。Windows だから。
仕方がないので Makefile をながめて次のように書き換えた。換えたところだけ示す。

bindir = c:\usr\local\bin
EXEEXT = .exe
# mkdir -p $(bindir)

1行目はコマンドのインストール先,c:\usr\local\bin にインストールすることにした。
2行目は実行ファイルの拡張子。3行目は今回はすでにフォルダがあるので全体をコメントアウトした。もしないなら -p オプションだけ削除する。
これで良し。make はないので nmake を使う。

>nmake
Microsoft (R) Program Maintenance Utility   Version 1.50
Copyright (c) Microsoft Corp 1988-94. All rights reserved.
ghc -cpp --make -package network href.hs -o href.exe
Chasing modules from: href.hs
(以下略)
>nmake install
Microsoft (R) Program Maintenance Utility   Version 1.50
Copyright (c) Microsoft Corp 1988-94. All rights reserved.
ghc -cpp --make -package network href.hs -o href.exe
Chasing modules from: href.hs
(以下略)

次,データベースの準備。
c:\usr\share\href にインストールするので c:\usr\share を先に作っておく。href はなくてもいい。

>set HREF_DATADIR=c:\usr\share\href
>mkhref ref/*.*

これでOK。あとはコントロールパネルで環境変数 HREF_DATADIR を設定しておけば普通に使える。

>href filter
Data.List.filter
filter :: (a -> Bool) -> [a] -> [a]    PRELUDE
filter f xs
・・ケ・ネ xs 、ホ、ヲ、チ f 、ャ True 、ヒ、ハ、・ラチヌ、タ、ア、・
スク、皃ソ・・ケ・ネ、ヨ、ケ。」

って,文字化けしてる!……EUCかぁ。

追記:
ref 以下のファイルを ShiftJIS に変換してからインストールしたら直った。
変換に使った Ruby のスクリプト。

require 'find'
require 'fileutils'
require 'nkf'

src = ARGV.shift
dest = ARGV.shift

Find.find(src) do |f|
  case File.ftype(f)
  when "directory"
    FileUtils.mkdir_p(f.sub(src, dest))
  when "file"
    File.open(f, "r") do |infile|
      File.open(f.sub(src,dest), "w") do |outfile|
        outfile.write NKF.nkf("-E -s", infile.read)
      end
    end
  end
end

さらに追記:
むぅ,こうすればコマンド2つですんだ。

>mkdir ref.sjis
>for %X in (ref/*) do nkf -E -s ref/%X > ref.sjis/%X