169
136

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Haskell アクション 超入門

Last updated at Posted at 2014-10-08

Haskellではアクションと呼ばれる機能により副作用が扱えます。アクションの使い方の初歩を説明します。ライブラリで用意されたアクションを手っ取り早く使うことを目的としているため、モナドや圏論には言及しません。

シリーズの記事です。

  1. Haskell 超入門
  2. Haskell 代数的データ型 超入門
  3. Haskell アクション 超入門 ← この記事
  4. Haskell ラムダ 超入門
  5. Haskell アクションとラムダ 超入門
  6. Haskell IOモナド 超入門
  7. Haskell リストモナド 超入門
  8. Haskell Maybeモナド 超入門
  9. Haskell 状態系モナド 超入門
  10. Haskell モナド変換子 超入門
  11. Haskell 例外処理 超入門
  12. Haskell 構文解析 超入門
  13. 【予定】Haskell 継続モナド 超入門
  14. 【予定】Haskell 型クラス 超入門
  15. 【予定】Haskell モナドとゆかいな仲間たち
  16. 【予定】Haskell Freeモナド 超入門
  17. 【予定】Haskell Operationalモナド 超入門
  18. 【予定】Haskell Effモナド 超入門
  19. 【予定】Haskell アロー 超入門

練習の解答例は別記事に掲載します。

参照透過性

Haskellの関数は同じ引数に対しては常に同じ値を返します。この性質を参照透過性と呼びます。

以下の例ではadd 1 2は常に3を返します。何か特定の条件下で結果が変化することはありません。

add x y = x + y

main = do
    print $ add 1 2
実行結果
3

副作用

Haskellでは参照透過性のない関数は定義できません。具体的には、乱数のように毎回異なる結果を返す関数は定義できません。

毎回異なる結果を返すようなものは内部に何らかの状態を持っており、結果を返すための処理などで状態が変化していると考えられます。状態が変化することを副作用と呼びます。

状態に影響を受けて結果が変化する処理には参照透過性がありません。つまり副作用は参照透過性と対立する概念です。

アクション

副作用が扱えないと困ることもありますが、例えば乱数を返す関数のようなものを定義することは可能です。それを関数とは区別してアクションと呼びます。

乱数に限らず、時刻取得やファイル読み込みなど、結果が変化する可能性があるものはすべてアクションを使います。

乱数

'A'~'Z'までの任意の文字をランダムに返すアクションrandAlphaを定義する例を示します。

※ パッケージrandomが必要です。Leksahではdependenciesに追加します。

import System.Random

randAlpha = randomRIO ('A', 'Z')

main = do
    r <- randAlpha
    print r
    print =<< randAlpha
    randAlpha >>= print
実行結果(毎回異なる)
'E'
'P'
'I'

アクションは使用方法も関数とは異なり、見慣れない演算子が出て来ます。それらを説明します。

<-

アクションから値を取り出すには<-を使います。矢印(←)をイメージした記号です。

v_bind_m.png

OK
main = do
    r <- randAlpha
    print r

アクションに関連付けられた処理(この場合は乱数生成)は<-で値を取り出す際に行われます。

注意点

<-doブロックの中でしか使えません。

NG
main =
    r <- randAlpha
    print r
