ISUCON7の予選で敗退した

Blogにまとめるまでが、ISUCON

と、言うことで久しぶりにこっちに書く。

前から興味はあったけれど、参加者のブログを見る程度だった。

そんな時に、会社の同期に誘ってもらったので、ホイホイ受けてみた。

事前準備

過去問がVagrantで公開されているので、本番と同じ10:00-18:00でISUCON6の問題を解いてみる、ISUCON5の問題をざっくり読んで改善点を挙げる、というゆるふわな対策を行った。GitHub Privateにリポジトリを用意したり、Google DocにTODOリストを作成したりと、ここまでは良い感じだった。

github.com

自分の担当はDeploymentだったので、AnsibleのPlaybookをちょこちょこ作っていた。最近は専らDockerfileを書いているので、すっかり使い方を忘れていたけれど、Best Practices — Ansible Documentation 見ながらなんとなく書けるので、Ansible楽で良い。

Host Machine -> Vagrantで立ち上げたisucon5用のVM にansible-playbookするためのinventory fileはこんな感じ。

当日

開始直後

いきなりインスタンスが3台あってちょっと焦る。

とりあえず、Ansible script流す。InventoryにIP Address追加するだけだから、流す分には特に困らなかった。なお、各自のssh keyは ssh-copy-id で送れというスパルタ仕様。

そして、ベンチ走らせてみる、確かデフォルトがPythonで1台にだけ負荷をかけた状態。

Time    2017-10-22 13:27:06 +0900 JST
Message ok
Score   2282
Best    2282
LoadLevel       1

Goに切り替える。やっぱり1台だけ。

Time    2017-10-22 13:32:00 +0900 JST
Message ok
Score   4521
Best    4521
LoadLevel       1

2台だと?

Time    2017-10-22 13:35:38 +0900 JST
Message ok
Score   5030
Best    5030
LoadLevel       1

あんまり増えないね。

ということで、コードリーディングタイム。

14過ぎ辺り

改善できそうな所を出し合う。

alp用のログフォーマットをNginxに適応して、実際のデータを確認。これは、最初にやっても良かったかも。あと、access_log /var/log/nginx/access.log ltsv;パスの後ろにltsvをつけ忘れるという凡ミスを犯した、ごめん。

github.com

DBのチューニングや、index効いてない、N+1問題などベタな所を修正。

iconの画像を、毎回DBから取得しているのが遅いよね。ということで、静的ファイルに書き出してNginxから返そうという方針に。

16時過ぎ辺り

新規、更新のiconはファイルに吐けるようになったので、既存の画像もファイルにしてしまおう。

Time    2017-10-22 16:43:26 +0900 JST
Message 負荷走行前のバリデーションに失敗しました。2017-10-22 16:43:25.201090568 +0900 JST m=+1.524851228 [Fatal]画像データが正しくありません (GET /icons/65ea5a70b1fcf8f5f816b01b87f2aca2251315b0.png )
Score   0
Best    7225
LoadLevel       0

謎のバリデーションエラーにより、Nginxから返す作戦を諦めアプリから返すように戻す。

しかし依然として出続けるバリデーションエラー。。。

この時は、変更を1号機にしか適応していなかったので、2号機に1号機のバイナリを持って行って再現確認。結果、再現せず。

ベンチマークツールに変なファイルキャッシュされたのか?と散々悩む。

18時過ぎ

ダメだ、バリデーションエラーが出続ける。もう、諦めてラーメンか?という思いの中、DBのデータから静的ファイルを生成するロジックを書き換えたら、バリデーションに通った!危うく、ISUCONに出たこと自体を無かったことにするとこだった。

Time    2017-10-22 18:21:50 +0900 JST
Message ok
Score   23513
Best    23513
LoadLevel       1

その後、iconはinitializeの時にNginxのメモリに載せたいよね。という話になるが

で、どう設定すれば良いの?

という残念な状態になり、必死にググるおっさんたち。

20時過ぎ

