Go Concurrency Patterns: Context[翻訳]

7月29日付けのgolang.orgブログエントリーで context というパッケージが紹介されました。

Go Concurrency Patterns: Context - The Go Blog

参考: Go言語のcontextパッケージについてのやりとり - ワザノバ | wazanova

今現在、業務でGo言語を使ったWebサービスを書いているのですが、contextはリクエストのキャンセルとタイムアウトとリクエストスコープの変数を扱う、Google社内で利用が標準化されているパッケージだという事なので、エントリーを翻訳しました。

以下和訳。

Introduction

Goサーバーでは、入ってくる各リクエストは専用のgoroutineで処理されます。リクエストハンドラは、データベースやRPCサーバーにアクセスするためにしばしば追加のゴルーチンを開始します。リクエスト上のゴルーチンは、一般的にエンドユーザーのアイデンティティ、認証トークン、リクエストのデッドラインなどのリクエスト特有の情報にアクセスする必要があります。リクエストがキャンセルされるかタイムアウトした場合、システムが資源を再利用できるようにするため、そのリクエスト上で動作していた全てのゴルーチンは即座に終了すべきです。

Googleで私たちは、リクエストスコープの値、キャンセレーションシグナル、そしてAPI境界をまたぐデッドラインを、リクエスト処理に関わる全てのゴルーチンに渡す事を簡単にするためのcontextパッケージを開発しました。パッケージは code.google.com/p/go.net/contextで公開されています。この記事では、パッケージの使い方と完全な動作例を紹介します。

Context

contextパッケージの核となるのはContext型です:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

(この記述は要約です。godocが信頼できます)

DoneメソッドはContextを実行中の関数へのキャンセレーションシグナルとして振る舞うチャンネルを返します:そのチャンネルがクローズされたら、関数は自分たちの仕事を放棄してreturnするべきです。ErrメソッドはそのContextがキャンセルされた理由を示すerrorを返します。Pipelines and CancelationでDoneチャンネルイディオムについてより詳しく議論しています。

Contextには、Doneチャンネルがreceive-onlyであることと同じ理由によりCancelメソッドがありません:キャンセレーションシグナルを受信している関数は、通常そのシグナルを送信する関数と同じではないからです。特に、親オペレーションがサブオペレーションのためにゴルーチンを開始したとき、それらのサブオペレーションは親をキャンセルできるべきではありません。そのかわりに、WithCancel関数(後述)は新しいContext値をキャンセルする方法を提供します。

Contextは複数ゴルーチンによる同時利用に対して安全です。コードは単一のContextをいくつのゴルーチンにでも渡せますし、それら全てにシグナルしてそのContextをキャンセルする事も出来ます。

Deadlineメソッドにより、関数は自分が仕事を開始するべきかどうか判断することができます;もし残り時間がほとんどなければ、全くの無駄になるかもしれません。コードはI/Oオペレーションのタイムアウトを設定するのに使う事も出来ます。

Valueにより、Contextでリクエストスコープなデータを運ぶ事が出来ます。そのデータは複数ゴルーチンによる同時利用に対して安全である必要があります。

派生コンテキスト

contextパッケージは既存のContextから新しいContextを派生するための関数を提供します。これらの値はツリーを形成します; あるContextがキャンセルされたとき、そこから派生している全てのContextもまたキャンセルされます。

Backgroundは全てのContextツリーのルートです;決してcancelされることはありません。

// Backgroundは空のContextを返します。これは決してキャンセルされず、デッドラインを持たず、値も持ちません。
// Backgroundは一般的にmain,init,そしてtests内で、やってくるリクエストのトップレベルコンテキストとして使われます。
func Background() Context

WithCancelとWithTimeoutは、親Contextがキャンセルされるよりも早くキャンセルできる派生Contextの値を返します。到着したリクエストに関連づけられたContextは、一般的にはリクエストハンドラがreturnした時にキャンセルされます。WithCancelは複数レプリカ使用時に冗長なリクエストをキャンセルするのにも便利です。WithTimeoutは、バックエンドサーバーへのリクエストにデッドラインを設定するのに便利です:

// WithCancel は、parent.Doneがクローズされるかcancelが呼び出されると
// Doneチャンネルがクローズされるようなparentのコピーを返します
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// CancelFuncはContextをキャンセルします
type CancelFunc func()

// WithTimeoutはparent.Doneがクローズされるかcancelが呼び出されるかtimeoutが
// 経過するとDoneチャンネルがクローズされるようなparentのコピーを返します。
// 新しいContextのDeadlineはnow+timeoutと、もしあればparentのdeadlineのうちの近い方です。
// もしtimerがまだ動いていれば、cancel関数はそのリソースをリリースします。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValueはrequest-scopedな値をContextに関連づける方法を提供します

// WithValue はValueメソッドがkeyに対するvalを返すようなparentのコピーを返します。
func WithValue(parent Context, key interface{}, val interface{}) Context

contextパッケージの使い方を見る一番の方法は、動いているサンプルを見る事です。

Example: Google Web Search

この例は、/search?q=golang&timeout=1s のようなURLを、クエリ"golang"をGoogle Web Search APIにフォワードして結果を表示するという風にハンドルするHTTPサーバーです。timeoutパラメーターはサーバーにこの時間が経過した後にリクエストをキャンセルするように指示します。このコードは三つの部分に分かれています:

  • server はmain関数と/search のハンドラを提供します
  • userip はユーザーIPアドレスをリクエストから取得してContextに関連づける関数を提供します
  • google はクエリをGoogleに送信するSearch関数を提供します

The server program

serverプログラムは/search?q=golang のようなリクエストに対して最初のいくつかのgolangGoogle検索結果を返します。ハンドラはctxと呼ばれる初期Contextを作成し、ハンドラがreturnした時にキャンセルされるように準備します。リクエストがtimeout URLパラメータを含んでいたら、Contextはtimeout経過後に自動的にキャンセルされます:

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctxはこのハンドラのContextです。cancleを呼ぶと、このハンドラ
    // が開始したリクエストへのキャンセレーションシグナルである
    // ctx.Doneチャンネルがクローズします
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // リクエストはタイムアウトを持つので、timeoutがexpireしたら
        // 自動的にキャンセルされるcontextを作ります
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // handleSearchがリターンしたらctxをキャンセル

ハンドラはリクエストからクエリを抜き出し、useripパッケージを呼び出す事でクライアントのIPアドレスを取得します。クライアントのIPアドレスはバックエンドリクエストに必要なので、handleSearchはそれをctxにくっつけます:

    // Check the search query.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

ハンドラはctxとqueryでgoogle.Searchを呼び出します:

    // Run the Google search and print the results.
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)

検索が成功したら、ハンドラは結果を表示します:

    if err := resultsTemplate.Execute(w, struct {
        Results          google.Results
        Timeout, Elapsed time.Duration
    }{
        Results: results,
        Timeout: timeout,
        Elapsed: elapsed,
    }); err != nil {
        log.Print(err)
        return
    }

Package userip

useripパッケージはリクエストからユーザーIPアドレスを抜き出してContextに関連づける関数を提供します。Contextはkey-valueマッピングを提供しますが、そのkeyとvalueはどちらもinterface{}型です。Key型は等価をサポートし、valueは複数ゴルーチンからの同時利用に対して安全である必要があります。useripのようなパッケージはこのマッピングの詳細を隠蔽し、特定のContext値への強く型付けされたアクセスを提供します。

キー衝突を避けるために、useripはexportされない型のkeyを定義してcontext keyとしてこの型の値を使います。

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int

// userIPkey is the context key for the user IP address.  Its value of zero is
// arbitrary.  If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0

FromRequestはuserIP値をhttp.Requestから抜き出します