エラー内容
parse error on input `<-'

doなしで<-相当の処理を書くことは可能ですが、ラムダについて説明する必要があります。詳細は続編のHaskell アクションとラムダ 超入門で説明します。

=<<

一時変数を経由せずにアクションから値を取り出そうとしてもうまくいきません。

NG
test = do
    print randAlpha -- エラー

アクションから値を取り出して関数に渡すには専用の演算子=<<を使用します。他の言語では見ない形ですが、直感的には漏斗のようなものでアクションから関数に値をドリップしているとイメージすれば良いでしょう。

f_rbind_m.png coffee.png

OK
main = do
    print =<< randAlpha

アクション版の$だと捉えておけば良いでしょう。

イメージ

C++をご存知の方は、iostreamの<<をイメージすれば良いかもしれません。

cout << "abc";

>>=

逆向きもあります。>>=はbindと呼ばれます。

m_bind_f.png

main = do
    randAlpha >>= print

説明の都合上=<<を先に出しましたが、実は>>=が基本で、=<<はその逆です。

do>>=には密接な関係があります。詳細は続編のHaskell アクションとラムダ 超入門で説明します。

let

letでアクションから値を取り出すことはできません。

NG
main = do
    let r = randAlpha
    print r -- エラー

letと組み合わせることは可能ですが、アクションを変数に束縛する意味となります。値を取り出しているわけではないことに注意してください。letを使っても、値を取り出すには依然として<-などを使う必要があります。

main = do
    let r = randAlpha
    r' <- r
    print r'
    print =<< r
    r >>= print
実行結果(毎回異なる)
'Q'
'D'
'A'

呼び出し

main以外でdoを使って、mainから呼び出せます。

test = do
    r <- randAlpha
    print r

main = do
    test
    test
    test
実行結果(毎回異なる)
'S'
'P'
'T'

doブロック全体で1つのアクションを構成します。testには引数がないため関数ではなく、アクションが束縛された変数です。

main

実はmainも同じ構造です。今まで曖昧なまま進めてきましたが、mainは関数ではなくアクションが束縛された変数です。プログラムの起動時に自動的に呼び出される点が特殊です。

練習

【問1】ランダムにアルファベット小文字の1文字表示を繰り返してください。'z'が現れたら"END"と表示して終了してください。

動作イメージ(毎回異なる)
'j'
'm'
'z'
"END"

解答例

return

returnは値を入れてアクションを作る関数です。

return.png

アクションを作って、すぐに値を取り出してみます。

main = do
    a <- return 1
    print a
    print =<< return 2
    return 3 >>= print
実行結果
1
2
3

繰り返しますが、letでは値を取り出すことはできません。

NG
main = do
    let a = return 1
    print a -- エラー

束縛されたアクションから、アクション用の演算子で値を取り出します。

OK
main = do
    let a = return 1
    a' <- a
    print a'
    print =<< a
    a >>= print
実行結果
1
1
1

アクションを返す関数

関数の戻り値にreturnを通すと、アクションを返す関数が作れます。

add x y = return $ x + y

main = do
    print =<< add 1 2
実行結果
3

C言語などでreturnにより値を返すのに似ていますが、返されるのは値ではなくアクションである点が異なります。そのためアクション用の演算子で値を取り出す必要があります。

doとreturn

doと組み合わせれば、printなどを使って最後にreturnする関数が作れます。

add x y = do
    print x
    print y
    return $ x + y

main = do
    print =<< add 1 2
実行結果
1
2
3

ますますC言語などに似て来ます。

注意点

returnは単にそういう名前の関数で、C言語のように関数から抜ける機能はありません。後ろにコードがあれば、抜けずにそのまま続行します。

main = do
    print "abc"
    return 1    -- 素通り
    print "def" -- 実行される
    return 2    -- 戻り値
実行結果
"abc"
"def"

returnは関数の最後でアクションを返すのに使われることが多いため、そのような使い方をしている限りはC言語などに見た目が似せられるということです。

練習

【問2】階乗を求める関数factを、アクションを返す関数に書き換えてください。

fact 0 = 1
fact n | n > 0 = n * fact (n - 1)

main = do
    print $ fact 5
実行結果
120

ヒント: fact 0 = return 1

解答例

IO

今回出て来るアクションはすべてIOと呼ばれる型で、型名はIO+型で表記されます。

※ IO以外にもアクションはありますが、今回は分かりやすさを優先してIOアクションを単にアクションとして扱います。IO以外のアクションは続編のHaskell 状態系モナド 超入門で取り上げます。

とりあえず実用的には以下のように認識すれば使えます。

例: IO Int → 数値が取り出せるアクション

  • IO Intで1つの型を表します。
  • IOがアクション、Intが取り出される型を表します。

型注釈

型を明示するには型注釈と呼ばれる行を追加します。一般的には本体の上に書きます。

randAlpha :: IO Char
randAlpha = randomRIO ('A', 'Z')

なお、一行で書くこともできます。

randAlpha = randomRIO ('A', 'Z') :: IO Char

大抵は型注釈を書かなくても型推論により型が決まります。型推論がうまくいかないケースでは型注釈が必須です。そのような例を次に示します。

サイコロ

randomRで数値を指定するには型注釈が必須です。

import System.Random

dice :: IO Int
dice = randomRIO (1, 6)

main = do
    print =<< dice
    print =<< dice
    print =<< dice
実行結果(毎回異なる)
2
3
1

型注釈を省略するとエラーになります。

エラー内容
No instance for (Random a0) arising from a use of `randomR'
The type variable `a0' is ambiguous
Possible cause: the monomorphism restriction applied to the following:
  dice :: IO a0 (bound at src\Main.hs:3:1)
Probable fix: give these definition(s) an explicit type signature
              or use -XNoMonomorphismRestriction
Note: there are several potential instances:
  instance Random Bool -- Defined in `System.Random'
  instance Random Foreign.C.Types.CChar -- Defined in `System.Random'
  instance Random Foreign.C.Types.CDouble
    -- Defined in `System.Random'
  ...plus 33 others
