TIM Labs

2012年12月アーカイブ

import Graphics.Gloss
が最初の行にあるので、まずは、Graphics.Glossについて調べてみよう。

gloss-1.7.8.1: Painless 2D vector graphics, animations and simulations.

haskell.orgのちゃんとした正式ドキュメントと、前回書いたものを比較してみよう。
Webから実行したときのプログラムがこれ。

import Graphics.Gloss
picture = circle 80

そして、Graphics.Gloss のページのサンプルプログラムがこれ。

import Graphics.Gloss
main = display (InWindow "Nice Window" (200, 200) (10, 10)) white (Circle 80)

mainがあって、displayの後ろのごちゃごちゃあって、最後に (Circle 80) とある。

まずは、これをそのまま sample.hs としてソースプログラムを作っちゃって、実行してみよう。

$ cat sample.hs
import Graphics.Gloss
main = display (InWindow "Nice Window" (200, 200) (10, 10)) white (circle 80)
$ ghc sample.hs
[1 of 1] Compiling Main             ( sample.hs, sample.o )
Linking sample ...
$ sample

出来上がった sample を実行すると、次のウィンドウが画面左上の方に出た。

sample-circle.png

さて、 display について調べよう。

displaySource
:: Display    Display mode.
-> Color    Background color.
-> Picture    The picture to draw.
-> IO ()
    
Open a new window and display the given picture.

との説明があった。
さらに、 Display については、

InWindow String (Int, Int) (Int, Int)   
Display in a window with the given name, size and position.

とあった、 InWindow の直後の文字列が、ウィンドウの名前になるようだ。
実際、sample.hs では、"Nice Window"としていたのが、ウィンドウのタイトルバーの位置に示されている。

最初の (Int, Int) がウィンドウのサイズ、次の (Int, Int) がウィンドウの位置である。

そして、display の第2パラメータが white になっていたが、これは背景色。

display の第3パラメータに指定されたもの(Picture)を表示する。
ここでは、Circle 80 である。

ということで、sample.hs を少しだけ書き換えてみた。

import Graphics.Gloss
display = circle 80

main = display (InWindow "Nice Window" (500, 500) (10, 10)) white display

WebからGlossを使うときには、このようになっているソースの最後のmainの行を除いた部分をWebで入力するのだろう。

このままでは、図形が1つだけしか描けない。
複数の図形を描くには、次の様にするとよい。

picture = pictures [
    circle 80,
    rectangleWire 200 100 ]

カンマ(,)で区切って、[]の中に並べると、それらを描いてくれるようだ。

$ cat sample2.hs
import Graphics.Gloss
picture = pictures [
    circle 80,
    rectangleWire 200 100 ]

main = display (InWindow "Nice Window" (300, 200) (10, 10)) white picture
$ ghc sample2
[1 of 1] Compiling Main             ( sample2.hs, sample2.o )
Linking sample2 ...
$ sample2

sample-circlebox.png
年内に何とかGlossで絵が描けるところまでできたので、安心して年が越せる。

最初の授業がまだ終わらないのだが、長くなったので、残りは次回にしよう。
第1回目の授業の記録を追ってみよう。

Haskellの先生が、Chris Smithさん。

そして、Sue Spenglerさんがこの学校の創立者、校長、秘書その他何でも屋さんのようである。
何か、ちょっと変わった学校のような気がする。
まあ、Haskellを子供に教えようという学校だから、そういうことくらい当然あるだろう。

とりあえず、どうやったらHaskell遊びができるのか、斜め読みをしてみた。

子供が(大人も)簡単にお絵描きをHaskell+Glossで試せるページが公開されている。

http://dac4.designacourse.com:8000/sim

ここでは、いくつかの選択があるのだが、Pictureを選んで、ちょっと実験したいことを書き込んで、Runボタンを押すと、右側半分がグラフィック画面になって、そこに指示した絵が出てくるようになっている。これを使えば、面倒なインストールの作業もいらない。

最初に説明されるのが、以下の2行だ。

import Graphics.Gloss

picture = circle 80

ということで、理解しなくてよいから、とにかくこれを入れて、Runボタンを押してみると円が表示された。

week1-1.png
ちゃんと円が出たようなので、OKのはずだ。
後は、ここにいろいろなことを書けば、複雑な絵だって、大丈夫なのではないだろうか。

で、次にやることは当然、このWebページを使わず、ghciから直接動かすことだ。
ということで、上の2行を入れてみた。