せめて、アプリを3台にスケールしようということで、2号機とDB用だった3号機にもアプリを展開。

Nginxのログを切って、MySQLのSlow Queryログも切って、できる限りのことをした結果。

Time  2017-10-22 20:50:48 +0900 JST
Message  ok
Score  42731
Best  44732
LoadLevel  1

4万ちょいでフィニッシュでした。

総評

Ansibleでスマートにデプロイしたかったけれど、makeが通らない!等焦ってしまい、書き捨てのShellで凌いだのが負けた気分だったし、そもそもデプロイするところまで事前に用意しておくべきだった。

ETC_BR=${1:-master}
APP_BR=${2:-master}

cd /etc.git
sudo git fetch
echo "[INFO] Checkout $ETC_BR"
sudo git checkout $ETC_BR
sudo git pull
sudo cp -r /etc.git/nginx/* /etc/nginx
echo "[INFO] Restart Nginx"
sudo systemctl restart nginx

cd /home/isucon/isubata/webapp/go
git fetch
echo "[INFO] Checkout $APP_BR"
git checkout $APP_BR
git pull
make
echo "[INFO] Restart Go APP"
sudo systemctl restart isubata.golang.service

あと、過去何度もNginxが登場しているというのに、Nginxへの理解が低すぎたのは大変よろしくなかった。

とても悔しかったので、来年リベンジしたいな。

冬休みの読書感想文

WEB+DB PRESS vol.90

WEB+DB PRESS Vol.90

WEB+DB PRESS Vol.90

Git実践活用

Git自体の使い方ではなく活用方法を教えてくれるので、特にルールが決まって無いのなら、これに従うと楽そう。 取りあえずググった解決方法試したら、git push -f でmasterブランチの履歴おかしくなりました。 という悲しい事件を未然に防ぐために、場当たり的ではなく体系的に学ぶのがオススメです。

git commit前に静的解析かけるなら、こちらの記事も合わせてどうぞ。

git用のpre-commit gemが便利すぎる - TakiTakeの日記

ドラゴンクエストX 開発ノウハウ大公開

ドラクエでもOracle, Cassandra使ってるのか、という勝手な親近感が湧く。 サラッと書いてあるけれど、KVSへ保存するバイナリサイズが当初の倍以上になったが、余裕を持って設計していたため基本構成はサービス開始当初のままというのはカッコいい。 通信遅延を違和感の無いようにラップする努力は見習いたいところ。

MMOに関しての技術書は、Mobageを支える技術もオススメです。

Mobageを支える技術 ~ソーシャルゲームの舞台裏~ (WEB+DB PRESS plus)

Mobageを支える技術 ~ソーシャルゲームの舞台裏~ (WEB+DB PRESS plus)

Release It!

Release It! 本番用ソフトウェア製品の設計とデプロイのために

Release It! 本番用ソフトウェア製品の設計とデプロイのために

現実は、入門書みたいに単純じゃないんだよ!と嘆いてる人にオススメの一冊。 実例を交えながら対策を説明してくれるので、ものすごく共感できる。

午前9時5分までに全サーバ上のアクティブなセッションは10,000件になっていた。

午前9時10分、サイト上のアクティブなセッションは50,000件を超えた。

午前9時30分、サイト上のアクティブなセッションは250,000件だった。そしてサイトはクラッシュした。

あるある(笑えない)

行儀の良いテストしかしていない。という指摘は、耳が痛い。

ElixirとRabbitMQでJenkinsっぽいサービスつくる その1

Elixirで何か作ってみたい

ということで、いろいろ考えた結果、俺の考えた最強のYet Another Jenkinsをつくってみようとおもいます。 Queueの管理はRabbitMQに任せる予定なので、いわゆるPub/Sub Applicationになる予定。 ブログなので、進捗等を雑にまとめていく。

コンセプト

UIはIFTTTくらいシンプルにする

シンプル イズ ベスト

サービスは実行スクリプトを持たない、Jobのコントロールにのみ専念する

Jenkinsの場合、Jobに直接スクリプトを登録できるのですが、これをやり過ぎると責任範囲が曖昧になってしまう上に、スクリプトをどこで定義しているのかわからなくなってしまうので禁止にする。 代わりに、実行ファイルもしくはPluginの機能を実行する。 最悪、サービスが止まっても、Jobをローカルで実行できることが望ましい。

JobはDocker container上で実行する

Slaveサーバ同士の環境差分や、Localだと動いた/動かない、Jenkinsだと動いた/動かない 問題が厄介なので、差分を無くすために積極的にDockerの恩恵にあずかる。

悩み中

JenkinsのBuild FlowやWork Flow Pluginに代わるJobの定義方法を模索中。 今のプロジェクトでは、これらをヘビーに使っていて、便利さは重々感じているのですが、Jenkinsに依存しちゃうのがいまいちなのと、Jenkins職人じゃないと管理できないのが辛い。

ひとまず、Bitbucketと連携を試みる

なぜGitHubじゃないのかというと、弊社がAtlassianのサービス使っているので、需要はこっちの方が高いからです。もちろん、将来的にはGitHubも対応予定。

Webhookのリクエスト確認

Git pushに対するWebhook

フローとしては

  1. Git push
  2. Bitbucket -- webhook --> Publisherアプリ
  3. Publisherアプリがwebhookの情報を加工してRabbitMQにメッセージとして送る
  4. Exchangeが適切なQueueにメッセージを追加
  5. ConsumerアプリがQueueからメッセージを取り出し処理する

Netcatで生データ見るのが、一番手っ取り早い。

$ nc -l 3000
POST / HTTP/1.1
Host: mysidia.takitake.tech:3000
Content-Length: 3404
Accept-Encoding: gzip, deflate
Accept: */*
X-Event-Key: repo:push
User-Agent: Bitbucket-Webhooks/2.0
X-Hook-UUID: 50ac74f5-e8d8-4b6a-82ce-02f433d4816c
Content-Type: application/json
Via: 1.1 util-102.ash1.bb-inf.net (squid/3.3.8)
Cache-Control: max-age=259200
Connection: keep-alive