(略)

diceから取り出せる型がBool, Foreign.C.Types.CChar, Foreign.C.Types.CDouble, その他33種類のうちどの型か曖昧だというエラーメッセージです。

練習

【問3】リストをランダムに並べ替える関数shuffleを実装してください。

具体的には以下のコードが動くようにしてください。

main = do
    print =<< shuffle [1..9]
実行結果(毎回異なる)
[3,5,4,9,2,7,8,6,1]

解答例

【問4】ボゴソートを実装してください。

具体的には以下のコードが動くようにしてください。

main = do
    xs <- shuffle [1..9]
    print xs
    print =<< bogosort xs
実行結果(毎回異なる)
[7,1,9,4,2,8,5,3,6]
[1,2,3,4,5,6,7,8,9]

ヒント: ソート済みかをTrue/Falseで返す関数isSortedを実装します。

解答例

unit

空のタプル()は値がないことを表しunitと読みます。

ignore _ = ()

main = do
    print $ ignore "foo"
実行結果
()

関数は必ず値を返す必要がありますが、返す値がないときには()を返します。C言語などで値を返さないvoid関数に相当します。

空のmain

何もしないmainを定義してみます。

main = return ()

mainはアクションとなる必要があるためreturn必須です。

終了ステータス

C言語ではmain()の戻り値がプロセスの終了ステータスとなります。

main.c
int main() {
    return 5;
}
シェルで確認
$ gcc main.c
$ ./a.out
$ echo $?
5

Haskellではmainから返されるアクションの値は捨てられるため、C言語のようにプロセスの終了ステータスとして扱われるわけではありません。

main.hs
main = return 5
シェルで確認
$ ghc main.hs
$ ./main
$ echo $?
0

runhaskellで実行するとmainから取り出された値は表示されますが、終了ステータスとして扱われるわけではありません。

シェルで確認
$ runhaskell main.hs
5
$ echo $?
0

終了ステータスを返すにはSystem.Exitモジュールを使います。

exit.hs
import System.Exit

main = do
    exitWith $ ExitFailure 5
シェルで確認
$ ghc exit.hs
$ ./exit
$ echo $?
5

print

今まで意識せずに使ってきたprintはアクションを返す関数です。文字が表示されるのは関数が呼ばれるタイミングではなく、関数が返したアクションから値を取り出すタイミングでの副作用です。

  • print: 関数
  • print "abc": 関数を呼んで、戻り値がアクション
  • let a = print "abc": 戻り値のアクションを束縛
  • _ <- print "abc": 戻り値のアクションから値を取り出し、副作用として文字を表示

確認します。まず値を取り出してみます。

main = do
    _ <- print "hello"
    return ()
実行結果
"hello"

取り出した値を表示して確認してみます。

main = do
    a <- print "hello"
    print a
実行結果
"hello"
()

()が取り出されます。()自体に値としての意味はなく、副作用として文字が表示されることに狙いがあります。

doブロックの末尾でreturnではなくprintを使うことができるのは、printがアクションを返すためです。

暗黙の取り出し

printから返されるアクションをletで束縛して、後で値の取り出しを試みます。

main = do
    let a = print "hello"
    _ <- a
    _ <- a
    a
    a
実行結果
"hello"
"hello"
"hello"
"hello"

<-により明示的に値を取り出さなくても、doの中にアクションを置くだけで自動的に値が取り出されます。これがアクション呼び出しの仕組みです。

再掲
import System.Random

randAlpha = randomRIO ('A', 'Z')

test = do
    r <- randAlpha
    print r

main = do
    test
    test
    test
実行結果(毎回異なる)
'S'
'P'
'T'

暗黙的に取り出された値は捨てられます。

練習

【問5】1~6の乱数を表示して返すアクションにより、暗黙の取り出しで値が捨てられていることを確認してください。

具体的には以下のコードが動くようにしてください。

main = do
    showDice
    showDice
    print =<< showDice
実行結果(毎回異なる)
5
2
6
6

解答例

bind

>>=は「アクションと、アクションを返す関数とを結びつける」ためbindと呼ばれます。ここでは向きに関係なく=<<も含めてbindとして扱います。

m_bind_f_m2.png
f_rbind_m_m2.png