Prelude> import Graphics.Gloss
Prelude Graphics.Gloss> picture = circle 80

<interactive>:3:9: parse error on input `='

これはダメなんだ。
やりたいことは円を描くだけだから、circle 80 だけ入れてみよう。

Prelude Graphics.Gloss> circle 80
Loading package array-0.4.0.0 ... linking ... done.
Loading package deepseq-1.3.0.0 ... linking ... done.
Loading package OpenGLRaw-1.3.0.0 ... linking ... done.
Loading package GLURaw-1.3.0.0 ... linking ... done.
Loading package ObjectName-1.0.0.0 ... linking ... done.
Loading package StateVar-1.0.0.0 ... linking ... done.
Loading package Tensor-1.0.0.1 ... linking ... done.
Loading package OpenGL-2.6.0.1 ... linking ... done.
Loading package containers-0.5.1.0 ... linking ... done.
Loading package GLUT-2.3.1.0 ... linking ... done.
Loading package bytestring-0.10.0.1 ... linking ... done.
Loading package binary-0.5.1.1 ... linking ... done.
Loading package bmp-1.2.3.2 ... linking ... done.
Loading package gloss-1.7.7.1 ... linking ... done.
Circle 80.0
Prelude Graphics.Gloss>

いっぱいロードしてくれたけれど、Circle 80.0 と表示されただけで、グラフィックスウィンドウが出て、そこに円が出るなんてことは起きなかった。

子供向けの授業では、ghcから直接glossを使う方法が書かれていないようなのだ。

次回までに、ghc/ghci から直接glossを楽しむ方法を調べておこう。
Haskellは、なぜか非常に難しい言語、理解しがたい言語という考えが結構はびこっているようだ。
確かに、既存の、いわゆる普通の言語、C,C++,Java,Perl,PHP...などと比べると、非常に違う。
過去に何らかのプログラミング言語を習っていると、新しい言語も簡単に習得できると言われている。
しかし、その考えはHaskellでは成り立たない。逆に、既習得言語の影響で、Haskellの概念の理解に困ることも多いのだ。

ということで、それならまだ純粋無垢な子供たちに、これから広まる言語、Haskellを教える方が良いのではないだろうか。
と思っていたら、世界には、ちゃんとやっている人、プロジェクトがあった。

Haskell For Kids!
 

というプロジェクトで、12,3歳の子供にプログラミングを教えるのだが、使用言語がHaskellなのだ。
プログラミングを教える場合、抽象的なことを考えられるようにしないといけないのだが、確かにHaskellは余分なおまじないが少ない言語で、プログラムは短くシンプルになる。

子供たちに、よくあるHaskellの本のように、延々と文字だけしか出て来ない環境で、わけの分からない理屈を教えるのは無理だろう。
ということで、お絵描き(Graphics)を最初から教えてしまうのだ。

とりあえず、Web上でグラフィックに対応したHaskellを動かして、その結果も表示してくれるサイトが用意されている。
まあ、そういうやり方でも勉強はできるだろうが、自分のPC上でもちゃんとグラフィックスが動くようにしたいものだ。
そのために、Glossというライブラリを使うようなのだ。

ということで、Glossのホームページを見つけて、そこにある説明に沿ってインストールしてみた。
http://trac.ouroborus.net/gloss

サンプルもインストールし、何が出きるのかをちょっと想像してみた。
物理シミュレーションのようなものから、インターラクション、レイトレーシングまであった。
Glossは2次元グラフィックスなのに、こんなのがあってビックリ。
はるか昔、まだ昭和の頃、レイトレーシングってやったなあと郷愁に浸ってしまった。
このレイトレーシング、静止画ではなくて動画として作られていて、床上の3つの球が延々と移動しているのだ。

raytracing.png

漢数字にするのはだいたいOKのようであるが、もうちょっと試してみよう。Haskells.jpgのサムネール画像

*Main> putStrLn (konvert 1111111)
壱陌壱拾壱萬壱阡壱陌壱拾壱

1がいっぱいあったとき、不要な壱は使われないことが多い気がする。
上の場合、陌拾壱萬阡陌拾壱の方が普通ではないだろうか?

ということで、拾、陌、阡の場合(""でない)とくっつける場合には、数は漢字に直さず、桁の漢字だけにしてみた。

combinedig (n,s)
    | n == 0            = ""
    | n == 1 && s/=""   = s
    | otherwise         = suuji!!n ++ s

*Main> putStrLn (konvert 1111111)
陌拾壱萬阡陌拾壱

これでかなり大きな数字まで漢数字にできる。
ところで、

*Main> putStrLn (konvert 0)

*Main>

となって、0は ""になってしまうのだ。
0も処理するために、konvertに、この次を追加しよう。

konvert 0 = "零"

すると、

*Main> putStrLn (konvert 0)


となって、大丈夫のようだ。

さらに、より大きい数(大数)を扱えるように、kuraiを変更した。

kurai    = ["","萬","億","兆","京","垓","秭","穰","溝","澗","正",
               "載","極","恒河沙","阿僧祇","那由他","不可思議","無量大数"]

無量大数は、10^68 のことである。(参照:ウィキペディア、命数法)

*Main> putStrLn (konvert (10^68))
壱無量大数

ということで、大きな数まで扱えるようになった。やれやれ。

*Main> 2^240
1766847064778384329583297500742918515827483896875618958121606201292619776
*Main> putStrLn (konvert (2^240))
七 阡六陌六拾八無量大数四阡七陌六不可思議四阡七陌七拾八那由他参阡八陌四拾参阿僧祇弐阡九陌五拾八恒河沙参阡弐陌九拾七極五阡七載四阡弐陌九拾壱正八阡五 陌拾五澗八阡弐陌七拾四溝八阡参陌八拾九穰六阡八陌七拾五秭六阡陌八拾九垓五阡八陌拾弐京阡六陌六兆弐阡拾弐億九阡弐陌六拾壱萬九阡七陌七拾六

ここまでのプログラム konvert3.hs

Haskells.jpgのサムネール画像1234567890を"12億3456萬7890"にするところまではできた。
残りは、1?9999を漢数字に直すことだ。
まず、そのために、漢字のリストを2つ追加した。

kurai,kuraisub,suuji  :: [String]
kurai    = ["","萬","億","兆","京","垓","秭","穰","溝","澗","正"]
kuraisub = ["","拾","陌","阡"]
suuji    = ["","壱","弐","参","四","五","六","七","八","九"]

これを使って、まず、拾、陌、阡、(十、百、千)を処理してみよう。
これは、萬、億、兆、京、、、、のときと同様の処理で済むはず。
ということで、

combinedig   1桁の数字と位の数字をくっつける関数
konvertsub     1?9999を文字列に直す関数

を作った。それぞれ、combine、 konvert にそっくりである。

combinedig :: (Int,String) -> String
combinedig (n,s)
    | n == 0     = ""
    | otherwise  = show n ++ s

konvertsub :: Int -> String
konvertsub n = concat (map combinedig (reverse (zip (reverse (splits 10 n [])) kuraisub)))

今まで、combineで、1?9999の数字を文字列に直すところが show n となっていたが、これを konvertsub n に変更すれば、位の漢字が全部出てくるはずだ。

combine :: (Int,String) -> String
combine (n,s)
    | n == 0     = ""
    | otherwise  = konvertsub n ++ s

さて、実行するとどうなるか。

*Main> putStrLn (konvert 1234567890)
1拾2億3阡4陌5拾6萬7阡8陌9拾

さて、残りは、1?9の数字を漢字に直すことだ。
show n で数字を出していたところを、漢数字壱?九のリストsuujiのn番目の漢字に直せば良いだろう。

combinedig (n,s)
    | n == 0     = ""
    | otherwise  = suuji!!n ++ s

さて、実行してみよう。

*Main> putStrLn (konvert 1234567890)
壱拾弐億参阡四陌五拾六萬七阡八陌九拾

ちゃんと動くようだ。今日は、ここまで。

ここまでのプログラム konvert2.hs
横道に入り過ぎたので、『関数プログラミング入門』に戻ろう。Haskells.jpgのサムネール画像
といっても、途中は面倒なので省略して、第5章に飛ぼう。
「5.1 数を言葉に変換する」という節がある。

*Main> convert 102340
"one hundred and two thousand three hundred and forty"

という風に、アラビア数字を長ったらしい英語に変換するものだ。

しかし、これが動いても、日本ではうれしくない。
やはり、アラビア数字を、漢数字に直してくれないと面白くない。
できることなら、翻訳のときに、convertを漢数字を出す関数に翻訳してくれていたら嬉しかった。

でも、それはダメだったようなので、なんとか作ってみよう。
関数名は、漢字への変換ということで、konvert としよう。

つまり、

*Main> putStrLn (konvert 1234567890123456789)
陌弐拾参京四阡五陌六拾七兆八阡九陌壱億弐阡参陌四拾五萬六阡七陌八拾九

というような関数konvertを作ってみよう。

細かい道具から準備していこう。

英語圏では、数字は3桁毎に区切るが、日本語では4桁毎となる。
ここでは、より一般化して、数字を n 桁毎に区切る関数を作ろう。
大きな数字も扱えるように、与えられる数字の型は Integer にしておこう。

この関数splits は、与えられた数字を、下の桁から、指定桁毎に切り出して、リストにしまう。

ということで書いてみたのが、これ↓。

splits       :: Integral a => a -> a -> [Int] -> [Int]
splits  d n x
     | n == 0      = []
     | n >= d      = splits d (div n d) (fromIntegral (mod n d) : x)
     | otherwise   = fromIntegral n : x

確認しよう。切り分けるのは10000ごとである必要はなく、1000ごとでも、それどころか16ことでも可能だ。

*Main> splits 10000 1234567890 []
[12,3456,7890]
*Main> splits 1000 1234567890 []
[1,234,567,890]
*Main> splits 10 1234567890 []
[1,2,3,4,5,6,7,8,9,0]
*Main> splits 16 1234567890 []
[4,9,9,6,0,2,13,2]

次に、4桁毎の位の漢字のリスト kurai を作っておく。

kurai    :: [String]
kurai    = ["","萬","億","兆","京","垓","秭","穰","溝","澗","正"]

これと、 [12,3456,7890] を組み合わせることで、

"12億3456萬7890" という数字を組み立ててみよう。
分割した数字のリストは大きい桁から小さい桁に向けて並んでいるが、位(kurai)は、小さい桁から大きい桁に向けて並んでる。
そのため、一度数字リストを逆順にして、漢字と合わせいかないといけない。
ということで、次の順番で変換する関数を考えよう。

[12,3456,7890]
[7890,3456,12]                    -- 逆順
[(7890,""),(3456,"萬"),(12,"億")]   -- zip
[(12,"億"),(3456,"萬"),(7890,"")]   -- 逆順(元の順序)
["12億","3456萬","7890"]            -- 位の数字と結合
"12億3456萬7890"                    -- 全体を結合

まず、数字と位文字とを結合する関数 combine を考えよう。

combine :: (Int,String) -> String
combine (n,s)
    | n == 0     = ""
    | otherwise  = show n ++ s

数字が0のとき、位の数字だけが出るのはまずいので、そのときの出力は""にしている。

*Main> putStrLn (combine (12,"億"))
12億
*Main>

これを使うことで、変換関数konvertは次のように書ける。

konvert :: Integer -> String
konvert n = (concat (map combine (reverse (zip (reverse (splits 10000 n [])) kurai))))

ここまでを動作確認してみよう。

*Main> putStrLn (konvert 1234567890)
12億3456萬7890
*Main> putStrLn (konvert 12340000000000056789)
1234京5萬6789

0兆0億 の部分は正しく無視されている。

ここまでで、半分できた感じなので、残りは次回にしよう。

全部を合わせたプログラムkonvert1.hs

ユニコード表作成の続きだ。といっても、残り作業はほんのちょっと。
先頭の数字を16進数表示に直せばよいだけだ。
行の先頭に最初の文字のコードを10進数で追加していたのはこれだ。

addhead xa@(x:_) = show (ord x) ++ "  " ++ xa

show (ord x) で、文字x の文字コードが出ていた。

16進数は、show の代わりに showHex でできるらしい。
しかし、Numericのインポートが必要だ。

*Main Data.List> import Numeric
*Main Data.List Numeric> showHex 1000

<interactive>:34:1:
    No instance for (Show ShowS)
      arising from a use of `print'
    Possible fix: add an instance declaration for (Show ShowS)
    In a stmt of an interactive GHCi command: print it