func FromRequest(req *http.Request) (net.IP, error) {
    s := strings.SplitN(req.RemoteAddr, ":", 2)
    userIP := net.ParseIP(s[0])
    if userIP == nil {
        return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
    }

NewContextは与えられたuserIP値を運ぶ新しいContextを返します:

func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

FromContextはuserIPをContextから抜き出します:

func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil.
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

Package Google

google.Search関数はGoogle Web Search APIへのHTTPリクエストを作ってJSONエンコードされた結果をパースします。これはContextパラメータctxを受け入れ、もしリクエストを飛ばしている間にctx.Doneがクローズされたらすぐにリターンします。

Google Web Search APIリクエストはサーチクエリとユーザーIPをクエリパラメータに含みます:

func Search(ctx context.Context, query string) (Results, error) {
    // Prepare the Google Search API request.
    req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
    if err != nil {
        return nil, err
    }
    q := req.URL.Query()
    q.Set("q", query)

    // If ctx is carrying the user IP address, forward it to the server.
    // Google APIs use the user IP to distinguish server-initiated requests
    // from end-user requests.
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Searchはヘルパー関数httpDoを使ってHTTPリクエストを発行し、もしリクエストかレスポンスが処理中にctx.Doneがクローズされたらキャンセルします。SearchはHTTPレスポンスを処理するクロージャをhttpDoハンドルに渡します:

    var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        // Parse the JSON search result.
        // https://developers.google.com/web-search/docs/#fonje
        var data struct {
            ResponseData struct {
                Results []struct {
                    TitleNoFormatting string
                    URL               string
                }
            }
        }
        if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
            return err
        }
        for _, res := range data.ResponseData.Results {
            results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
        }
        return nil
    })
    // httpDo waits for the closure we provided to return, so it's safe to
    // read results here.
    return results, err

httpDo関数は新しいゴルーチンの中でHTTPリクエストを実行してレスポンスを処理します。もしゴルーチンがexitする前にctx.Doneがクローズしたらキャンセルします。

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

コードをContextに適合する

多くのサーバーフレームワークがリクエストスコープな値を運ぶためのパッケージと型を提供しています。私たちは、既存のフレームワークとContextパラメータを期待するコードとの間をブリッジする新しいContextインターフェイスの実装を定義する事が出来ます。

例えば、Gorillaのgithub.com/gorilla/contextパッケージは、HTTPリクエストからkey-valueペアへのマップを提供する事でハンドラにデータをリクエストに関連づける事を許します。gorilla.goにおいて、私たちはGorillaパッケージ内で特定のHTTPリクエストに関連づけられた値を返すValueメソッドを持つContext実装を提供します。

他のパッケージは、Contextに似たキャンセレーションサポートをサポートしてきました。例えば、TombはDyingチャンネルをcloseすることでキャンセルをシグナルするKillメソッドを提供します。Tomはまたこれらのゴルーチンが終了するのを待つ、sync.WaitGroupに似たメソッドも提供します。tomb.goでは、私たちは親のContextがキャンセルされるか与えられたTombがkillされるかの場合にキャンセルされるContext実装を提供します。

結論

Googleでは、私たちはGoプログラマーにContextパラメーターをリクエストの到着から返却までの間に呼ばれる全ての関数の第一引数にContextパラメーターを渡す事を要求しています。これは、たくさんの異なるチームで開発されたGoのコードに良好な相互運用性をもたらします。タイムアウトとキャンセレーションのシンプルな制御を導入し、セキュリティクレデンシャルのようなクリティカルな値が適切にGoプログラム内を移動する事を確かなものにします。

Context上に構築したいサーバーフレームワークは、それらのパッケージとContextパラメータを期待するものの間をブリッジするContext実装を提供するべきです。そうすればそれらのクライアントライブラリは呼び出しコードからContextを受け入れることでしょう。リクエストスコープなデータとキャンセレーションのための共通インターフェイスを確立する事により、Contextはパッケージ開発者がスケーラブルなサービスを作るためのコードを共有するのを簡単にするのです。

By Sameer Ajmani

