2018-02-11

アマゾンで江添亮の詳説C++17が予約可能になった

江添亮の詳説C++17 | 江添 亮 |本 | 通販 | Amazon

2017年に発行された最新のC++17の新機能をあますところなく解説した私の本、江添亮の詳説C++17がアマゾンで予約可能になったようだ。

本の内容はすでにGitHubで公開している。ライセンスはGPLv3だ。プログラミングの参考書は自由なライセンスで提供されるべきである。

https://github.com/EzoeRyou/cpp17book

次は入門書とTips集を書きたい。

2018-02-09

新しいプログラミング言語を学ぶということ

去年の暮から一ヶ月ほど、Haskellを学んでいる。目的は色々あるが、まったく新しいプログラミング言語を学ぶ経験をもう一度するのが最大の目的だ。

C++17の新機能を余すところなく解説した「江添亮の詳説C++17」は3月に出版される。C++20にはまだ間がある。C++の入門書を書く絶好の機会だ。入門書というのは毎月腐るほど出版されているが、良書は少ない。結局、参考書の中で最も売れ行きが容易に予想可能なのは、最も読み手がいるであろう入門者向けの本であり、入門書の出版がいたずらに増え、粗製濫造されているとみえる。入門書は上級者は読まないので適切な批判もされにくいのでこのような惨状になっているのだろう。

私の入門書はそのような悪書にしたくない。しかし、私はすでにC++の初心者ではないし、C++の初心者がつまづく点がわからない。というより、ここしばらくまったく新しいプログラミング言語を学んだことはない。C++以外にもPython、JavaScript、bashなどを必要に迫られて書き散らすこともあるが、相当古いJavaScript以外は真剣に学んだことがない。

これはよくない。入門書を書く前に、少し時間を割いてでも、全く新しいプログラミング言語に入門すべきだ。そのための言語としてはHaskellがふさわしい。と、こう考えた。

なぜHaskellなのか。HaskellはC++とは全く違った言語だからだ。例えばJavaScriptは表面的にはC++とかなり似た文法を持っているので、全く新しいプログラミング言語を学ぶ目的には使えない。PythonやRubyは文法的には少し違うが、すでに触ったことがあり、それほど苦労した覚えがないのでやはりだめだ。Scalaは汚らわしいJavaの実行環境を私の神聖なメインPCにインストールしたくないために不適だ。

Haskellを学ぶには、Learn you a Haskell for great goodという本が良いらしいので、早速買い求めて読み始めた。この本はとても軽くて読みやすい本だ。そしてユーモアが多い。真面目な参考書に個々までのユーモアは必要なのかと疑問に思うぐらいに多いのだが、しかし全くの初心者が読む本には、これぐらいのユーモアが必要なのではないかと思う。

というのも、だ。私自身、久しぶりに経験して驚いているのだが、全く新しいことを学ぶのは途方もなく疲れる上に非効率的な作業だからだ。不思議なもので、それほど難しいはずはない内容なのに、なぜかなかなか頭に入らない。一日に本を読む時間は何時間もあるが、短期的にいくら時間をかけても内容が頭に入ってこない。一晩寝るなどして時間を開けないとわずか数ページの内容も読み進めることができない。文字を読むことはできるにしても、その内容が理解できず、また残らないとしたら意味がない。

そういう賽の河原のような不毛な作業を続けるには、ユーモアがとても効果的だということがわかる。ただでさえ退屈な作業なのに、ユーモア欠落症患者によって書かれた文章まで読まされるのでは続くものも続かない。

私が書く入門書にはユーモアが必要だということがわかる。

プログラミング言語を学ぶには、プログラミングの実行環境が手元にあったほうがよい。しかしHaskellの実行環境はあまりすぐれているとは言い難い。Haskell PlatformはDebian系ならばapt一発で入るのでまだいいのだが、クールなキッズは皆使うというStackは、私の手元ではまともに動いた試しがない。

そもそもだ。CabalもStackもてんでなっていない。パッケージマネージャと名乗るのもおこがましい。だいたい管理できていない。パッケージを追加することはできるが削除できないのだから。削除するにはどうするか? すべてのパッケージを削除して最初からインストールをやり直す。富豪にもほどがある。Stackもだめだ。結局管理できていない。管理できないからどうするか? とりあえずプロジェクトごとに特定のディレクトリ下に全部環境を構築しよう。富豪にもほどがある。

