例えば2次元配列を扱う際に計算中は1次元に展開して持っておいて、出力時に戻そうとしたときに、リストを固定の長さで分割して二重(2次元)リストにして返す、ということがしたかった。
実装手順
ライブラリ関数でできあいのものがあるかわからなかったので、自作する。
自分で再帰する
まずはナイーブに、自分で再帰させて実装する:
splitByWidth1 :: Int -> [a] -> [[a]] |
splitAtで分割し、残りがあれば再帰させてリストを作成する。
テスト:
main :: IO () |
unfoldrを使う
自分で再帰するのは明らかに面倒なので、unfoldrを使ってみる。
unfoldrはMaybeを返す関数を渡して、Nothingが返るまでのJustの値をリストに展開してくれる:
import Data.List (unfoldr) |
Justにはその段の値と、続けるための情報というペアを返すsplitAtの結果がうまくそのまま使える形になっていた
if-elseをやめる
自分でif判定するのはまどろっこしい。
Bool値が真のときにはJust aを、偽のときにはNothingを返すwhenMaybeというユーティリティ関数を作って:
import Data.Bool (bool) |
Justの場合は中身に関数を適用する、という具合にしたい。
<$> でまさにそういうことができる:
splitByWidth3 w = unfoldr (\ss -> splitAt w <$> whenMaybe (not $ null ss) ss) |
- 動作の説明:
ssがnot $ nullならJust ssとなり、関数も適用されてJust (splitAt w ss)となるssがnot $ nullじゃなければ、Nothingでunfoldr終了となる
- 「
<$>はfmapの中置記法シノニム」とのことで、型シグネチャは(<$>) :: Functor f => (a -> b) -> f a -> f b fmapはFunctor型クラスのメソッドで、Functorのインスタンスに関数を適用させられるFunctorであるMaybeは、Justの中身に関数(splitAt w)が適用される(Nothingはそのまま)
引数を2回適用してるのを回避したい
ラムダ式の引数 ss を2ヶ所(not $ null と whenMaybe)に渡す必要があるのをなんかうまいことやりたい。
not . nullをf、whenMaybeの引数の順序を入れ替えたflip whenMaybeをgとすると、g x (f x)という形になっている。
こういう時は、<*> を使って(g <*> f) xと書けるらしい:
splitByWidth4 w = unfoldr (\ss -> splitAt w <$> (flip whenMaybe <*> not . null) ss) |
(<*>) :: Applicative f => f (a -> b) -> f a -> f b- haskell - Is there a standard function that computes
f x (g x)? - Stack OverflowApplicative fのfが((->) r)という関数適用に相当して?型シグネチャを変形すると((->) r) (a -> b) -> ((->) r) a -> ((->) r) b≡(r -> a -> b) -> (r -> a) -> (r -> b)という型に相当する、とかApplicative ((->) a)の<*>のソース:(<*>) f g x = f x (g x)実装が2回適用するようになっている
関数合成する
ラムダ式の引数が最後に渡すだけになったので、関数合成してポイントフリースタイルにしてやる:
splitByWidth5 w = unfoldr $ (splitAt w <$>) . (flip whenMaybe <*> not . null) |
flipをなくす
VSCodeで修正のサジェストされて、flip <*> を =<< に置き換える:
splitByWidth6 w = unfoldr $ (splitAt w <$>) . (whenMaybe =<< not . null) |
(=<<) :: Monad m => (a -> m b) -> m a -> m b- バインド
>>=の左右逆とのこと
ひとまず完。
わかりやすい? わかりにくい?
最後の splitByWidth6 を見るとパッと見、簡潔ですっきりしてるので、わかりやすい気もする。
しかし <$> などがどうやって動くのかを知っている必要がある。
するとファンクターがどうとかアプリカティブがどうとかいう話を掘っていく必要が出てくる。
splitByWidth6 を見て動作を理解できるか?
わかってる人が見たら簡潔で理解しやすいのかもしれないが、自分には無理だね…。
プログラミングするのにどれだけのことが一般的に理解できるものとしていいものなんだろうか? 自分からすると圏論はわからんしHaskellでどう活きてるのかも理解してない。 そこが普通理解できるものとして要求されるとなると厳しい。 ただベタに書くんだったら別にHaskellを使う必要もなくなってしまう。
圏論を理解したいと思って本を読んだりするんだけど、ムズいスね。
リンク
- 圏論とプログラミング - mrsekut-p
- 関手とFunctor - mrsekut-p を見ると、
Functor、fmapがどういうことかちょっとわかるような気がする
- 関手とFunctor - mrsekut-p を見ると、
- haskell - Is there a standard function that computes
f x (g x)? - Stack Overflow
動画
後日談:もっと簡単な方法
- Grouping a list into lists of n elements in Haskell - Stack Overflow
takeWhile (not . null) . unfoldr (Just . splitAt n)- 無限リストを
takeWhileで取り出せばよかったか…unfoldrで止めなきゃ、と思い込んでた… chunksOfsplitモジュールに用意されてた