(翻訳) Trelloは何が違うのか by Joel Spolsky

最近、rebuild.fm で紹介されていたタスク管理ツールのTrelloを試しに使ってみたんですが、個人的にかなりヒットでした。このTrello、Joel on Softwareで有名なJoel Spolsky氏のサービスだと知って驚いたんですが、氏のTrelloに関するブログエントリーが興味深かったので、著者の許諾を得て翻訳します。


原文:How Trello is different - Joel on Software

Trelloは何が違うのか(How Trello is different)

2012年1月6日 金曜日

つい数ヶ月前、私たちは非常にシンプルなWebベースのチーム・コラボレーション・システムのTrelloをローンチした。まだ始まったばかりの1.0状態だというのに、フィードバックは圧倒的に好感触で、採用の勢いは非常に力強い。

Fog Creekにとって、Trelloは新しい種類の開発プロジェクトだ。100%ホストされており、「インストール・ソフトウェア」バージョンのTrelloというものは決して存在しないだろう。このおかげで、私たちは開発プロセスを多くの面で現代化できた。私たちはTrelloに関わる いかなる部分にもVisual Basicコードが全く含まれていない ことをお知らせ致します。次はなんだ?空飛ぶ車か?

あなたが(もっぱらソフトウェア開発者向けに投入された過去製品と比較して)気づくであろう最も大きな違いは、Trelloが完全に水平的なプロダクトだということだ。

水平的とは、様々な職業の人々がそれを使えるということだ。ワープロやWebブラウザーは水平的だ。歯医者がドリルであなたを拷問にかけるために使うソフトウェアは垂直的だ。

垂直的ソフトウェアは成功させやすくお金を儲けやすいので、あなたの最初のスタートアップとしては良い選択だ。これには2つのキーとなる理由がある。

  • 顧客を見つけやすい。もしあなたが歯医者ソフトを作るのなら、あなたはどの集会に出かけて、どの雑誌に広告を打てばいいのか知っている。ただ歯医者を見つけさえすればいいのだ。
  • 利ざやが良い。あなたのユーザーは仕事のプロフェッショナルで、あなたが彼らの問題を解決できるのなら、彼らがあなたにお金を払うのは理にかなっている。

様々な職業の人にとって便利な水平的な製品を作るというのは、成功させるのは殆ど不可能だ。あなたは、開発コストを膨大なユーザー数で償却できる他の水平的プロダクトと競合しているので、あまり高額な料金を請求することはできない。ハイリスク・ハイリターンだ。ゼロから起業したスタートアップには向かないが、Fog Creekのように成熟し安定した起業の2つ目や3つ目の製品のアイデアとしては悪くない。

すまないが、1991年に戻って私がMicrosoft Excelチームにいた時の話をさせてくれ(ああ、確かにきみが当時まだ生まれていなかったのは認めるが、コンピューターは発明されていたんだ。分かったら僕の膝に飛び乗っててじっとしていてくれ)。

誰しも、Excelを財務モデリングアプリケーションだと考えていた。これは式やそのようなものを使って、計算モデルを作り出すために使われていた。例えば、"もし金利が来年0.00001%増加するなら、ラスベガスの住宅所有者の何パーセントが破産するか?"のような計算をするものだと思っておいてほしい。

1993年前後、私たちのうち二、三人が、人々がどのようにExcelを使っているかを知るために顧客のもとに出かけた。

私たちはそこで、高度に統制された巨大な公共事業の"今週の負傷者数"スプレッドシートを管理することだけが仕事だという男を発見した。

彼は週に一度、施設名と、その週に負傷者が0であったという意味の数字の0を含む、十の施設が列挙されたExcelスプレッドシートを開いた(負傷者が出たことは一度もなかった)。

彼は今日の日付をスプレッドシートの一番上に入力し、印刷し、三穴バインダーに閉じ、そしてそれが彼の、まさに仕事のすべてだった。少し悲しいことだった。彼は一日二度の昼休みを取っていた。私だってもしそれが私の仕事の全てだったらそうしただろう。

