Haskell で yes コマンドを実装した

皆さんは `yes` コマンドをご存知でしょうか。私は知りませんでした、以下の記事を読むまでは。

* [Unixコマンド”yes”についてのちょっとした話 | コンピュータサイエンス | POSTD](http://postd.cc/a-little-story-about-the-yes-unix-command/)

上記記事は 10 分程度で読めて、かつとてもおもしろいので是非読んで欲しいのですが、忙しい諸兄姉のために 3 行でまとめます。

* `yes` という y を標準出力するだけのコマンドがあるよ
* 1979 年から現在まで活発に開発が続けられているよ
* Rust で実装したらめっちゃ速くなったよ

これを読んで、「Haskell ならどれぐらいのスループットが出るんだろう」と思ったので実装してみました。

## 組み込みの `yes`

まずは組み込みの `yes` コマンドが [Mac Book Pro 2015 年モデル](https://www.apple.com/jp/macbook-pro/specs-2015/)でどれ位のものなのか見てみます。

```
$ yes | pv -r > /dev/null
[40.6MiB/s]
```

なるほど。

## 普通の Haskell 実装

では普通に Haskell で実装した場合どれぐらいでしょうか。

```Main.hs
module Main where

main :: IO ()
main = loop
 where
  loop = putStrLn "y" >> loop
```

これを実行してみます。

```
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[10.9MiB/s]
```

なるほど。Python の 2 倍ぐらいは出てますかね。

## 最適化オプション

stack で作成したテンプレートには最適化オプションがついていないので付けてみます。ついでに不要なオプションは削除します。

```diff
diff --git a/yes.cabal b/yes.cabal
index 5b3fd34..a780153 100644
--- a/yes.cabal
+++ b/yes.cabal
@@ -16,7 +16,7 @@ cabal-version:       >=1.10
 executable yes
   hs-source-dirs:      app
   main-is:             Main.hs
-  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  ghc-options:         -O2
   build-depends:       base
   default-language:    Haskell2010
```

上記変更を加えた状態で実行した結果です。

```
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[10.9MiB/s]
```

何も変わりませんでした。

### ByteString を使用する

Haskell の String は遅いので ByteString を使用します。

```Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Data.ByteString.Char8 as BC8

main :: IO ()
main = loop
 where
  loop = BC8.putStrLn "y" >> loop
```

この結果は以下の通りです。

```
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[23.3MiB/s]
```

倍以上のスループットが出るようになりました。

### 低レベル I/O

`putStrLn` ではなく `hPutStrLn` を使用してみます。

```Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Data.ByteString.Char8 as BC8
import System.IO

main :: IO ()
main = loop
 where
  loop = BC8.hPutStrLn stdout "y" >> loop
```

この結果は以下のとおりです。

```
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[23.7MiB/s]
```

若干スループットが上がったようですが、誤差の範囲です。

### 再起をやめる

戦略を変更し再起をやめてみます。

```Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Data.ByteString.Char8 as BC8
import System.IO

main :: IO ()
main = mapM_ (BC8.hPutStrLn stdout) (repeat "y")
```

この結果は以下のとおりです。

```
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[23.7MiB/s]
```

まったく変わりませんでした。

### 更に低レベル I/O

`hPutStrLn` ではなく`hPut` を使用してみます。

```Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Data.ByteString.Char8 as BC8
import System.IO

main :: IO ()
main = mapM_ (BC8.hPut stdout) (repeat "y\n")
```

この結果は以下のとおりです。

```
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[30.4MiB/s]
```

スループットが上がりましたが、組み込みの `yes` にはまだ届きません。

残念ながらここでチューニングのネタ切れです。こうするといいよ、という提案があれば是非ご教示ください。

ソースコードは Github の以下にあります。

* [satosystems/yes](https://github.com/satosystems/yes)

## おまけ： C 言語だとどうなの？

C 言語で最も簡単に実装したのが以下です。

```main.c
#include <stdio.h>

int main() {
    for (;;) {
        puts("y");
    }
    return 0;
}
```

これを `gcc -O3 -o yes main.c` でビルドして実行した結果は以下のとおりです。

```
$ ./yes | pv -r > /dev/null
[40.7MiB/s]
```

さすがは C 言語、組み込みの `yes` と同じスループットが出ています。

## おまけ： 様々な `yes`

こちら [yes/yes.hs at master · mubaris/yes](https://github.com/mubaris/yes) で様々な言語で `yes` が実装されています。

Haskell は以下のように実装されており、なるほど `yes` とはこういう仕様だったんだ、とわかりました。

```yes.hs
mport System.Environment
import System.Exit

main = getArgs >>= parse

parse :: [String] -> IO b
parse [] = putStrLn "y" >> parse []
parse (a:_) = putStrLn a >> parse [a]
```