Haskellではアクションと呼ばれる機能により副作用が扱えます。アクションの使い方の初歩を説明します。ライブラリで用意されたアクションを手っ取り早く使うことを目的としているため、モナドや圏論には言及しません。
シリーズの記事です。
- Haskell 超入門
- Haskell 代数的データ型 超入門
- Haskell アクション 超入門 ← この記事
- Haskell ラムダ 超入門
- Haskell アクションとラムダ 超入門
- Haskell IOモナド 超入門
- Haskell リストモナド 超入門
- Haskell Maybeモナド 超入門
- Haskell 状態系モナド 超入門
- Haskell モナド変換子 超入門
- Haskell 例外処理 超入門
- Haskell 構文解析 超入門
- 【予定】Haskell 継続モナド 超入門
- 【予定】Haskell 型クラス 超入門
- 【予定】Haskell モナドとゆかいな仲間たち
- 【予定】Haskell Freeモナド 超入門
- 【予定】Haskell Operationalモナド 超入門
- 【予定】Haskell Effモナド 超入門
- 【予定】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'
アクションは使用方法も関数とは異なり、見慣れない演算子が出て来ます。それらを説明します。
<-
アクションから値を取り出すには<-
を使います。矢印(←)をイメージした記号です。
main = do
r <- randAlpha
print r
アクションに関連付けられた処理(この場合は乱数生成)は<-
で値を取り出す際に行われます。
注意点
<-
はdo
ブロックの中でしか使えません。
main =
r <- randAlpha
print r
parse error on input `<-'
※ do
なしで<-
相当の処理を書くことは可能ですが、ラムダについて説明する必要があります。詳細は続編のHaskell アクションとラムダ 超入門で説明します。
=<<
一時変数を経由せずにアクションから値を取り出そうとしてもうまくいきません。
test = do
print randAlpha -- エラー
アクションから値を取り出して関数に渡すには専用の演算子=<<
を使用します。他の言語では見ない形ですが、直感的には漏斗のようなものでアクションから関数に値をドリップしているとイメージすれば良いでしょう。
main = do
print =<< randAlpha
アクション版の$
だと捉えておけば良いでしょう。
イメージ
C++をご存知の方は、iostreamの<<
をイメージすれば良いかもしれません。
cout << "abc";
>>=
逆向きもあります。>>=
はbindと呼ばれます。
main = do
randAlpha >>= print
説明の都合上=<<
を先に出しましたが、実は>>=
が基本で、=<<
はその逆です。
※ do
と>>=
には密接な関係があります。詳細は続編のHaskell アクションとラムダ 超入門で説明します。
let
let
でアクションから値を取り出すことはできません。
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
は値を入れてアクションを作る関数です。
アクションを作って、すぐに値を取り出してみます。
main = do
a <- return 1
print a
print =<< return 2
return 3 >>= print
1
2
3
繰り返しますが、let
では値を取り出すことはできません。
main = do
let a = return 1
print a -- エラー
束縛されたアクションから、アクション用の演算子で値を取り出します。
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()
の戻り値がプロセスの終了ステータスとなります。
int main() {
return 5;
}
$ gcc main.c
$ ./a.out
$ echo $?
5
Haskellではmain
から返されるアクションの値は捨てられるため、C言語のようにプロセスの終了ステータスとして扱われるわけではありません。
main = return 5
$ ghc main.hs
$ ./main
$ echo $?
0
runhaskellで実行するとmain
から取り出された値は表示されますが、終了ステータスとして扱われるわけではありません。
$ runhaskell main.hs
5
$ echo $?
0
終了ステータスを返すにはSystem.Exitモジュールを使います。
import System.Exit
main = do
exitWith $ ExitFailure 5
$ ghc exit.hs
$ ./exit
$ echo $?
5
今まで意識せずに使ってきた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として扱います。
bindの結合先がただの「関数」ではなく「アクションを返す関数」なのを確認します。
inc x = return $ x + 1
main = do
return 2 >>= inc >>= print
print =<< inc =<< return 2
3
3
アクションを返さない関数にはbindできないことを確認します。
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(アプリカティブ)スタイルと呼ばれます。
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
<$>
は次のような働きをします。
- アクションから値を取り出して関数に引数として渡す
- 関数の戻り値をアクションに閉じ込める
引数が複数あるときは<*>
でつなぎます。
詳細は次の記事が参考になります。
- @kazu_yamamoto: Applicativeのススメ - あどけない話 2010.12.11
まとめ
bind
- アクション
>>=
関数(アクションを返す) - 関数(アクションを返す)
=<<
アクション
Applicativeスタイル
- 関数
<$>
アクション - 関数
<$>
アクション<*>
アクション
どちらも最終的に得られるのはアクションです。
※ この絵に深い意味はありません。
練習
【問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
で途中経過を表示します。
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
アクションを使うと関数の戻り値もアクションにする必要があるため、戻り値の取り回しに影響します。
※ 手続型言語のようなコードになっています。
デバッグについての詳細は次の記事が参考になります。
- @kazu_yamamoto: Haskell でのデバッグ - あどけない話 2012.6.6
練習
【問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でよく見掛けるサンプルコードを移植してください。
function counter() {
var c = 0;
return function() {
return ++c;
};
}
var f = counter();
console.log(f());
console.log(f());
console.log(f());
ヒント: return $ do
⇒ 解答例
ループ
手続型言語ではループを頻繁に使います。
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
を使ってください。
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インタプリタを移植してください。
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】unsafePerformIO
とputStrLn
を使って、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
⇒ 解答例
参考
アクションについての詳細は次の記事が参考になります。