次の二週間、私たちは数十のExcelの顧客の元を訪れたが、Excelを実際に「計算」と呼べるもののために使っている人間はどこにもいなかった。彼らのほとんど全員は、表を作るのに便利だからExcelを使っていたのだ。

(無関係な注:私たちが見つけた数少ない「計算」をしていた顧客は銀行で、彼らは「デリバティブ」と呼ばれる爆破装置を考えだしていた。彼らは10年のうち9年は銀行家のボーナスを最大化し、10年に一度西洋文明に崩壊を引き起こしかけるためにExcelを使っていた。ブラック・スワンに関する何かだ。多分、浮動小数点の丸め誤差だろう)

 何の話をしてたんだっけ?そうそう、ほとんどの人々がExcelをリストを作るために使ってたということだ。突如として私たちは、Excelを時代遅れにせんとした、上等な未来的スプレッドシートであるLotus Improvがなぜ完全に失敗したのかを完全に理解した。あれは計算が大得意だったが、表の作成はひどく下手で、そしてみんなはExcelを計算ではなく表のために使っていたからだ。

ビンゴ!頭のなかで明かりが灯った。

素晴らしい水平的キラーアプリケーションとは、実際には着飾ったデータ構造に過ぎないのだ。

スプレッドシートは単にwhat-if分析をするための道具ではない。これらは特定のデータ構造を提供する。表だ。ほとんどのExcelユーザーは式を入力しない。彼らは、表が必要な時にExcelを使う。再計算などではなく、罫線こそがExcelの最重要機能だったのだ。

ワープロは単に本やレポートや手紙を書く道具ではない。これらは特定のデータ構造を提供する。自動的に折り返されてページに分割されるテキストの列だ。

パワーポイントは単に退屈な会議を作るためのツールではない。これは特定のデータ構造を提供する。フルスクリーン画像の配列だ。

Trelloを見て「お、カンバンボードじゃないか。アジャイルにソフトウェアを開発するためのものだろ」という人もいる。ああ、そのとおりだとも。でも結婚式の計画にも使えるし、休暇に出かける場所の候補を家族にシェアするのにも使えるし、求人の応募の経緯を記録するのにも使えるし、他の無数の用途にも使える。実際のところ、Trelloはあなたが人々のグループとともにリストのリストを管理したいと思う全ての場所で使えるのだ。

そのような種類のデータ構造を必要とするものは無数にあり、そしてTrello以前に素晴らしい「リストのリスト」アプリケーションは存在していなかった(アウトラインプロセッサはあったが、あれは私に言わせればUIデザインの袋小路だ。だからプログラマーには魅力的だが、一般人には役に立たない)。

いちどTrelloに足を踏み入れれば、あなたは全てのことにこれを使うだろう。私は常に約30個のTrelloボードを使い、それらをいままでみんなと使ってきた。年老いた両親から、ともに休暇を計画する人と、仕事上のすべてのチームと、私が関わっているほとんど全てのプロジェクトで。

だから、オーケー、これがTrelloの最初の大きな違いだ:垂直的ではなく、水平的であること。しかし他にもたくさんの違いがある。

継続的にデリバリーされている。 メジャーリリースとマイナーリリースがあるというよりも、私たちはほとんど単に、新しい機能を開発者のもとから顧客に押し出すだけだ。あなたが開発してテストして、しかし次のメジャーリリースを待っているがために届けられていない機能というのは、在庫になる。在庫は重荷だ。あなたが費やしたお金は、売上を上げることもなくどんどん無駄になってゆく。たしかに、100年前には私たちも「CD-ROM」と呼ばれるものを持っていて、そういう方法でソフトウェアを出荷していた。だから私たちは世界中の顧客にそれを押し付けるまで機能を一箇所に集めておく経済的な理由があった。だが、もはやそういう方法で仕事をする理由は存在しない。もちろんもう知っているだろう。ちょっと言ってみるだけだがー5分前にVisual Basicを使うのを止めたんだ。「すばらしい新世界」だ。