そういうわけで、Haskellの実行環境を整えるだけで1日を浪費した。C++の入門書では環境構築についてそれなりにページを割かなければならないと痛感した。具体的にはWindowsとMacはC++の学習に不適切なOSなのでGNU/Linuxを推奨したい。主要なディストロならC++の実行環境はデフォルトで入っている。

さて、そうこうしているうちに一ヶ月たち、流石にHaskellである程度のコードは書けるようになってきた。まだモナドについて完全に理解していないのだが、理解できるのも時間の問題だ。なんだ、Haskellも言うほど難しくはないではないか。

しかし、プログラミング言語の学習に当たってはこの時期が一番つらい。プログラミング言語の学習過程では、序盤はさっぱりわからず、中盤はすべてを理解したつもりになり、終盤でやはりわからない状況になる。

プログラミング言語の学習の中盤でつまづくのは、言語の以外な落とし穴だ。例えば私の場合は以下のような例で躓いた。

モナドを学び始めたので、確認がてら、以下のようなコードを書いた。

getContents >>= 複雑な式>>= putStr

しかしこれでは見づらいので、複雑な式を関数に分けることにした。複雑な式の中身は特に重要ではないので、ここでは単にpureとする。

-- こうすればカリー化によって引数を書く必要がないぞ。Haskellは完全に理解した。
f = pure
getContents >>= f >>= putStr

その後、fはそのままにしたまま別の処理を試したくなった。

f = pure
getContents >>= 別の式 >>= putStr

するとエラーになった。意味がわからない。

Haskellではf = pureと書くとfは多相にならず、型を決定しなければならないが、型を推論するための文脈がないのでエラーになるというのが原因だ。解決方法としては、fの型を明示的に書く、引数を書く、GHC拡張を使うなどの方法がある。

しかし、これは割と初心者が引っかかりやすい落とし穴ではないか。例えば以下のコードがなぜエラーになるか、初心者がわかるのだろうか。

f = pure
main = do
    print ( f 0 == Just 0 )
    print ( f 0 == [0] )

どうでもいいことだが、C++を長年やっていた人間として、f = pureとしたあとにf 0 == [0]と書くと、fはpureになり文脈上リストなのでf 0は[0]になるというのは割と驚きがあるし、リストという割と特別扱いされているような昨日でさえ、単なるライブラリ実装であり、(:)はデータコンストラクターにすぎないというのも驚きがある。

早くモナドを完全に理解したい。

2018-02-05

Haskellで書いてみたらC++の10倍遅かった

エレガントな解法、エレファントな解法 〜モンテカルロ法を添えて〜

コインを100回投げたうちに、表もしくは裏が連続して10回でる確率をモンテカルロ法で計算する。

なにはともかくC++で実装してみた。

template < typename Random >
bool coin_toss( const unsigned int try_count, const unsigned int length, Random & r )
{
    unsigned int count{} ;
    int prev{} ;

    std::uniform_int_distribution<> d( 0, 1 ) ;

    for ( unsigned int i = 0 ; i != try_count ; ++i )
    {
        int result = d(r) ;
        if ( result == prev )
        {
            ++count ;
            if ( count == length )
                return true ;
        }
        else
        {
            prev = result ;
            count = 1 ;
        }
    }

    return false ;
}

template < typename Random >
double monte_carlo( unsigned int try_count, Random & r )
{
    unsigned int count{} ;
    for ( unsigned int i = 0 ; i != try_count ; ++i )
    {
        if ( coin_toss( 100, 10, r ) )
            ++count ;
    }

    return double(count) / double(try_count) ;
}

int main()
{
    std::array<unsigned int, sizeof(std::mt19937)/sizeof(unsigned int)> c ; ;
    std::generate( std::begin(c), std::end(c), std::random_device{} ) ;
    std::seed_seq s( std::begin(c), std::end(c) ) ;
    std::mt19937 engine(s) ;

    for ( unsigned int i = 100 ; i != 1000000 ; i *= 10 )
    {
        auto r = engine ;
        std::cout << i << "\t : " << monte_carlo( i, r ) * 100.0 << "%\n" ;
    } 
}

C++11から高度な乱数ライブラリが使えるようになったのだが、現在、実行時に適切に初期化する方法に面倒なボイラープレートコードが必要だ。実行時に乱数で初期化してくれる機能が現在提案されている。

ところで、最近Haskellを学んでいるので、このコードをHaskellでも書いてみようと思った。

まずは乱数だ。コインの表裏はBoolで表現するとして、Haskellらしく無限の乱数リストを生成しよう。