{"repository": {"name": "mysidia", "scm": "git", "links": {"avatar": {"href": "https://bitbucket.org/takitake/mysidia/avatar/16/"}, "html": {"href": "https://bitbucket.org/takitake/mysidia"}, "self": {"href": "https://api.bitbucket.org/2.0/repositories/takitake/mysidia"}}, "full_name": "takitake/mysidia", "uuid": "{65abe83f-810a-469f-bfe0-e2d4c409618b}", "type": "repository", "is_private": true, "owner": {"links": {"avatar": {"href": "https://bitbucket.org/account/takitake/avatar/32/"}, "html": {"href": "https://bitbucket.org/takitake/"}, "self": {"href": "https://api.bitbucket.org/2.0/users/takitake"}}, "uuid": "{22c717bd-843a-47f8-8a52-38eef8145ddb}", "display_name": "Takeshi Takizawa", "type": "user", "username": "takitake"}}, "push": {"changes": [{"old": null, "closed": false, "created": true, "truncated": false, "commits": [{"author": {"raw": "TakiTake <TakiTake.create@gmail.com>", "user": {"links": {"avatar": {"href": "https://bitbucket.org/account/takizawatake01/avatar/32/"}, "html": {"href": "https://bitbucket.org/takizawatake01/"}, "self": {"href": "https://api.bitbucket.org/2.0/users/takizawatake01"}}, "uuid": "{608bf02a-5eb8-4822-b16e-699e6fd8fbe3}", "display_name": "Takeshi Takizawa", "type": "user", "username": "takizawatake01"}}, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/takitake/mysidia/commit/8fb0eefc6a340d57598ca553a07fade1a10f2c30"}, "html": {"href": "https://bitbucket.org/takitake/mysidia/commits/8fb0eefc6a340d57598ca553a07fade1a10f2c30"}}, "type": "commit", "message": "Initial commit with contributors\n", "hash": "8fb0eefc6a340d57598ca553a07fade1a10f2c30"}], "forced": false, "links": {"commits": {"href": "https://api.bitbucket.org/2.0/repositories/takitake/mysidia/commits?include=8fb0eefc6a340d57598ca553a07fade1a10f2c30"}, "html": {"href": "https://bitbucket.org/takitake/mysidia/branch/master"}}, "new": {"name": "master", "links": {"commits": {"href": "https://api.bitbucket.org/2.0/repositories/takitake/mysidia/commits/master"}, "html": {"href": "https://bitbucket.org/takitake/mysidia/branch/master"}, "self": {"href": "https://api.bitbucket.org/2.0/repositories/takitake/mysidia/refs/branches/master"}}, "type": "branch", "target": {"links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/takitake/mysidia/commit/8fb0eefc6a340d57598ca553a07fade1a10f2c30"}, "html": {"href": "https://bitbucket.org/takitake/mysidia/commits/8fb0eefc6a340d57598ca553a07fade1a10f2c30"}}, "type": "commit", "message": "Initial commit with contributors\n", "hash": "8fb0eefc6a340d57598ca553a07fade1a10f2c30", "author": {"raw": "TakiTake <TakiTake.create@gmail.com>", "user": {"links": {"avatar": {"href": "https://bitbucket.org/account/takizawatake01/avatar/32/"}, "html": {"href": "https://bitbucket.org/takizawatake01/"}, "self": {"href": "https://api.bitbucket.org/2.0/users/takizawatake01"}}, "uuid": "{608bf02a-5eb8-4822-b16e-699e6fd8fbe3}", "display_name": "Takeshi Takizawa", "type": "user", "username": "takizawatake01"}}, "date": "2015-09-25T12:44:44+00:00", "parents": []}}}]}, "actor": {"links": {"avatar": {"href": "https://bitbucket.org/account/takitake/avatar/32/"}, "html": {"href": "https://bitbucket.org/takitake/"}, "self": {"href": "https://api.bitbucket.org/2.0/users/takitake"}}, "uuid": "{22c717bd-843a-47f8-8a52-38eef8145ddb}", "display_name": "Takeshi Takizawa", "type": "user", "username": "takitake"}}

bitbucket.repo.pushをRabbitMQのrouting keyにして、messageは上記のJSONをmessagepackでエンコードしようかな。

Google様Pub/Subサービスしてた

対抗するわけではないので、積極的に参考にしていく方向で

What is Google Cloud Pub/Sub? - Cloud Pub/Sub — Google Cloud Platform

pre-commitへPull Request送ったときの調査方法

pre-commitって何?という人は、こちらを先に見てください

takitake.hatenablog.com

もしや、バグ?

ignoreできないファイルがある

普段から、pre-commitを愛用していますが、中にはチェックして欲しくないファイルもあります。そんなときは、.pre_commit.ignoreにファイルのパターンを記載するのですが、どうしてもignoreできないケースがありました。

# .pre_commit.ignore
foo/*/*.yml

fooディレクトリ以下にある、YAMLファイルは対象外

# foo/bar/baz.yml
key: val

実際、余計な改行が入っている

$ pre-commit run
pre-commit: Stopping commit because of errors.
foo/bar/baz.yml:2: new blank line at EOF.

無視されず、怒られた orz

ちょっとコードを追って見る

rbenvでRubyを管理しているので、.rbenvディレクトリ以下にGemがインストールされています。RubyのVersionは適宜読み替えて下さい。

$ cd ~/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/

bin以下に実行ファイルがある

# bin/pre-commit
#!/usr/bin/env ruby

require 'pre-commit/cli'

if !File.exists?(".git")
  abort "No .git directory found."
end

PreCommit::Cli.new(*ARGV).execute or exit 1

ARGVで引数を渡している

module PreCommit
  class Cli

    def initialize(*args)
      @args = args
    end

    def execute()
      action_name = @args.shift or 'help'
      action = "execute_#{action_name}".to_sym
      if respond_to?(action)
      then send(action, *@args)
      else execute_help(action_name, *@args)
      end
    end

    def execute_run(*args)
      require 'pre-commit/runner'
      PreCommit::Runner.new.run(*args).tap { |ok| puts "No failed checks." if ok }
    end
  end
end

action_name 今回は、runなので execute_run が呼ばれる

# lib/pre-commit/runner.rb
module PreCommit
  class Runner
    include PreCommit::Utils::StagedFiles

    def run(*args)
      set_staged_files(*args)
      run_single(:warnings)
      run_single(:checks  ) or return false
      true
    end
  end
end

runメソッドでは、まずはcommit対象のファイルを探してそう。

set_staged_filesメソッドは、このクラスに無いので

include PreCommit::Utils::StagedFiles

で、読み込んでるのかな?

そろそろ、追うの大変になってきたので、binding.pryを仕込む

# lib/pre-commit/runner.rb
require 'beybug'
require 'pry-beybug'

module PreCommit
  class Runner
    include PreCommit::Utils::StagedFiles

    def run(*args)
      binding.pry
      set_staged_files(*args)
      run_single(:warnings)
      run_single(:checks  ) or return false
      true
    end
  end
end

beybugとpry-beybugをrequireして、止めたい箇所に binding.pry を挿入

$ pre-commit run
Frame number: 0/5

From: /Users/takeshi.takizawa/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/lib/pre-commit/runner.rb @ line 28 PreCommit::Runner#run:

    26: def run(*args)
    27:   binding.pry
 => 28:   set_staged_files(*args)
    29:   run_single(:warnings)
    30:   run_single(:checks  ) or return false
    31:   true
    32: end

step実行で先に進めていく

From: /Users/takeshi.takizawa/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/lib/pre-commit/utils/staged_files.rb @ line 48 PreCommit::Utils::StagedFiles#filter_files:

    40: def filter_files(files)
    41:   files.reject do |file|
    42:     !File.exists?(file) ||
    43:     File.directory?(file) ||
    44:     (
    45:       size = File.size(file)
    46:       size > 1_000_000 || (size > 20 && binary?(file))
    47:     ) ||
 => 48:     repo_ignores.any? { |ignore| File.fnmatch?(ignore, file) }
    49:   end
    50: end

[4] pry(#<PreCommit::Runner>)> repo_ignores.any? { |ignore| File.fnmatch?(ignore, file) }
=> true
[5] pry(#<PreCommit::Runner>)> file
=> "foo/bar/baz.yml"

ちゃんと、ignore対象になっているみたい。ignoreファイルの書き方はあってそう。

ソースコードに戻ると、listをひとつひとつ実行して、エラーが一つでもあれば show_output でエラー表示しつつ false 返している。

    def run_single(name)
      show_output(name, execute(list_to_run(name)))
    end

    def show_output(name, list)
      if list.any?
        @stderr.puts send(name, list)
        return false
      end
      true
    end

    def execute(list)
      list.map do |cmd|
        result = nil

        seconds = Benchmark.realtime do
          result = cmd.new(pluginator, config, list).call(staged_files.dup)
        end

        puts "#{cmd} #{seconds*1000}ms" if debug

        result
      end.compact
    end

どのチェックに引っかかったか分かるように、デバッグメッセージ埋め込む。

    def execute(list)
      list.map do |cmd|
        result = nil

        seconds = Benchmark.realtime do
          result = cmd.new(pluginator, config, list).call(staged_files.dup)
        end

        puts "#{cmd} #{seconds*1000}ms" if debug

        unless result.nil
          puts "=" * 30
          puts "#{cmd} #{result}"
          puts "=" * 30
        end

        result
      end.compact
    end
$ pre-commit run
==============================
PreCommit::Checks::Whitespace foo/bar/baz.yml:2: new blank line at EOF.
==============================
pre-commit: Stopping commit because of errors.
foo/bar/baz.yml:2: new blank line at EOF.

pre-commit: You can bypass this check using `git commit -n`

PreCommit::Checks::WhitespaceでCheckが通らなかった事がわかった。

"class Whitespace"でgrepして定義位置探す。

# lib/plugins/pre_commit/checks/whitespace.rb
module PreCommit
  module Checks
    class Whitespace < Plugin
      def call(staged_files)
        binding.pry
        errors = `git diff-index --check --cached HEAD -- #{files_string(staged_files)} 2>&1`
        return if $?.success?

        # Initial commit: diff against the empty tree object
        if errors =~ /fatal: bad revision 'HEAD'/
          errors = `git diff-index --check --cached 4b825dc642cb6eb9a060e54bf8d69288fbee4904 -- 2>&1`
          return if $?.success?
        end

        errors
      end
    end
  end
end

call直後で止めてみる

$ pre-commit run

Frame number: 0/11

From: /Users/takeshi.takizawa/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/lib/plugins/pre_commit/checks/whitespace.rb @ line 27 PreCommit::Checks::Whitespace#call:

    25: def call(staged_files)
    26:   binding.pry
 => 27:   errors = `git diff-index --check --cached HEAD -- #{files_string(staged_files)} 2>&1`
    28:   return if $?.success?
    29: 
    30:   # Initial commit: diff against the empty tree object
    31:   if errors =~ /fatal: bad revision 'HEAD'/
    32:     errors = `git diff-index --check --cached 4b825dc642cb6eb9a060e54bf8d69288fbee4904 -- 2>&1`
    33:     return if $?.success?
    34:   end
    35: 
    36:   errors
    37: end

[1] pry(#<PreCommit::Checks::Whitespace>)> staged_files
=> []
[2] pry(#<PreCommit::Checks::Whitespace>)> "git diff-index --check --cached HEAD -- #{files_string(staged_files)} 2>&1"
=> "git diff-index --check --cached HEAD --  2>&1"

あぁ、本来は対象のファイルを指定して git diff-index --check するのだけれど、staged_filesが空の場合はファイル指定無し、つまり、全ファイルがチェック対象になっちゃうのね。

空の時はチェックしなければ良いじゃない

github.com

後始末

$ gem pristine pre-commit
Restoring gems to pristine condition...
Restored pre-commit-0.22.1

pre-commitを初期状態に戻す。

Scottyで簡易サーバ作成

つくったもの

ScottyでWeb API呼び出しを束ねる(予定)のServerを作成しました。Yesodは、俺にはまだ早かった。

構成

Web Browser -> Web Server (Scotty) -> API Server (Sinatra)

SinatraAPI Server作成

Rubyの方が慣れているので、API Server側はRubyでサクッとつくる。

$ mkdir api
$ cd api
$ bundle init
$ vim Gemfile
$ bundle install

Gemfile

source "https://rubygems.org"

gem "sinatra"

index.rb

require 'sinatra'

get '*' do
  'Hello World!'
end

サーバ立ち上げる

$ ruby index.rb

Scottyでクライアント作成

$ mkdir server
$ cd server
$ cabal sandbox init
$ cabal init
$ vim server.cabal
$ cabal install

server.cabal

-- Initial server.cabal generated by cabal init.  For further
-- documentation, see http://haskell.org/cabal/users-guide/

name:                server
version:             0.1.0.0
-- synopsis:
-- description:
license:             MIT
license-file:        LICENSE
author:              Takeshi Takizawa
maintainer:          takitake.create@gmail.com
-- copyright:
category:            Web
build-type:          Simple
-- extra-source-files:
cabal-version:       >=1.10

executable server
  main-is:             Main.hs
  -- other-modules:
  -- other-extensions:
  build-depends:       base >=4.7 && <4.8
                      ,http-conduit
                      ,scotty
  -- hs-source-dirs:
  default-language:    Haskell2010

Main.hs

{-# LANGUAGE OverloadedStrings #-}

import Web.Scotty
import Network.HTTP.Conduit (simpleHttp)

main :: IO ()
main = scotty 3000 $ do
    get "/:word" $ do
        res <- simpleHttp "http://localhost:4567/"
        raw res

サーバ立ち上げる

# ここの手順が自信ないです
# 今時は、どうやるんだろ?
$ cabal install
$ ./dist/dist-sandbox-4f285b0c/build/server/server

動作確認

ブラウザで、 http://localhost:3000/ にアクセス。Hello World!が表示されること。

所感

Ruby Haskell
Rails Yesod
Sinatra Scotty
httparty http-conduit

かなと。

  • cabal installは、毎回buildするもの?gem installに比べもの凄い時間かかる
  • ByteString.Lazyをどう表示したら良いか分からなくて、コンパイラに散々怒られて辛い

Rubyが辛くなってきた俺はHaskellに浮気する

はじめに

社内で、Web API呼び出しを束ねるWeb API作成ブームだから、Haskellでブームに乗ってみようかなと。Ruby好きなんだけれど、人数が増えてくるとコードのフォーマットがバラバラになるのが辛くて、型チェックが羨ましくなったのが選択理由。

関数プログラミング実践入門

確実にこの本に感化されてるw こちらは、いざHaskell使って何か作ろうとしたときに、つまずくであろう箇所を丁寧に解説してくれている。Haskellをサンプルに、オブジェクト試行の言語と比較しながら解説されているので、自分にはよく刺さった。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

次に購入した本、Haskellという言語自体を学ぶのに適している。 堅い言語とは逆に、軽快な口調で解説しているので、とっつきやすい。ファンクターの説明辺りで理解が追いつかなくなったので、精進せねば。

環境構築

Haskell Platform

何はともあれ、https://www.haskell.org/platform/

for Vimmer

fast-tag

ctagsはHaskell未対応なので、fast-tagsにtagsファイル作成してもらう

ghc-mod

依存解消でエラーになったので、下記ページを参考に

Build failed · Issue #425 · kazu-yamamoto/ghc-mod · GitHub

$ ghc-pkg unregister --force monad-control
$ cabal install --constraint "monad-control < 1" ghc-mod

Framework

https://www.haskell.org/haskellwiki/Web/Frameworks https://www.techempower.com/benchmarks/

Yesod

Yesodが人気なようなので、Yesodでベースに作成しようと思う。

Rubyの子クラスで定数を再定義したのに親の定数が参照されちゃう

ちゃう

class Parent
  CONST = 'parent'

  def initialize
    p CONST
  end
end

class Child1 < Parent
  CONST = 'child'
end

Child1.new # "parent"

"child"を期待してたら、"parent"が返ってきた!?

定数の場合、今のスコープになかったら、外のスコープから探索するので initializeメソッドの外に出たら、Parent::CONSTがいるので、その値を返しているみたい。

じゃあ、子クラス毎にinitializeメソッドを定義すれば

class Parent
  CONST = 'parent'
end

class Child1 < Parent
  CONST = 'child'

  def initialize
    p CONST
  end
end

Child1.new # "child"

やりました、期待通り"child"が返ってきました!

ないな

要は 子クラス::CONST を参照してくれれば良いのでしょ

class Parent
  CONST = 'parent'

  def initialize
    p self.class::CONST
  end
end

class Child1 < Parent
  CONST = 'child'
end

Child1.new # "child"

いけそう

class Parent
  CONST = 'parent'

  def initialize
    p self.class::CONST
  end
end

class Child2 < Parent; end


Child2.new # "parent"

さらに、継承しても

class Parent
  CONST = 'parent'

  def initialize
    p self.class::CONST
  end
end

class Child2 < Parent; end

class Grandchild < Child2
  CONST = 'grandchild'
end

Grandchild.new # "grandchild"

期待通り!