stimulant and competitive programming (original) (raw)
Foldableとは?
foldr
やfoldl
ができる型クラス。
class Foldable (t :: Type -> Type) where
というように宣言されている。
https://hackage.haskell.org/package/base-4.20.0.1/docs/Data-Foldable.html
ちなみに t :: Type -> Type
というのは単に型t a
と書けるようなt
ということを明示しているだけらしい。昔はclass Monad m where
と書いてメソッドでm a
と書いていたが、確かにこれだとダックタイピング的な気がする。
Foldable の実装
foldMap
かfoldr
のどっちかのメソッドを定義するだけでいい。それらを実装できるということが具体的にどんな条件なのかはfoldMap
で考えればわかりやすい。
foldMap :: Monoid m => (a -> m) -> t a -> m foldMap f t = ...
という定義から分かるように、コンテナt
にfoldMap
を実装するためには、t a
からa
をモノイドへの変換関数f
に渡せて、その戻り値を(複数ある場合は)<>
で右結合的に合成できる必要がある(foldMap
は右結合)。ここで、a
をモノイドへの変換関数に渡せるなら普通は(a
を受け取れる)任意の関数にも渡せるだろうし、その結果を<>
で合成できるなら任意の二項演算子でも合成できるはずなので、foldMap
を実装するのは以下のfoldMapBy
を実装するのと同じと言える。
foldMapBy :: (a -> b) -> (b -> b -> b) -> t a foldMapBy f op t = ...
これを使ってfoldMap
は次のように書ける。
foldMap f t = foldMapBy f (<>) t
ちなみにt a
からa
を関数に渡せさえすればいいというのはファンクターと似ているが、関数の結果をt
に入れ直さなくてもいい。コンテナをリストで考えれば、a
は複数個あるのでそれぞれに関数を適用したあと情報を失わず保存するには再びそれらをリストに入れればいい。したがってリストからa
を取り出す操作はファンクターが相応しいわけだが、もし関数適用した後でa
をすぐにまとめ上げてしまうならそれはそれで情報は保たれる。この自然(?)な構造がFoldableということなのだろう。
ところで要素の順序が上手く定まらないコンテナでは右結合とは?という話になるように思うが、Foldableは順序について特に口出ししていないようなので、とにかくfoldMap
を定義で来たなら、そこで<>
の評価されていく順序が要素の右から左に並べた順序ということなんだろうと思う。foldr
を実装した場合も同じことで、foldl
等のデフォルト実装は単にfoldMap
(foldr
)の逆向き操作として定義されているので、そうやって勝手に順序を定義しても矛盾はないと思う。
foldMap と foldr
foldr
のデフォルト実装は
foldr :: (a -> b -> b) -> b -> t a -> b foldr f z t = appEndo (foldMap (Endo #. f) t) z
となっている。
Endo
というのはEndo
型のデータコンストラクタで、Endo
型というのはa -> a
な関数を関数合成.
を二項演算子としてモノイドにするためラッパー。#.
は.
と同じもの。なぜこんな定義なのかというと、例えば、foldr
f 0 [1,2,3] == f 1 (f 2 (f 3 0)だが、これは
*1 0と同じなので、
<>として関数合成が使えれば
foldMapで
foldr`が実装できる。
ちなみに、逆にfoldr
を使ったfoldMap
のデフォルト実装は
foldMap f = foldr (mappend . f) mempty
と直感的なものになっている。
二つのデフォルト実装の感覚的な違いは、二つの関数を素朴に実装できる条件に由来しているように思う。
foldr
を素朴に実装する場合、foldr
に与える二項演算子f
の中で要素とそれまでの(f
による)畳み込みの結果を同時に受け取れる必要がある。これは例えば、合成と関数適用が別々のインターフェイスで、かつ関数適用がmap的な方法でしか行えないコンテナなら難しい。一方でfoldMap
はそのコンテナで素朴に実装できるし、foldr
を素朴に実装するための要素とそれまでの(f
による)畳み込みの結果を同時に受け取れるインターフェイスを使っても素朴に実装できる。
もちろん今言ったようなコンテナでも、関数適用用のインターフェイスで部分適用をして、合成用インターフェイスで関数合成すれば、foldr
は実装できる。したがって両者は同値なのだが、それはラムダ計算とHaskellが同値というような意味だろうと思う。
Endo の stimes
Endo
のソースを見ていると、Endo
のstimes
が特殊化されていた。stimes
というのはstimes n x
という関数で、Semigroup
であるx
をn
個<>
で合成させるSemigroup
のメソッド。デフォルト実装は単純に考えるなら<>
を再帰で繋げるものだが、その場合、Endo
のstimes
はEndo
の中身である関数f
をf . f . f ...
という風に関数合成するのとほぼ同じことになる。しかし実際にはf
を呼びながらn
回回る再帰関数を作ってそれを返すというようなコードになってて、いわば一種のインライン展開みたいな最適化をしている。しかも、再帰関数を返すだけなのでstimes
自体の計算量はO(1)になる。
この特殊化はこのissueが元になっていて、そのissueは「stimes
が作るツリーはEndo
にとっては最適とは言えない」というような発言から始まっている。最初は何のことかと思ったが、実はstimes
のデフォルト実装はバイナリ法的に実装されている。ここでいうバイナリ法というのは累乗modを高速に計算するテクニックで、要は a^8
を計算するのに a^2 = a * a ->
a4 = a2 * a2 -> a8 = a4 * a4というような高速化だ(ちょっと工夫すれば2冪じゃなくても使える)。この高速化は任意の半群上の累乗(演算子を乗法とみなした時の累乗)でも使えるので、
stimesも使っているというわけだ。これによって
n個の合成をO(logN)で行えるわけだが、この高速化を
Endoに使っても、
Endoの中身の関数は結局
n`回呼ぶ必要があるので、合成がO(1)で終わる上の特殊化の方がいい。
ちなみにstimes
のデフォルト実装の威力を試したコード。
import Data.Semigroup data PM = PM Integer Integer deriving Show
instance Semigroup PM where
(PM m x) <> (PM _ y) = PM m ((x * y) mod
m)
powmod x n m = let (PM _ y) = stimes n $ PM m x in y main = do print $ powmod 123 12345678910 1234567
foldrM
foldrM
はControl.Monad
にあるfoldM
の右結合版だが、
import Data.Foldable import Control.Monad.Identity
main = do print $ take 10 $ runIdentity $ foldrM (\x xs -> Identity (x : xs)) [] [1..]
としても終了しない。Identity
モナドを使うなら単にfoldr
を使うのと変わらないのだが、foldrM
はfoldl
を使って書かれているので無限リストを評価してしまうからだ。実際に以下のようにfoldr
で実装すると終了する。
import Control.Monad.Identity
foldrM f a [] = return a foldrM f a (x:xs) = foldrM f a xs >>= f x
main = do print $ take 10 $ runIdentity $ foldrM (\x xs -> Identity (x : xs)) [] [1..]
foldrM
がfold
を使う理由は foldr/build が関係しているらしいが、インライン展開云々という難しい話になるので理解できなかった。
*1:f 1) . (f 2) . (f 3