euphonictechnologies’s diary

Haskell超初心者の日記です。OCamlが好きです。

follow us in feedly

Haskellで"Hello, world."、簡単な出力、計算と数字を入力するところまで

前回のエントリで、Haskell+IntelliJの環境が出来上がった。プラグインとかHLintとか準備出来てないけど、とりあえずプログラムは書けるようになった。公式の10分で学ぶHaskellHaskell入門があるけど、ちょっと知能指数の低い俺には長かったり、例えば文字列がダブルクォートだとかそういう他のプログラミング言語と変わらないようなことも冗長に思えた。あとコンソールのREPLにはあんまり興味が無い。とにかく手を動かして、プログラムをコンパイルして動かせるようにしてみたい、のですっごい走り書きで超簡単なHaskellプログラムを書いてそこから10分で学ぶ〜に戻れるようにしてみる。

まずはHaskellで"Hello, world."

helloというHaskellプロジェクトをIntelliJでつくって、cabalを通してghcコンパイルして何か表示するところまでやってみよう。

f:id:euphonictechnologies:20140802193810p:plain

HaskellのプログラムはMain.mainから始まる。Main.hsの中にはMainというモジュールを書くのが決まりだ。Javaと一緒。ファイル名とモジュール名を一致させるのが習わしらしい。

f:id:euphonictechnologies:20140802193914p:plain

ここにプログラムを書いていこう。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)だ。

f:id:euphonictechnologies:20140802194402p:plain

f:id:euphonictechnologies:20140802194408p:plain

f:id:euphonictechnologies:20140802194414p:plain

で、結果がこうなるはず。

f:id:euphonictechnologies:20140802194614p:plain

あとHaskellPythonと同じでインデントでプログラムの構造を決めるオフサイドルールを採用している。私は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を使って関数の名前とか検索ができる。