bindの結合先がただの「関数」ではなく「アクションを返す関数」なのを確認します。

OK
inc x = return $ x + 1

main = do
    return 2 >>= inc >>= print
    print =<< inc =<< return 2
実行結果
3
3

アクションを返さない関数にはbindできないことを確認します。

NG
inc x = x + 1

main = do
    print $ inc =<< return 2
エラー内容
No instance for (Show (m0 b0)) arising from a use of `print'
The type variables `m0', `b0' are ambiguous
Possible fix: add a type signature that fixes these type variable(s)
(略・以下多数)

この仕様は、一度アクションに値を入れたら、その値を使った計算もアクションに閉じ込めることを目的としています。これにより副作用がアクションの中に閉じ込められ、アクションの外では参照透過性が保たれます。

<-はアクションの外に値を取り出しているように見えますが、doブロックの中でしか使えず、doブロックは最後にreturnなどでアクションを返すことから、結果としてアクションの中に閉じ込められたままです。

Applicativeスタイル

bindの結合先はアクションを返す関数という制限がありますが、アクションから値を外に出さないことが目的なら「自動的に入れてくれれば良い」と感じるかもしれません。

実際にそういう方法もサポートされていてApplicative(アプリカティブ)スタイルと呼ばれます。

applicative_style.png

import Control.Applicative

inc x   = x + 1
add x y = x + y

main = do
    print =<< inc <$> return 1
    print =<< add <$> return 1 <*> return 2
実行結果
2
3

<$>は次のような働きをします。

  1. アクションから値を取り出して関数に引数として渡す
  2. 関数の戻り値をアクションに閉じ込める

引数が複数あるときは<*>でつなぎます。

詳細は次の記事が参考になります。

まとめ

bind

  • アクション >>= 関数(アクションを返す)
  • 関数(アクションを返す) =<< アクション

Applicativeスタイル

  • 関数 <$> アクション
  • 関数 <$> アクション <*> アクション

どちらも最終的に得られるのはアクションです。

coffee2.png

※ この絵に深い意味はありません。

練習

【問6】Applicativeスタイルを使って、次のコードから<-を排除してください。

※ 具体的にはfib n ...の定義だけを修正してください。

fib 0 = return 0
fib 1 = return 1
fib n | n > 1 = do
    a <- fib (n - 2)
    b <- fib (n - 1)
    return $ a + b

main = do
    print =<< fib 6

ヒント: 演算子の関数化(+)

解答例

デバッグ

traceによるデバッグをputStrLnに書き替えて比較します。デバッグにはtraceを使った方が簡単ですが、デバッグ以外の理由でアクションを使うこともあるため、書き方の例として示します。

元となるデバッグなしのコードです。

fact 0 = 0
fact n | n > 0 = n * fact (n - 1)

main = do
    print $ fact 5
実行結果
120

traceで途中経過を表示します。

trace
import Debug.Trace

fact 0 = trace "fact 0 = 1" 1
fact n | n > 0 = trace dbg0 $ trace dbg1 ret
    where
        ret  = n * fn1
        fn1  = fact $ n - 1
        dbg0 = "fact " ++ show n ++ " = " ++
               show n ++ " * fact " ++ show (n - 1)
        dbg1 = dbg0 ++ " = " ++
               show n ++ " * " ++ show fn1 ++ " = " ++ show ret

main = do
    traceIO $ show $ fact 5
実行結果
fact 5 = 5 * fact 4
fact 4 = 4 * fact 3
fact 3 = 3 * fact 2
fact 2 = 2 * fact 1
fact 1 = 1 * fact 0
fact 0 = 1
fact 1 = 1 * fact 0 = 1 * 1 = 1
fact 2 = 2 * fact 1 = 2 * 1 = 2
fact 3 = 3 * fact 2 = 3 * 2 = 6
fact 4 = 4 * fact 3 = 4 * 6 = 24
fact 5 = 5 * fact 4 = 5 * 24 = 120
120

putStrLnで途中経過を表示します。printを使わないのは、文字列が""で囲まれて表示されるのを回避するためです。

アクション
fact 0 = do
    putStrLn "fact 0 = 1"
    return 1
fact n | n > 0 = do
    let dbg = "fact " ++ show n ++ " = " ++
              show n ++ " * fact " ++ show (n - 1)
    putStrLn dbg
    n' <- fact (n - 1)
    let ret = n * n'
    putStrLn $ dbg ++ " = " ++ show n ++ " * " ++ show n' ++ " = " ++ show ret
    return ret

