読者です 読者をやめる 読者になる 読者になる

無駄と文化

実用的ブログ

Haskellで目指せワンライナー! 〜 1行コードでがんばる 〜

Haskell

これはHaskellアドベントカレンダー2015の(2日遅れの)8日目の記事です。
本当にすみませんでした。


導入

小さな部品(関数)を組み合わせて大きなプログラム(関数)を作り上げる、これこそが関数型の醍醐味かなと思います。
しかもHaskellは標準関数も豊富で、それらを糊付けする手段も豊富です。上手くやればプログラム全体を1行で書き上げてしまうことも可能なんじゃないでしょうか。夢がありますよね。

そんなわけで、Haskellにおいて1行(ワンライナー)コードを書くための指針をまとめます。


メリット

まずはワンライナーのメリットをおさらいです。

  1. いちいち関数名や変数名を考えずに済む
  2. ワンライナーコードは他のワンライナーのパーツとして容易に組み込める

1.について、 いちいち細かな関数に分けて別々に記述していると、それだけ名前を考える必要がありますね。
正直、英語よく分からないし、名前を考えてる時間って無駄じゃないですか?

なので名前つけるの辞めましょう。
例えば、文字列の頭と末尾の空白を取り除く関数だったら、

reverse . dropWhile (' '==) . reverse . dropWhile (' '==)

これ、
これをコピペして使えばOK。名前は知らない^1


2.について、 ワンライナーコードはそのままコピペしてあらゆる場所にそのままぶち込めますからね。コピペプログラマにはそれがいちばん助かるんですよ。


心構え

そんなわけで、あらゆる関数を1行で書き綴るための心構えです。

  1. ポイントフリースタイルでがんばる
  2. do構文はあきらめる ((<$>), (<*>), (>>=) でがんばる)
  3. パターンマッチはあきらめる (リスト内包表記でがんばる)
  4. 再帰はあきらめる

なお、ブレース{} やセミコロン; で適切に区切ることで、do構文などを見た目上1行で書くことも出来るのですが、このやり方はダサいのでご法度とします。


1. ポイントフリースタイルでがんばる

まず、仮引数は悪だと思いましょう。

f x = g (h x)

このx、名付けに悩みません?
a にしようか? n の方が適切じゃない?とかいろいろ考えちゃいますよね。

悩みの元は消し去りましょう。関数合成(.) を使って、

f = g . h

こう。


その他、(.)flip を駆使すればかなりの場合でポイントフリースタイルを実現できるはずです。

よくあるパターンを挙げると、

f x y z = g z y x
-- is eq to
f = (flip .) . flip . (flip .) $ g


f x y = g (h x y)
-- is eq to
f = (g .) . h

みたいな。


その他、SKIコンビネータの理論を持ち出せば、パターンマッチを含まない関数定義は s = \x y z -> x z (y z)k = \x y -> x = const を上手く使うことでポイントフリーにできるはずです。

ま、それも面倒なときはテキトーな仮引数を振って無名関数でお茶を濁しますけど。


2. do構文はあきらめる

行圧縮を志すならば、do構文は疑ってかかるべきだと思います。
do構文なんて、改行必須だわインデント揃えなくちゃいけないわで場所取ってしょうがない。

do構文を使わずとも(<$>)(<*>)(>>=) でなんとかなりますよ。

-- Monad 覚えたてでよくやるダメパターン
f = do x <- m
       return $ g x

-- Functor で充分
f = g <$> m

これこそ絶対に do するべきじゃないパターン。


では、値を引き出すべきモナドが複数に増えたら?

-- g が多引数関数なら、
f = do x1 <- m1
       x2 <- m2
       x3 <- m3
       return $ g x1 x2 x3

-- ApplicativeFunctor の出番ですね
f = g <$> m1 <*> m2 <*> m3

ApplicativeFunctor の本領発揮ですね。


では途中で評価はするけど値を引き出さないモナドが出てきたら?

f = do x1 <- m1
       m2        -- m2 を評価するけど値は使わない
       x3 <- m3
       return g x1 x2 x3

-- (<*) を使えば上手く値を捨ててくれます
f = g <$> m1 <* m2 <*> m3

m1 <* m2 = const <$> m1 <*> m2 なのでうまい具合に値を捨ててくれます。

対になる演算子として (*>) があり、左側のモナドの中身を捨ててくれるので、上手く使えば、

f = do m1        -- 一個め使わない
       x2 <- m2
       m3        -- 三個めも使わない
       return x2

-- is eq to
f = m1 *> m2 <* m3

みたいな感じで、コードが劇的に短くなりますね。


では、g 自体がモナディックな関数(モナドを返す関数)だったらどうでしょう?

f = do x1 <- m1
       x2 <- m2
       x3 <- m3
       g x1 x2 x3  -- g が Monad を返すので return は不要

-- ApplicativeFunctor として合成した後に join すればOkay
f = join $ g <$> m1 <*> m2 <*> m3

いい感じですね。
Monad だからってとりあえず do するのは辞めましょう。これはわりと真面目に。


3. パターンマッチはあきらめる

パターンマッチもあきらめよう。

パターンマッチが使える場所は

  • 関数定義の左辺
  • do構文のbindの中
  • case式

とかだと思うんですが、ほら、どれも改行で区切らないといけないんで。


とはいえ、パターンマッチがHaskellを特徴づける強力な機能の一つであることも認めます。特に代数的データ型に包まれた値を取り出すときとかに。シンプルかつ明瞭かつ強力。
なので当社では、パターンマッチをあきらめきれないお客様にリスト内包表記を使ってパターンマッチを1行で書く方法をおすすめしています。

例えば、Either を使ったこんな関数

f :: Show (a, b) => Either a b -> String
f (Left x)  = "left: " ++ show x
f (Right y) = "right: " ++ show y

これを、(either を使わずに)1行で書くと、

f = \z -> listToMaybe $ ["left: " ++ show x | Left x <- [z]] ++ ["right: " ++ show y | Right y <- [z]]

なんと、どのパターンにもマッチしない時には Nothing を返してくれるという安全設計。

リスト内包表記の中ではパターンマッチが使え、かつ、マッチしなかった場合でもエラーにならずフィルターされるだけという仕様だからこそ成せる技です。


一般化すると、

f (Ptn1 x) = Just $ g1 x
f (Ptn2 x) = Just $ g2 x
...
f (PtnN x) = Just $ gN x
f _        = Nothing

-- is eq to
f = \z -> listToMaybe $ [g1 x | Ptn1 x <- [z]] ++ [g2 x | Ptn2 x <- [z]] ++ ... ++ [gN x | PtnN x <- [z]]

いいですね。


まぁ、この例なら、

f = either (("left: "++) . show) (("right: "++) . show)

というように either を使えば一撃ですが。


4. 再帰はあきらめる

ちょっともう手に負えなくなりつつあるので端折りますが、
List や Tree などの Foldable なデータ型上での再帰なら fold でがんばりましょう。
それでもダメなら fix で無名再帰する感じで。


まとめ

名前付けるの面倒でワンライナーをドカッと詰め込んじゃうこと、自分はあります。
既存のコードを1行で書きなおすのは頭の体操にもなりますしねー。


私からは以上です。