coin_seq :: (RandomGen g) => g -> [Bool]
coin_seq gen = randoms gen

あとはこれをtake 100して一回分の試行とする。1000回試行したければ、drop 100したのを再帰的にtake 100すればよい。しかし、もっといい方法がある。要素数100の[Bool]が入ったリスト、つまり[[Bool]]を無限に作ればいい。そのリストをtake 1000すれば1000回の試行分になる。

split_n :: Int -> [a] -> [[a]]
split_n n seq = take n seq : split_n n (drop n seq)

さて、seq_n = take 100 $ split_n 100 (coin_seq gen)のようなリストseq_n :: [[Bool]]は作れた。あとは[Bool]に連続した要素がn個あるかどうかを調べるcoin_toss nと、それをリストのすべての要素に対して行うmonte_carloを書けばいいだけだ。

coin_tossが結果をBoolで返すとすると、とりあえずseq_nにcoin_tossをmapしてTrueだけfilterして、その結果のリストの要素数を数えれば、成功した試行数がわかる。あとは試行数で割って確率を求めればいいだけだ。

monte_carlo :: Int -> [Bool] -> Double
monte_carlo try_count seq = ((fromIntegral n) / (fromIntegral try_count)) * 100.0
    where
        seq_n = take try_count (split_n 100 seq)
        n = length . filter id $ map (coin_toss 10) seq_n

coin_toss len seqはInt -> [Bool] -> Boolな関数で、seqの中にlen個の連続したTrueもしくはFalseがあるかどうかを数えればよい。

いろいろ考えたが、まずgroup seqして連続した同じ要素を分割して[[Bool]]を作り、それぞれの[Bool]の要素数をいれたリスト[Int]をmapし、len以上の要素だけをfilterして、結果のリストに要素があればTrueを返す計算をすればいいのではないか。つまり、

has_contiguous_elems :: (Eq a) => Int -> [a] -> Bool 
has_contiguous_elems len seq = not $ null $ filter (>= len) $ map length (group seq)

coin_toss :: Int -> [Bool] -> Bool
coint_toss _ [] = 0
coin_toss len seq = has_contiguous_elems len seq

となる。全体的にはこうだ。

import System.Random
import Data.List

coin_seq :: (RandomGen g) => g -> [Bool]
coin_seq gen = randoms gen

split_n :: Int -> [a] -> [[a]]
split_n n seq = take n seq : split_n n (drop n seq)

has_contiguous_elems :: (Eq a) => Int -> [a] -> Bool 
has_contiguous_elems len seq = not $ null $ filter (>= len) $ map length (group seq)

coin_toss :: Int -> [Bool] -> Bool
coint_toss _ [] = 0
coin_toss len seq = has_contiguous_elems len seq

monte_carlo :: Int -> [Bool] -> Double
monte_carlo try_count seq = ((fromIntegral n) / (fromIntegral try_count)) * 100.0
    where
        seq_n = take try_count (split_n 100 seq)
        n = length . filter id $ map (coin_toss 10) seq_n

main = do
    gen <- getStdGen
    let s = coin_seq gen
        in mapM (\n -> putStrLn ((show n) ++ "\t : " ++ (show (monte_carlo n s)) ++ "%") )
                [100, 1000, 10000, 100000] 

このコードは動いた。ただし、最適化オプションを使っても、C++の10倍遅かった。

やはりlen個の連続した要素が存在するかどうかを調べるhas_contiguous_elemsが遅いのではないかと思い、これを再帰で実装することにした。

has_contiguous_elems' :: (Eq a) => Int -> [a] -> Bool 
has_contiguous_elems' _ [] = False
has_contiguous_elems' len (x:xs) =
    if count >= len
        then True
        else has_contiguous_elems' len $ dropWhile (== x) xs
    where count =  1 + (length $ takeWhile (== x) xs)

この実装に差し替えたところ、実行速度はC++の9倍遅かった。

すると、他の部分もリストからリストを生成するという記述をやめて、再帰で実装すればもう少し早くなるのだろうか。

そして思うのは、同様のリストからリストを清々するような処理は、所詮リストのメモリーサイズがキャッシュに収まる程度なので、C++で書くとHaskellより早いのではないかとも思ったが、まだ試していない。

2018-02-04

年収1200万円と2億の資産でできる贅沢はパトロネージュ

以下のはてな匿名ダイアリーの記事が注目を集めている。