main = do
    print =<< fact 5
実行結果
fact 5 = 5 * fact 4
fact 4 = 4 * fact 3
fact 3 = 3 * fact 2
fact 2 = 2 * fact 1
fact 1 = 1 * fact 0
fact 0 = 1
fact 1 = 1 * fact 0 = 1 * 1 = 1
fact 2 = 2 * fact 1 = 2 * 1 = 2
fact 3 = 3 * fact 2 = 3 * 2 = 6
fact 4 = 4 * fact 3 = 4 * 6 = 24
fact 5 = 5 * fact 4 = 5 * 24 = 120
120

アクションを使うと関数の戻り値もアクションにする必要があるため、戻り値の取り回しに影響します。

※ 手続型言語のようなコードになっています。

デバッグについての詳細は次の記事が参考になります。

練習

【問7】次のクイックソートの途中経過をtraceではなくputStrLnで表示するように修正してください。

import Debug.Trace

qsort []     = []
qsort (n:xs) = trace dbg $ qsort lt ++ [n] ++ qsort gteq
    where
        dbg  = "qsort " ++ show (n:xs) ++ " = qsort " ++
               show lt ++ " ++ " ++ show [n] ++ " ++ " ++ show gteq
        lt   = [x | x <- xs, x <  n]
        gteq = [x | x <- xs, x >= n]

main = do
    traceIO $ show $ qsort [4, 6, 9, 8, 3, 5, 1, 7, 2]

解答例

IORef

アクションによって値が変更できる変数のようなものです。

import Data.IORef

main = do
    a <- newIORef 1
    b <- readIORef a
    writeIORef a 2
    print =<< readIORef a
    print b
実行結果
2
1

aは変更可能な値への参照IORefです。アクションを通して値を読み書きします。

関数 引数 戻り値
newIORef 初期値 IORefを作成するアクション
readIORef 参照 値を読み取るアクション
writeIORef 参照→値 値を書き込むアクション

練習

【問8】次のJavaScriptでよく見掛けるサンプルコードを移植してください。

JavaScript
function counter() {
    var c = 0;
    return function() {
        return ++c;
    };
}

var f = counter();
console.log(f());
console.log(f());
console.log(f());

ヒント: return $ do

解答例

ループ

手続型言語ではループを頻繁に使います。

JavaScript
for (var i = 0; i < 5; ++i) {
    console.log(i);
}

IORefで移植すると大袈裟なコードになります。

import Data.IORef

main = do
    i <- newIORef 0
    let loop = do
        i' <- readIORef i
        if i' < 5
            then do
                print i'
                writeIORef i $ i' + 1
                loop
            else return ()
    loop
実行結果
0
1
2
3
4

ループカウンターを引数にすると簡単になります。

main = do
    let loop i | i < 5 = do
            print i
            loop $ i + 1
        loop _ = return ()
    loop 0
実行結果
0
1
2
3
4

※ ループを実現するにはforMという関数もありますが、活用するにはラムダが必要です。詳細は続編のHaskell アクションとラムダ 超入門で説明します。

練習

【問9】次のJavaScriptのコードを移植してください。sだけIORefを使ってください。

JavaScript
var s = 0;
for (var i = 1; i <= 100; ++i) {
    s += i;
}
console.log(s);

【問10】問9のコードからIORefを排除してください。sumは使わないでください。

解答例

IOUArray

IORefの配列版です。パッケージarrayが必要です。Leksahではdependenciesに追加します。

Uはアンボックス化を意味しています。詳細は続編のHaskell IOモナド 超入門で説明します。

import Data.Array.IO

main = do
    a <- newArray (0, 2) 0 :: IO (IOUArray Int Int)
    print =<< getElems a
    writeArray a 0 3
    print =<< readArray a 0
    writeArray a 1 6
    print =<< getElems a
    writeArray a 2 7
    print =<< getElems a
実行結果
[0,0,0]
3
[3,6,0]
[3,6,7]

aは値が変更可能な配列IOUArrayです。アクションを通して値を読み書きします。newArrayでは型を指定する必要があります。

  • IOUArray Int(インデックスの型) Int(値の型)
関数 引数 戻り値
newArray 範囲→初期値 配列を作成するアクション
readArray 配列→インデックス 値を読み取るアクション
writeArray 配列→インデックス→値 値を書き込むアクション
getElems 配列 すべての値をリストで取得するアクション

HaskellにはIOUArray以外にも配列があります。詳細は次のスライドが参考になります。

練習

