例えば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
で止めなきゃ、と思い込んでた… chunksOf
split
モジュールに用意されてた