『すごいHaskellたのしく学ぼう!』を読み進めていたら、再帰関数のところでいきなり躓きました。
最初に出てきたのがこのコードです。
replicate' :: Int -> a -> [a]
replicate' n x
| n <= 0 = []
| otherwise = x : replicate' (n-1) x
まず x : の書き方がよく分かりませんでした。ChatGPTに聞いたら、リストの先頭に要素を一個くっつける演算子とのこと。うわー、そうだった。なるほど、こういうときに使うのか、という感じです。大事なポイントとして「左側は要素、右側はリストじゃないといけません」と教わりました。
分かったつもりで本を進めようとしたら、その次の一文でまた止まります。「負のケースに対応するために、ここではパターンではなくガードを使っています。」……どういうこと?
負の数を考えなければ、こう書けるみたいです。
replicate' :: Int -> a -> [a]
replicate' 0 x = []
replicate' n x = x : replicate' (n-1) x
なるほど、パターンマッチでも書ける。けど、条件(<=みたいなの)が付けられないと。
でもどっちみちガードの方が見やすいな、と思いました。
再帰はどこで止まるのか
ここで一番こんがらがったのが、「この再帰、どこで止まるんだ?」というところでした。
あ、そっか。n <= 0 があるから、再帰的に呼び出していっても最後に0になったときに止まるのか……。いや、ん?そもそも再帰が止まる条件って何だっけ?
しばらく考えて、ようやく腑に落ちました。再帰は何かのスイッチで止まるというより、自分の中で自分を呼び出さずに [] を返すから止まるんですね。
そう気づいてから、このコードがすごく分かりやすく見えてきました。
-- 1. 止まる条件
| n <= 0 = []
-- 2. 止まる条件に近づく呼び出し
| otherwise = x : replicate' (n-1) x
「止まる条件」と「そこに近づく変化」のセットで考えると、再帰の怖さが少し減りました。
ガードとパターンマッチが一緒に出てきた
次に出てきたのがこの関数です。
take' :: Int -> [a] -> [a]
take' n _
| n <= 0 = []
take' _ [] = []
take' n (x:xs) = x : take' (n-1) xs
突然ガードとパターンマッチが一緒に出てきて、ビックリしました。こんな書き方もありなんだ。
でも、さっきの「止まる条件」と「変化」のセットで分けて考えたら、なんとか分かった気がします。
take' n _
| n <= 0 = []
take' _ [] = []
これが止まる条件(2つ)で、
take' n (x:xs) = x : take' (n-1) xs
これが変化、ということなんだと思います。とはいえ、こんなの自分で考えつける気がしないんですが、慣れてくるもんなんでしょうか。
ちなみに、この「止まる条件」のことを「基底部」と呼ぶらしいです。ちょっと前のページに書いてあったみたいですが、そのときは頭がついていってませんでした。
そして結局、再帰関数は頭がこんがらがって面倒くさくなってしまったので、最後のクイックソートのところだけ写経して終わりました。また気が向いたら読み返そうと思います。