リリース前に徹底的なテストをしていない。 私たちはそれでもなんとかなると考えた。なぜならTrelloは無料で、顧客はより寛容だからだ。だが本当のことをいうと、本当の理由はバグは数ヶ月ではなく数時間で修正されるので、「人々が経験するバグ」の数は少ないからだ。

取り組みを公開している。 Trelloチームのルールは「デフォルトで公開」だ。私たちが現在取り組んでいることとこれから取り組むものを全て一覧できる公開のTrelloボードがある。私たちはこれを、顧客にお気に入りの機能に投票とコメントをしてもらうために使っている。ところでTrelloの開発中には、これは秘密だった。開発チームがリーン・スタートアップの原則を使えるように、私たちはカスタマーフィードバックをくれるたくさんのベータテスターを抱えていたが、バージョン1.0をつくり上げるために費やしたその秘密の9ヶ月は、我々に競争市場で大幅なリードをもたらした。だた今や私たちは製品を出荷したので、私たちの計画を喋らない理由はない。

”ベンとジェリーの”ではなく、”高速に成長する”製品である。 Strategy Letter Iを読んでくれ。Trelloのビジネスゴールは、究極的には10億人のユーザーを獲得することだ。これは、私たちの最優先事項が、採用を妨げるいかなる障害も取り除くことであると意味している。人々がTrelloを使わない理由に挙げそうな全ての事柄を発見し、排除する必要がある。例えば:

Trelloは無料である。 製品に料金を請求することでおきる摩擦は、大規模な成長のための最大の障害だ。長い目で見れば、少数のユーザーから大量のお金を取り出す方法より、多数のユーザーから少量のお金を取り出す方法を見つける方がずっと簡単だと私は思う。ひとたび10億人のユーザーを獲得すれば、その中の誰があなたの作った製品から最大の価値を得ているか見つけるのは簡単だ。その最大価値を受けている人は、あなたに喜んでお金を払うだろう。他の者はそれほどサポートコストがかからない。

APIとプラグインアーキテクチャが最優先事項である。 他の言い方で言うとこうだ:もしそれが基本的なAPIを公開して高価値なユーザー(あなたのプラットフォームから最大価値を受け取っている人たち)が私たちのために作ってくれるものなら、決して自分たちで作るな。Trelloチームでは、プラグインで提供できる機能は全てプラグインで提供しなくてはならない。

APIは現在とても原始的な形をしている。既にそれを使ってなにか面白いことができるが、現在鋭意開発中だ。)

最先端の技術を使っている。 そのせいで私たちはたまに自分の指を切ってしまうのだが。私たちの開発者は、MongoDB, WebSockets, CoffeeScript, そしてNodeに血を流している。だが少なくとも、彼らは楽しんでいる。それに今日の厳しいジョブマーケットでは、偉大な開発者は彼らが何に取り組むかについて、大きな支配権を持っている。あなたが彼らに、何百万人もの人々を感動させるエキサイティングな製品を与え、彼らが単純な仕組みがうまくいっていない理由を探り当てようとしている間にTCP-IPの奥深くまで潜らせることができるなら、彼らは楽しみ、仕事を愛するだろう。それに、私たちは次の10年に取り組むプロダクトを作っているのだ。今日の単なる「最新鋭」の技術は、五年後には古びて軋みが生じるだろう。私たちは「最新式」をほんの少しだけ上回りたい。これは計算済みのリスクなのだ。

過激すぎるものはひとつもない。要するに、Fog Creek Softwareは全てのY Combinatorのスタートアップが、spezが真夜中にLispが壊れた時にRedditをリブートするためにラップトップを抱いて寝ていた頃から使ってきたテクニックでインターネット製品を作る。もしまだTrelloを試していないなら試してみてくれ。うまくいったら、私にTwitterで教えてくれ

Joel on Software

Joel on Software

More Joel on Software

More Joel on Software