贅沢な生活って何が楽しいの?(追記しました

独身34歳男、年収900万+配当収入300万くらい。

贅沢の良さがわからない。

家は相続で貰ったので家賃無し。金融資産は相続したものと合わせて2億を超えた。

生活費は月に12万くらい。配当込みで年間800万以上金が増えていく。

別に金を使うのが嫌いなわけではない。ただ、使う気がおきない。

この増田は1200万円の年収と2億の資産を保有している。増田が過去に行った贅沢は以下の通り。

  • 飛行機のファーストクラス(100万円)
  • ホテルのスイートルーム(100万円)
  • オーダーメイドのスーツ(50万円)
  • 懐石料理(20万円)
  • ステーキ(10万円)

これらの贅沢がありきたりでつまらないのは当然だ。なぜならば、これらの贅沢には、何も1200万円の年収と2億円の資産は必要ないからだ。ただ100万円を貯金して一度に使えばできる程度の贅沢だ。100万円を1年ぐらいかけて貯金するのであれば、34歳男の平均年収である400万円でも可能だ。

せっかく贅沢をするのであれば、年収1200万円と2億円の資産をもって初めて可能になる贅沢をするべきだ。では増田の境遇ではじめて可能になる贅沢とは何か。多額の借金と長期的な支出だ。そして、究極的にはぱとろネージュと呼ばれる行為、すなわち食客を抱えることだ。

多額の借金

増田のような境遇にない我々凡人は、それほど多額の借金ができない。大抵の凡人が生涯に負うことのできる多額の借金は、奨学金と住宅ローンぐらいなものだろう。しかも学費や住宅といった目的が限定されている。

しかし増田は違う。1200万円の年収と2億円の資産を担保に、1億や2億の借金ができるだろう。借金ができる環境はとても贅沢だ。借金で得た金を使って利息を上回る利益を出すことができれば借金をした以上の価値を生む。例えば増田は自分の興味がある事業を立ち上げることができる。ただし、これには利息を上回る利益を出すことができずに資産を失うリスクはある。

長期的な支出

増田が例に挙げている贅沢は、短期的な一度きりの支出ですむものだ。100万円を払ってファーストクラスに乗ったりスイートルームに泊まったりするのはいかにも贅沢に思えるが、一回払うだけでその後は何の支出もいらない。

しかし増田は違う。増田は月に50万円ぐらいの支出を長期的に続けることができる。長期的に支出を行えるのは、我々凡人には不可能な贅沢だ。では、長期的な支出が必要な贅沢とは何か。

答えは人だ。増田は長期的に人をフルタイムで雇うことができる。増田は秘書を雇い日常の雑事をすべて押し付けることもできる。増田の時間は貴重である。増田がファーストクラスやスイートルームや懐石料理を楽しむとして、その予約作業という雑事は、わざわざ贅沢な増田の手をわずらわす作業ではない。秘書にやらせればよい。

そして、古来より最高の贅沢とされる人への支出がある。パトロネージュ、すなわち食客を抱えることだ。

増田にはより発展してほしい芸術や学問はないだろうか。もっと具体的に書こう。自分の気にいる漫画や小説を読みたい。自分の気にいるゲームで遊びたい。ある自由なソフトウェアに自分の望みの機能を追加したい。ある病気を根絶する研究が進んでほしい。ある技術がもっと発展してほしい。しかし、増田は芸術や学問を極めることはできない。増田に絵や詩人の才能はなく、プログラミングはできず、医学博士や工学博士として研究に身を捧げる人生も送りたくはないとする。しかし、世の中には芸術や学問に身を捧げたいと願う天才はいる。増田はそのような天才を食客として衣食住の不便をなくし、芸術や学問を追求させることができる。

増田が増田を書くことができるのは、コンピューターが発展したからだ。ところで現代のコンピューターの発展はチャールズ・バベッジの研究に負うところが大きい。もしチャールズ・バベッジがコンピューターの研究に打ち込まなければ、コンピューターの発展は数十年は遅れていただろう。チャールズ・バベッジは裕福な家の出身ではあったが、コンピューターの研究のためにパトロンが必要だった。

この話の教訓としては、自分が楽しむためにカネを使えないのであれば、他人が楽しむためにカネを使ってみてはどうか。その結果として芸術や学問が発展するのであればこれ幸い。

2018-02-02

江添ボドゲ会@2月11日

だいたい毎月行っている自宅ボドゲ会を2月11日に開催します。

江添ボドゲ会@2月11日 - connpass