*Main Data.List Numeric>

importしたからちゃんと使えるはずなのだが、エラーが出てきてしまった。
原因を調べよう。

*Main Data.List Numeric> :t showHex
showHex :: (Integral a, Show a) => a -> ShowS

ということで、入力は1つなので、間違いはないはず。
もしかして、10進数の文字列に直すshowとは何か違うかもしれないので、確認しよう。

*Main Data.List Numeric> :t show
show :: Show a => a -> String

show はString が出てきているが、showHex は ShowS が出てくるのだ。
つまり、showHexは、showとは違うのだ。
では、ShowS型って何だろう。

*Main Data.List Numeric> :i ShowS
type ShowS = String -> String     -- Defined in `GHC.Show'

haskell.orgの説明を見ると、

type ShowS = String -> StringSource
The shows functions return a function that prepends the output String to an existing String.

となっていた。showsの関数は、既に存在する文字列の前に付加されるらしいのだ。

それから、ちょっとネットで探して、結局、showHex は、16進数にする数値だけではなく、16進数になった文字列が前にくっつけられる文字を第2引数として加えないとダメなのだ。

要するに、

showHex :: (Integral a, Show a) => a -> String -> String

と考えてないといけない。

*Main Data.List Numeric> showHex 1000 ""
"3e8"
*Main Data.List Numeric> showHex 1000 "番地"
"3e8\30058\22320"
*Main Data.List Numeric> putStrLn (showHex 1000 "番地")
3e8番地

さて、これで、16進数に直す方法が分かった。

addhead xa@(x:_) = showHex (ord x) "  " ++ xa

showHexの第2引数に "  " を与えることで、16進数の後ろに2文字の空白文字がくっつくので、その後に漢字が並べば大丈夫だ。

それで実行してみると、こうなった。

*Main Data.List Numeric> putStrLn (take 100 unicodetable)
4e00  一丁丂七丄丅?万丈三上下丌不与?丐丑丒?且丕世丗丘丙????丞丟
4e20  ?両?丣两?並?丨?个丫?中丮丯丰丱串??丵丶?丸丹?主丼??丿
4e40  乀乁乂乃乄久?乇???之?乍乎乏

成功だ。あとは、unicodetable を書き出すだけ。

main = writeFile "UnicodeTable.txt" unicodetable

これでファイルがちゃんと書き出される。

長くなったので、unicodeTable.hsを用意した。

書き出したユニコード表 UnicodeTable.txt

unicodetable4e00.png

さて、ファイルへの出力が分かったので、何か役立ちそうなテキストファイルを出力してみよう。
ということで、Unicode表を作ってみよう。
ユニコード漢字全文字を作るのは、ユニコード全20902字で説明したが、プログラムを再掲しよう。

import Data.Char
import Data.List

fromK :: Char -> [Char]
fromK c = c :  fromK (chr ((ord c) + 1))
unicode = take 20902 (fromK '一')

こでで、unicode を評価すれば、長さが20902の文字列ができあがる。
これをそのままファイルとして出力するには、

*Main Data.List> writeFile "unicodetest.txt" unicode

だけで良いのだが、これでは、1行に20902文字並んだとんでもないファイルができるだけだ。
一定の間隔で改行を入れないと表示のときにこまるので、一定間隔でこの長い文字列(リスト)を切ろう。
リストを2つに分ける関数 splitAtがある。切る位置と、元のリストを与えれば、2つに分けてくれる。

*Main Data.List> :t splitAt
splitAt :: Int -> [a] -> ([a], [a])

さて、これを使って、一定間隔でリストを切る関数を作ってみた。

splitEvery     :: Int -> [a] -> [[a]]
splitEvery n s
           | null sx    = [fx]
           | otherwise  = fx : (splitEvery n sx)
          where (fx,sx) = splitAt n s

動作確認すると、こんな感じ。

*Main Data.List> splitEvery 5 [1..16]
[[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15],[16]]

これで、長大な文字列を32文字毎に切った文字列のリストができる。

unicode = splitEvery 32 (take 20902 (fromK '一'))

次に、各32文字長の文字列を改行を挟みながら1つの文字列にしてしまおう。
改行で区切る関数がlinesなのだが、その逆の改行を挟みながらくっつける関数はunlineである。

unicodetable = unlines unicode

これで、32文字毎に改行したものが得られた。

*Main Data.List> putStrLn (take 100 unicodetable)
一丁丂七丄丅?万丈三上下丌不与?丐丑丒?且丕世丗丘丙????丞丟
?両?丣两?並?丨?个丫?中丮丯丰丱串??丵丶?丸丹?主丼??丿
乀乁乂乃乄久?乇???之?乍乎乏?乑???乕乖乗乘乙乚?乜九乞也
?
*Main Data.List>

これをファイルに書き出せば、ユニコード表の完成である。

しかし、これでは、コードの対応を取り難い。
各行の最初に、文字コードを入れてみよう。

unicodetable = unlines (map addhead unicode)

addhead xa@(x:_) = show (ord x) ++ "  " ++ xa

文字列の先頭に、先頭文字のコードを入れてみた。
これで、もう一度プログラムを読み込んで実行していみよう。

*Main Data.List> putStrLn (take 100 unicodetable)

*Main Data.List> putStrLn (take 100 unicodetable)
19968  一丁丂七丄丅?万丈三上下丌不与?丐丑丒?且丕世丗丘丙????丞丟
20000  ?両?丣两?並?丨?个丫?中丮丯丰丱串??丵丶?丸丹?主丼??丿
20032  乀乁乂乃乄久?乇???之?

先頭の文字コードが10進数のままだと分かりにくい。
ここは16進数に直してみよう。

だが、長くなったので、つづく。
Haskellで、SVGファイルを生成しているということは、テキストファイルを書き出しているはずだ。

ということで、このTutorilalを見ていたら、hasktut_head.png

writeFile "tut0.svg" $ writePolygons (blue [[(100,100),( ... 続く

という箇所があった。$ より右側は、文字列を組み立てているだけだと思われた。

Prelude> :t writeFile
writeFile :: FilePath -> String -> IO ()

ということは、ファイルパスと文字列を渡すだけで、ファイルに書いてくれるのだ。簡単!
Haskells.jpgのサムネール画像
元々は『関数プログラミング入門』を入手して始めたHaskell手習いであるが、やはり入出力がないと面白くないとおもったのだが、この本ではなかなかそういうことが出てこないのだ。
本の終わりに近い「第10章 モナド」でやっと出てくるようなのだ。
途中は無視して、ファイルの入出力だけつまみ食いをしよう。

では、テスト!

Prelude> writeFile "test.txt" "Hello, Haskell!"

とやるとすぐ戻ってきたが、ちゃんとファイルはできただろうか。

直接テキストファイル "test.txt" を見たら、中身が "Hello, Haskell!" になっていて成功だ。

テキストファイルの書き出しができたら、次は読み込みだ。
readFile という関数が用意されていたので、変数に読み込んでみよう。

Prelude> :t readFile
readFile :: FilePath -> IO String

Prelude> readFile "test.txt"
"Hello, Haskell!"
Prelude> let str = readFile "test.txt"
Prelude> str
"Hello, Haskell!"

となって、str にファイルの内容が読み込まれたようなのだ。
でも、

Prelude> length str

<interactive>:45:8:
    Couldn't match expected type `[a0]' with actual type `IO String'
    In the first argument of `length', namely `str'
    In the expression: length str
    In an equation for `it': it = length str

となってしまい、str はふつうの文字列ではないと叱られるのだ。

str の型を調べてみると

Prelude> :t str
str :: IO String

となっていて、 String ではなくて、 IO String なのだ。

では、どうすれば、ふつうの文字列へ読み込めるだろうか。

readFile を = で受けていたのを、<- に変えてみた。

Prelude> str <- readFile "test.txt"
Prelude> str
"Hello, Haskell!"
Prelude> length str
15
Prelude> :t str
str :: String

これで、ファイルの内容を、ふつうの文字列、String として扱うことができるようになった。

ということで、極めて簡単であるが、ファイル入出力の基本が分かったことにしよう。
では、次回は、なにかもう少し役に立ちそうなファイル出力をやってみよう。



hasktut_head.png
このロボットの絵につられて、Haskellで簡単にお絵描きができるらしいと思って、英語の文献を読んでみた。


いちばん気になるのは、どういう方法で絵を描いているかだ。
それで、英語もプログラムも無視して、プログラムで作ったと思われる最初の絵を探したら、これだった。tut0.svg (テキストファイル)

拡張子がSVGになっているのだが、Scalable Vector Graphics のことである。つまり、拡大縮小しても汚くならない。そして、このファイルはテキストファイルで、XMLで記述されているのだそうだ。
ということで、エディタで開いて、中身を見ようとしたら、テキストが表示されると思ったら、

tut0.png
が表示されてしまった。使っているのは、Emacsだ。しかたがないので、何の機能もなさそうな馬鹿エディタで開いてみたら、やっと中身が見えた。

<svg xmlns="http://www.w3.org/2000/svg"><polygon points="100.0,100.0 200.0,100.0 200.0,200.0 100.0,200.0 " style="fill:#cccccc;stroke:rgb(0,0,255);stroke-width:2"/><polygon points="200.0,200.0 300.0,200.0 300.0,300.0 200.0,300.0 " style="fill:#cccccc;stroke:rgb(0,0,255);stroke-width:2"/></svg>

ということは、このファイルをプログラムで作り上げてしまおうという魂胆のようだ。

こんな単純なのではなく、こんな絵もあったので、いろいろなことがやろうと思えばできそうだ。

tut1.png

こんな絵があるということは、何らかの方法で形状データのファイルを読み込んでいると思われる。

Haskellは、入出力は他の言語のようにはなっていない筈なんだ。
とりあえず、入出力関係を調べなければ。
(つづく)

Haskellの本を読んでいると、延々と文字しか出てこない。
やっぱり何か絵というか図というか、グラフィックスをやりたくなってしまった。
ということで、読んでいる本『関数プログラミング』『Real World Haskell』

  Haskells.jpgのサムネール画像    RealWorldHaskell.jpg

から離れて、ちょっとネットで検索してみた。
すると、こんなものが見つかった。

hasktut_head.png


というタイトルのPDFファイルが見つかった。

42ページしかないPDFファイルで、Haskellを楽しく教えてくれそうだ。
なお、これは英語のファイルであるが、絵、コードなどが半分くらいあるので、英語の分量としては20ページ程度に過ぎない。

中には、GHCロボットの挿絵がいろいろあるが、これはHaskellで作ったものとは思えないのだが、どうやらHaskellで作ったらしい絵があり、プログラムがちょろちょろ出てくる。
きっと、このプログラムを繋げて実行すると、絵が出てくるのではないかと思われる。


RealWorldHaskell.jpgHaskellの情報は、英語だと本当に豊富だ。
明らかに桁が違う。

ところで、タイムインターメディアで翻訳し、同じビルのオライリージャパンから出した "Real World Haskell"なんだが、

http://book.realworldhaskell.org/

にて、以前からフリーで公開されている。
もちろん英語版で、コメントを書き込むこともできる。



さて、次回までに、ちと英語を読んで内容を確認しなくては。
さて、円周率を連分数展開して計算することで有理数としては、いくらでも精度をあげられるらしいことが判明した。

しかし、有理数(分数)のままでは困る。10進数に直したい。

ということで、小数部分を、10**n を掛けることで、小数点の位置をずらしてしまおう。
そうして、整数部分だけをtruncateで取り出せば良いはずだ。

ということで、とりあえず小数点以下50桁まで求めてみよう。
そのために、 10 ** 50 を掛けてみる。

Prelude Data.List Data.Ratio> 10**50
1.0e50

想定外の指数表示になってしまった。
有理数にして、きちんと確認してみよう。

Prelude Data.List Data.Ratio> toRational (10**50)
100000000000000007629769841091887003294964970946560 % 1

と思ったら、なんと、1の後ろに0が50個並ばなかった。
雑な近似が行われていることが判明した。
** を使った場合、浮動小数点数としての計算で誤魔化されるみたいだ。

ベキ乗の演算子は、** と ^ の2つがあった。
それでは、^ を使うと、どうなるだろうか。

Prelude Data.List Data.Ratio> 10^50
100000000000000000000000000000000000000000000000000

こちらは、誤差がでないので、こちらを使おう。

どうやら、これで精度は問題ないみたいだ。
連分数を100項まで計算し、10^50との積の整数部分を取り出してみた。

Prelude Data.List Data.Ratio> truncate ((contfracPi' 100) * 10^50)
314159265358979323846264338327950288419716939937510

円周率の値はネット上にゴロゴロしているので比較してみたが、大丈夫のようだ。
ということで、調子に乗って、1000桁まで求めてみた。連分数は1500項までの近似を使った。

Prelude Data.List Data.Ratio> truncate ((contfracPi' 1500) * 10^1000)
3
14159265358979323846264338327950288419716939937510
58209749445923078164062862089986280348253421170679
82148086513282306647093844609550582231725359408128
48111745028410270193852110555964462294895493038196
44288109756659334461284756482337867831652712019091
45648566923460348610454326648213393607260249141273
72458700660631558817488152092096282925409171536436
78925903600113305305488204665213841469519415116094
33057270365759591953092186117381932611793105118548
07446237996274956735188575272489122793818301194912
98336733624406566430860213949463952247371907021798
60943702770539217176293176752384674818467669405132
00056812714526356082778577134275778960917363717872
14684409012249534301465495853710507922796892589235
42019956112129021960864034418159813629774771309960
51870721134999999837297804995105973173281609631859
50244594553469083026425223082533446850352619311881
71010003137838752886587533208381420617177669147303
59825349042875546873115956286388235378759375195778
18577805321712268066130019278766111959092164201989

1000桁を1秒程度で計算してくれるようだ。なかなか収束が良い連分数のようだ。

さらに調子に乗って、10000桁を求めてみた。

Prelude Data.List Data.Ratio> truncate ((contfracPi' 15000) * 10^10000)

しかし、なかなか反応がないので、食事に行ったら、結果が出ていた。

3.
14159265358979323846264338327950288419716939937510
14159265358979323846264338327950288419716939937510
58209749445923078164062862089986280348253421170679
82148086513282306647093844609550582231725359408128

................ 中略 ................

26456001623742880210927645793106579229552498872758
46101264836999892256959688159205600101655256375678

実際は、10001桁の数字が延々と続いていただけだが。

結局、40分くらいかかったようだ。

円周率を追いかけ回すのは、このあたりでお終いにしよう。

前回、無理数の取り扱いを複素数的な考えで取り扱ってフィボナッチ数列の一般項を求めた。意味的にはさほど遅くないはずの実装ではあるが、それでも 100,000,000 番目を求めようとすると結構時間がかかってしまう。実際に計測するとこんな感じだ。

90.67s user 0.21s system 99% cpu 1:30.94 total

チューニングの余地があるのは恐らく素朴に書いた累乗計算、fibExp だ。素朴と言っても一応アルゴリズム的な考慮はしたつもりだが、とはいえ言語について詳しく知っているわけでもなし、色々素人が小手先のチューニングを施すよりも、標準ライブラリに任せたほうがこういうものは余程パフォーマンスがいいものと相場が決まっている。

Haskell での累乗計算は (^) を使う。GHCi に聞いてみると、

> :i (^)
(^) :: (Num a, Integral b) => a -> b -> a       -- Defined in `GHC.Real'
infixr 8 ^