【問11】次のJavaScriptで実装されたBrainf*ckインタプリタを移植してください。

JavaScript
var bf = ">+++++++++[<++++++++>-]<.>+++++++[<++++>" +
         "-]<+.+++++++..+++.[-]>++++++++[<++++>-]<" +
         ".>+++++++++++[<+++++>-]<.>++++++++[<+++>" +
         "-]<.+++.------.--------.[-]>++++++++[<++" +
         "++>-]<+.[-]++++++++++.";

var jmp = new Uint32Array(bf.length + 1);
for (var i = 0, loops = []; i < bf.length; ++i) {
    switch (bf[i]) {
    case '[':
        loops.push(i);
        break;
    case ']':
        var start = loops.pop();
        jmp[start] = i;
        jmp[i] = start;
        break;
    }
}

var pc = 0, r = 0, m = new Uint8Array(30000);
while (pc < bf.length) {
    switch (bf[pc]) {
    case '+': ++m[r]; break;
    case '-': --m[r]; break;
    case '>': ++r; break;
    case '<': --r; break;
    case '.':
        process.stdout.write(String.fromCharCode(m[r]));
        break;
    case '[':
        if (m[r] == 0) pc = jmp[pc];
        break;
    case ']':
        if (m[r] != 0) pc = jmp[pc] - 1;
        break;
    }
    ++pc;
}
実行結果
Hello World!

ヒント: putChar, Data.Word.Word8, fromIntegral

解答例

JavaScript内にハードコーディングされているBrainf*ckコードの出典です。

unsafePerformIO

アクションから値を引き剥がす関数です。<-=<<などを回避できます。

【注】参照透過性を破壊するため、特別な事情がない限り使わないでください。あくまで参考程度の紹介です。

デバッグで示したputStrLnによる例を書き替えます。factの戻り値がアクションではなくなっていることに注意してください。

非推奨
import System.IO.Unsafe

fact 0 = unsafePerformIO $ do
    putStrLn "fact 0 = 1"
    return 1
fact n | n > 0 = unsafePerformIO $ do
    let dbg = "fact " ++ show n ++ " = " ++
              show n ++ " * fact " ++ show (n - 1)
    putStrLn dbg
    let n' = fact (n - 1)
    let ret = n * n'
    putStrLn $ dbg ++ " = " ++ show n ++ " * " ++ show n' ++ " = " ++ show ret
    return ret

main = do
    print $ fact 5
実行結果
fact 5 = 5 * fact 4
fact 4 = 4 * fact 3
fact 3 = 3 * fact 2
fact 2 = 2 * fact 1
fact 1 = 1 * fact 0
fact 0 = 1
fact 1 = 1 * fact 0 = 1 * 1 = 1
fact 2 = 2 * fact 1 = 2 * 1 = 2
fact 3 = 3 * fact 2 = 3 * 2 = 6
fact 4 = 4 * fact 3 = 4 * 6 = 24
fact 5 = 5 * fact 4 = 5 * 24 = 120
120

このコードは次の記事を参考にしました。

何のためにunsafePerformIOが存在するかは、次の記事が参考になります。

練習

【問12】unsafePerformIOputStrLnを使って、traceの標準出力版関数trace'を実装してください。

具体的には以下のコードが動くようにしてください。

【注】ghciでは挙動が変わります。解答例はコンパイルしたときにのみ正常動作します。

fact 0 = trace' "fact 0 = 1" 1
fact n | n > 0 = trace' dbg0 $ trace' dbg1 ret
    where
        ret  = n * fn1
        fn1  = fact $ n - 1
        dbg0 = "fact " ++ show n ++ " = " ++
               show n ++ " * fact " ++ show (n - 1)
        dbg1 = dbg0 ++ " = " ++
               show n ++ " * " ++ show fn1 ++ " = " ++ show ret

main = do
    print $ fact 5
実行結果
fact 5 = 5 * fact 4
fact 4 = 4 * fact 3
fact 3 = 3 * fact 2
fact 2 = 2 * fact 1
fact 1 = 1 * fact 0
fact 0 = 1
fact 1 = 1 * fact 0 = 1 * 1 = 1
fact 2 = 2 * fact 1 = 2 * 1 = 2
fact 3 = 3 * fact 2 = 3 * 2 = 6
fact 4 = 4 * fact 3 = 4 * 6 = 24
fact 5 = 5 * fact 4 = 5 * 24 = 120
120

解答例

参考

アクションについての詳細は次の記事が参考になります。

169
136
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
169
136

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?