AtCoder 用 Haskell 逆引き (失敗編)
この記事は
Haskell 入門者あるあるをまとめたものです。
予備知識
- TLE: time limit exceed (時間制限超過)
 - MLE: memory limit exceed (メモリ制限超過)
 
芝刈り・しばかれ
前提の import です:
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
それではあるあるを挙げていきます。
1. HLS が動かない
エラー表示も、定義ジャンプも、型名表示すら無いままコンテストに挑みます。ひもじい気分を味わいます。
2. リストへのランダムアクセスで TLE
連結リストへのランダムアクセスは 
xs !! i
xs VU.! i
3. 永遠に処理が戻ってこない (タイポ再帰)
Haskell では遅延評価ができます。再帰的な変数を作ってしまうと、制御が戻らなくなることがあります。
let xs = 3 : xs
 in 0 : xs
let ys = 3 : xs
 in 0 : ys
4. 永遠に処理が戻ってこない (無限リスト)
無限リストをフィルタリングすると無限リストが返ってきます。どこかで切り落としましょう。
filter (<= boundary) [1..]
takeWhile (<= boundary) [1..]
 5. Vector.Unboxed で 2 次元配列が持てない
型が合わないときは謎のコンパイルエラーを食らいます。エラーメッセージの癖を掴みましょう。
mat <- VU.replicateM n getLineIntVec
mat <- V.replicateM n getLineIntVec
 6. UArray でタプルが持てない
同上です。逆に Vector.Unboxed.Vector でタプルが持てるのは、内部的に Vector 2 本になるためです。
table <- newArray @IOUArray bounds_ (0, 0)
table <- newArray @IOArray bounds_ (0, 0)
7. Boxing により MLE
テーブルサイズの大きな DP で起きました。 Unboxed なデータ型を使いましょう。また DP では遅延評価に頼らないようにしましょう。
table <- newArray @IOArray bounds_ e0
table <- newArray @IOUArray bounds_ e0
 8. 言語拡張 NPlusKPatterns でエラー
言語拡張を有効にして ghci を起動します:
$ ghci -XNPlusKPatterns
Prelude> 
一見 N + K パタンは問題なく動きます:
Prelude> let x = 1
Prelude> let (n + 1) = x
Prelude> n
0
しかし n が負の数になるとエラーが発生します:
Prelude> let x = 0
Prelude> let (n + 1) = x
Prelude> n
*** Exception: <interactive>:2:5-15: Non-exhaustive patterns in n+1
使わないほうが無難かもしれません。
 9. n 回ループで iterate を使ってTLE
問題です。次のループ処理のうち、 TLE の危機に有るコードはどれですか?
(!! n) $ iterate f s0snd $ until ((== n) . fst) (bimap succ f) (0, s0)foldl' (\x _ -> f x) s0 [1 .. n]VU.foldl' (\x _ -> f x) s0 (VU.replicate n False)
PAST #8 I では以下となりました。圧倒的に iterate が遅いです:
- 
iterate: TLE - 
until: 684 ms - 
foldl': 632 ms - 
VU.foldl': 626 ms 
戒めにテンプレートを追加しました:
-- | Runs the given function `n` times.
times :: Int -> (a -> a) -> a -> a
times !n !f !s0 = snd $ until ((== n) . fst) (bimap succ f) (0 :: Int, s0)
他にも 3 次元の range が遅かったりします。
 10. Unbox を実装できない
昔の vector は Unbox の実装が異様に難しいです。自力で実装するよりも、他のパッケージを頼るのが無難です。
- 
vectorの代わりに unboxing-vector を使う
Sum type に対してはUnboxableを実装できない 点は留意します。 - 
vector-th-unbox を使う
Template Haskell でコード生成します。 
-- vector-th-unbox: https://www.stackage.org/lts-16.11/package/vector-th-unbox-0.2.1.7
import Data.Vector.Unboxed.Deriving (derivingUnbox)
newtype UnionFind = UnionFind (VU.Vector UFNode)
-- | `Child parent | Root size`
data UFNode = UFChild {-# UNPACK #-} !Int | UFRoot {-# UNPACK #-} !Int
_ufrepr1 :: UFNode -> (Bool, Int)
_ufrepr1 (UFChild x) = (True, x)
_ufrepr1 (UFRoot x) = (False, x)
_ufrepr2 :: (Bool, Int) -> UFNode
_ufrepr2 (True, x) = UFChild x
_ufrepr2 (False, x) = UFRoot x
derivingUnbox "UFNode" [t|UFNode -> (Bool, Int)|] [|_ufrepr1|] [|_ufrepr2|]
終わりに
自力で Haskell が書けなくて挫折感を味わいました。今でも型関連のエラーが直せないときは質問します。 AtCoder に絞っても相当難しい言語なので、完全な習得には年単位で時間が必要なのではないかと思います。
その分だけ得るものは多いと感じています。快適な AC を!
Discussion