ということなので、a が Num クラスに属してさえいれば、a はなんだっていい。ということは、前回の FibNum を Num クラスのインスタンスにしてしまえばいいわけだ。それさえできれば標準の (^) が独自の型にも適用できるようになる。素晴らしい。

前回円周率を連分数展開を利用して求めたが、死ぬほど遅かった。
ということで、今回は違う連分数展開で計算してみよう。

これも、Wikipediaの連分数のところに載っているやつだ。

4pi.png


収束が結構良いのが 4/π を連分数で求めるものだ。
4/π=連分数 となっていたので、π=4/連分数 としただけだ。
連分数をそのままfoldrを使って書いてみた。

contfracPi' n  =  4 / (foldr (\x y -> 2*x-1+x*x/y) (1%1) [1..n])

これで、円周率を求める関数ができた。
実際に計算してみよう。

Prelude Data.List Data.Ratio> let contfracPi' n  =  4 / (foldr (\x y -> 2*x-1+x*x/y) (1%1) [1..n])
Prelude Data.List Data.Ratio> contfracPi' 10
11567456 % 3682035
Prelude Data.List Data.Ratio> fromRational (contfracPi' 10)
3.141593167908507
Prelude Data.List Data.Ratio> fromRational (contfracPi' 20)
3.141592653589806
Prelude Data.List Data.Ratio> fromRational (contfracPi' 30)
3.141592653589793
Prelude Data.List Data.Ratio>

連分数の計算を、20項あまりまで取ると、精度が浮動小数点の限界に達してしまうようだ。
ちなみに、円周率はこうだ。

Prelude Data.List Data.Ratio> pi
3.141592653589793

どうやら、収束はかなり良い気がする。

となると、やはり、円周率を何桁も求めてみたいと思うだろう。

しかし、浮動小数点になってしまうと、精度がすぐなくなってしまう。

いったいどうすれば、100桁、1000桁、10000桁の円周率を得ることができるのだろうか。
分数としては十分な精度が得られているはずなのだが、どうすれば高い精度の10進数に直せるのだろうか。

このアーカイブについて

このページには、2012年12月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2012年11月です。

次のアーカイブは2013年1月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。