Haskellで"Hello, world."、簡単な出力、計算と数字を入力するところまで
前回のエントリで、Haskell+IntelliJの環境が出来上がった。プラグインとかHLintとか準備出来てないけど、とりあえずプログラムは書けるようになった。公式の10分で学ぶHaskellにHaskell入門があるけど、ちょっと知能指数の低い俺には長かったり、例えば文字列がダブルクォートだとかそういう他のプログラミング言語と変わらないようなことも冗長に思えた。あとコンソールのREPLにはあんまり興味が無い。とにかく手を動かして、プログラムをコンパイルして動かせるようにしてみたい、のですっごい走り書きで超簡単なHaskellプログラムを書いてそこから10分で学ぶ〜に戻れるようにしてみる。
まずはHaskellで"Hello, world."
helloというHaskellプロジェクトをIntelliJでつくって、cabalを通してghcでコンパイルして何か表示するところまでやってみよう。
HaskellのプログラムはMain.mainから始まる。Main.hsの中にはMainというモジュールを書くのが決まりだ。Javaと一緒。ファイル名とモジュール名を一致させるのが習わしらしい。
ここにプログラムを書いていこう。Mainモジュールにmain関数を定義していく。Main.mainの戻り値がIO ()
であることに注意すると、
module Main where main = do print "Hello, world!"
となる。doは副作用があるプログラムを扱う部分を作り出す。printは明らかに副作用を持つのでdoの中にprintを入れてしまおう。Haskellでは副作用のある関数呼び出しを無闇矢鱈に振り回すことは固く禁じられている。画面に入力したり出力したりする関数は副作用がある。たとえばファイルに書いたりデータベースに書き込みをしたり読み込みをしたりも同じだ(画面への表示は画面というファイルへの書き込みだというのがUnixの画期的な発明である)。なのでそういう部分は他の"純粋な"部分から隔離される。IO ()はその世界の名前で、型の名前でもある。
実行してみよう。Run->Run...で出てくるちっこいウィンドウでhelloを選ぶ。SDKが設定されていない場合はGHCを選んでOKを押してもう一度Run(またはShift+F10)だ。
で、結果がこうなるはず。
あとHaskellはPythonと同じでインデントでプログラムの構造を決めるオフサイドルールを採用している。私は4文字スペースでインデントしている。
次、関数型言語の入門ではfactかfibと相場が決まっているので、factを書いてみると
module Main where main = do print "Hello, world!" print "fact 10 is" print (fact 10) -- factの定義 fact 1 = 1 fact n = n * fact ( n - 1 )
factの数学的定義をそのまま書き下してみた。
答えは
"Hello, world!" "fact 10 is" 3628800
これで計算、出力をするすごく小さいプログラムが書けた。
printfが使いたい
Haskellでもprintfが使いたい。変態的な関数かもしれないけれど使いたいんだからしょうがない。デバッグ等にも必要なのでこれを使って上のプログラムを書き換えてみる。 printfはHackageによるとText.Printfの中で定義されているらしい。 importを使うことでText.Printfモジュールを使えるようにインポートすることができる。ここでMainの中でprintfが使いたいのでimportをmoduleの中で以下のように呼び出しながらprintfでfact 10の値を表示してみる。
module Main where -- Text.Printfをインポートしてprintfを使えるようにしてみる。 import Text.Printf main = do print "Hello, world!" -- ここでprintfを使ってみよう。%dは整数を表示する指定子というのはHaskellでも変わらない。 printf "fact 10 is %d\n" (fact 10) fact 1 = 1 fact n = n * fact ( n - 1 )
こうすると...?
Error:(7, 9) ghc: No instance for (PrintfArg a0) arising from a use of `printf' The type variable `a0' is ambiguous Possible fix: add a type signature that fixes these type variable(s) Note: there are several potential instances: instance [safe] PrintfArg Char -- Defined in `Text.Printf' instance [safe] PrintfArg Double -- Defined in `Text.Printf' instance [safe] PrintfArg Float -- Defined in `Text.Printf' ...plus 12 others In a stmt of a 'do' block: printf "fact 10 is %d" (fact 10) In the expression: do { print "Hello, world!"; printf "fact 10 is %d" (fact 10) } In an equation for `main': main = do { print "Hello, world!"; printf "fact 10 is %d" (fact 10) }
となり、エラーが出てしまう。理由はThe type variable `a0' is ambiguous
とあるようにprintfの2つ目の引数の型が推論できないからのようだ。理論をすっ飛ばすこの記事では難しそうなことは深くは追いかけない。この引数(fact 10)
が整数なのは人間には明らかなようだがコンパイラはありうるすべての型について考えた挙句決めかねる様子。コンパイラはPossible fix: add a type signature that fixes these type variable(s)
というように教えてくれているので素直にfactの関数に型宣言をしてあげると
module Main where import Text.Printf main = do print "Hello, world!" printf "fact 10 is %d\n" (fact 10) -- ここで型宣言 fact :: Int -> Int fact 1 = 1 fact n = n * fact ( n - 1 )
"Hello, world!" fact 10 is 3628800
ちゃんと計算できている。fact :: Int -> Int
の部分が型宣言で、引数をIntでとってIntで返す関数だと明示的にしてあげることでprintf "fact 10 is %d\n" (fact 10)
の(fact 10)
の部分がIntに固定されてprintfが正しい型で文字を表示できるようになる。
文字を入力したい
今10の階乗を計算して表示できるようになったけど、10以外も表示したい。入力を受け付けられるようにしたい。HackageのSystem.IOの中にreadLnなる関数があるのでこれを使おう。これは引数の型を推論させてその型の数字を入力として受け取りIO t0の型(t0は推論された任意の型)にしてくれる。具体的にコードを見てみると
module Main where import Text.Printf main = do print "Hello, world!" -- 推論してもらえないので(printfが絡むと推論は失敗しやすい)型を明示的に宣言する。 -- このreadLnはIO Int型を返す。ここは入出力を取り扱う汚いIO ()の世界なので -- 外界に持ち出せないようにIntではなくてIO IntとIO型の一種となる。 inNum <- readLn :: IO Int printf "%d\n" inNum printf "fact %d is %d\n" inNum (fact inNum) fact :: Int -> Int fact 1 = 1 fact n = n * fact ( n - 1 )
こうすると例えばecho '100' | ./hello
などとしてhello実行ファイルに標準入力を渡してやると(これはIDEからではなくこんそーるからやるほうが楽)
"Hello, world!" 100 fact 100 is 0
(もちろん./helloを実行して100と改行を入力して普通に入力してもいい)
...あれ?100の階乗が0になっている。よく考えてみると100!はとてつもなくでかい数字になる。具体的にはWolframalpha - 100!。つまりオーバーフローしている。Integerは巨大整数を扱えるのですべてのIntをIntegerに変えてみると
module Main where import Text.Printf main = do print "Hello, world!" inNum <- readLn :: IO Integer printf "%d\n" inNum printf "fact %d is %d\n" inNum (fact inNum) fact :: Integer -> Integer fact 1 = 1 fact n = n * fact ( n - 1 )
"Hello, world!" 100 fact 100 is 9332621544394415268169923885626670049071596826438162146859296389521759999322991560894146397615651828625369792082722375825118521091686400000000000 0000000000000
できた! ここでfactではなくてfibにすることも簡単。今日習った方法で数字を入力して数字を出力することができるので、整数を取り扱う関数fibを作るだけだ。整数でなくてもいい、Doubleに変えれば浮動小数点演算もできる。
今日習ったこと
- 入力と出力。readLnとprintfで取り扱える。
- プログラムエントリポイントはMain.main
- fact関数を定義してみた。ただしこのfact関数は不完全なことに注意。
- Intはビット数に制限がある整数。ちなみに30ビット以上であることが仕様で決まっている。Integerは巨大整数を扱える。
- Hoogleを使って関数の名前とか検索ができる。