2020/02/25


執筆者様に Twitter でお声掛け頂き、発売前ながら献本頂く事になりました。執筆された森下様、送付頂いた技術評論社様、ありがとうございました。

Visual Studio Code は登場から色々な機能を取り込みつつ着実にユーザを増やし、統合開発環境としては今や飛ぶ鳥を落とす勢いになった言って良いでしょう。以下は 2019 年の Stackoverflow Survey で公開された有名な開発環境の調査結果です。

StackOverflow Survey 2019

僕は普段は Vim というテキストエディタを使っていますが、実は僕は色々なテキストエディタを試します。Emacs も人並み程度使えますし、Visual Studio Code も拡張を自分で書いた事がある程度には使っています。

Search results - mattn | Visual Studio Code , Visual Studio Marketplace

...

https://marketplace.visualstudio.com/search?term=mattn&target=VSCode&category=All%20categories&sortBy=Relevance

正直に言うと、Visual Studio Code が登場後しばらく経ってから端末表示機能を実装した時に、少しだけ悔しくて同時期に Vim の作者 Bram Moolenaar 氏が「端末機能を足そう」と言い出した時には誰よりも早く、そして Bram Moolenaar 氏による UNIX の実装よりも早く Windows のパッチを送った事を覚えています。そして個人的には今も尚、Visual Studio Code は良い目標の1つだと思っています。

本書は Visual Studio Code を知らない開発者が Visual Studio Code を使いこなせる様になるまでに、机の片隅に置いておくと便利な一冊となるでしょう。各ツールバーやコマンドパレット、Visual Studio Code での Git の扱い方、デバッガの使い方、拡張のインストールや拡張の作り方、Visual Studio Code を使った TypeScript 開発の実例を交えた説明、Go 開発の実例を交えた説明、多くの方がマッチしそうな内容になっています。本書を片手に写経しながら読み進めたならば、おそらく Visual Studio Code をあまり触った事が無かった方でも読み終わる頃には使いこなせる様になっているのではないかと思います。

僕は Visual Studio Code が登場した当初から Visual Studio Code を使っていますが、それでも知らない内容が沢山書かれていました。Visual Studio Code には自分が扱う言語拡張しかインストールしていなかったので、Visual Studio Code が扱えるデバッガ多さを紹介した画面キャプチャには驚きました。またデバッガの設定情報が書かれている launch.json で console や useWSL といった項目があり、それらを使ってもっと便利にデバッグできる事も知りました。とても勉強になりました。

一番の収穫は、特定のワークスペースでのみ拡張を有効にする為の設定でした。この辺りを知りたい方は、ぜひ購読してみるのが良いと思います。

本書で1点、惜しいなと思う事をあえて言うとすれば索引が若干少ない所だと思います。400ページを超えるこれだけのボリュームで内容も程よく濃いので、後から読み返したくなる事は間違いなくあると思います。そんな時に索引から読みたい箇所に戻れるととても便利だろうなと思いました。個人的に特に良かったなと思ったのが Visual Studio Code を使っての todo アプリ開発を TypeScript と Go の両方で実践している章でした。これらはこれから実務で統一的に Visual Studio Code を使って行こうとされておられる企業の方々には良い教科書になりえると思いました。

ところで最近、Vim の Language Server 事情も少し変化があり、vim-lsp-settings という、Language Server Client である vim-lsp のサポートを行うプラグインを作っています。多くの Language Server を Visual Studio Code 並みに簡単にインストールする事ができる様になっています(coc よりも多いです)。直近では Visual Studio Code の様にプロジェクト専用設定を行う事もできる様になったので、ワークスペース専用にデータベース接続先を設定する事も可能です。lighttiger2505 さんが開発しておられる SQL の Language Server、sqls も簡単にインストールする事ができます。さらに efm-langserver という汎用の Language Server との連携も便利です。tsuyoshicho さんが作ってくれている vim-efm-langserver-settings を入れるとインストールされているツール類を linter として多くのフォーマットのエラーをチェックする事が出来る様になっています。こういった Language Server に関するアイデアはエディタの垣根を越えて共有される、とてもフェアな良い技術の進歩であり僕もやっていて楽しいです。これからも Visual Studio Code に追いつける様に頑張っていきたいと思います。

今後も、Visual Studio Code と気持ちよい技術的な競争ができるといいなと本書を読んで改めて思いました。

Posted at by



2020/02/21


Go 言語は struct のレシーバがポインタの場合は実体であってもポインタの場合であっても呼び出せるので、もし struct が参照カウントに従い動作する様な場合は実体でコピーされてしまっては困る場合があります。例えば以下の様なインタフェースを考えます。

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

type foo struct {
    n int64
    q chan struct{}
}

func (f *foo) Add() {
    if atomic.AddInt64(&f.n, 1) == 1 {
        f.q = make(chan struct{})
    }
}

func (f *foo) Done() {
    if atomic.AddInt64(&f.n, -1) == 0 {
        f.q <- struct{}{}
    }
}

func (f *foo) Watch() {
    <-f.q
}

func main() {
    var f foo

    f.Add()
    f.Add()
    f.Add()
    go func() {
        fmt.Println("いーち!")
        time.Sleep(time.Second)
        f.Done()
        fmt.Println("にー!")
        time.Sleep(time.Second)
        f.Done()
        fmt.Println("さーん!")
        time.Sleep(time.Second)
        f.Done()
    }()

    f.Watch()
    fmt.Println("ダーッ!")
}

このコードは main の中だけで動く場合には機嫌良く動きます。次にこの処理を分散してみたい考えてみます。関数 doSomething1 と doSomething2 に foo を引数で渡します。

func doSomething1(f foo) {
    time.Sleep(2 * time.Second)
    fmt.Println("さーん!")
    time.Sleep(time.Second)
    f.Done()
}

func doSomething2(f foo) {
    fmt.Println("いーち!")
    time.Sleep(time.Second)
    f.Done()
    fmt.Println("にー!")
    time.Sleep(time.Second)
    f.Done()
}

func main() {
    var f foo

    f.Add()
    f.Add()
    f.Add()
    go doSomething1(f)
    go doSomething2(f)

    f.Watch()
    fmt.Println("ダーッ!")
}

この処理は一見うまく行きそうに見えます。しかし実行するとデッドロックが起きます。

いーち!
にー!
さーん!
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.(*foo).Watch(...)
    C:/Users/mattn/go/src/github.com/mattn/misc/inoki_app/main.go:27
main.main()
    C:/Users/mattn/go/src/github.com/mattn/misc/inoki_app/main.go:55 +0xfd

「しっかり atomic.AddInt64 を使っているのにおかしい」と思うかもしれません。しかし実際は doSomething1 や doSomething2 の引数として foo の実体を渡した際にはコピーが発生してしまいます。参照カウンタである foo.n は両方の関数に 3 が渡り、foo.n が 0 になる事はありません。もちろんこれは引数をポインタにする事で回避できます。

func doSomething1(f *foo) {
    time.Sleep(2 * time.Second)
    fmt.Println("さーん!")
    time.Sleep(time.Second)
    f.Done()
}

func doSomething2(f *foo) {
    fmt.Println("いーち!")
    time.Sleep(time.Second)
    f.Done()
    fmt.Println("にー!")
    time.Sleep(time.Second)
    f.Done()
}

func main() {
    var f foo

    f.Add()
    f.Add()
    f.Add()
    go doSomething1(&f)
    go doSomething2(&f)

    f.Watch()
    fmt.Println("ダーッ!")
}

こういった struct をライブラリとして提供したい場合、使い手側に「ポインタで使って欲しい」と示す事ができないと、いくらでもバグが発生してしまいます。そこで使うテクニックが noCopy です。Go 言語を知っていてここまで読んだ方であれば、これが何かに似ていると気付いたはずです。そう sync.WaitGroup です。sync.WaitGroup も実体で引数に渡すとデッドロックが発生します。sync.WaitGroup の場合は以下のテクニックを使っています。

type WaitGroup struct {
    noCopy noCopy

    // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
    // 64-bit atomic operations require 64-bit alignment, but 32-bit
    // compilers do not ensure it. So we allocate 12 bytes and then use
    // the aligned 8 bytes in them as state, and the other 4 as storage
    // for the sema.
    state1 [3]uint32
}

type noCopy struct{}
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

go vet は Go 言語でのお作法の良くない書き方を検出してくれるツールですが、この Lock() と Unlock() を持ったインタフェースを実体でコピーしようとすると go vet の copylocks というチェック機能により警告がでる仕組みになっています。

# github.com/mattn/misc/inoki_app
.\main.go:5:21: doSomething passes lock by value: sync.WaitGroup contains sync.noCopy
.\main.go:11:14: call of doSomething copies lock value: sync.WaitGroup contains sync.noCopy

実際に組み込んでみましょう。

package inoki

import (
    "sync/atomic"
)

type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

type Toukon struct {
    noCopy noCopy

    n int64
    q chan struct{}
}

func (f *Toukon) Add() {
    if atomic.AddInt64(&f.n, 1) == 1 {
        f.q = make(chan struct{})
    }
}

func (f *Toukon) Done() {
    if atomic.AddInt64(&f.n, -1) == 0 {
        f.q <- struct{}{}
    }
}

func (f *Toukon) Watch() {
    <-f.q
}

言語仕様上、禁止する事はできないのでコンパイルは出来てしまいますが、go vet を使う IDE 等ではちゃんと警告がでる様になっています。

ダー!

便利なテクニックなので使ってみてみるといいと思います。

Posted at by



2020/02/10


vim-jp の Slack で「zsh の PATH 環境変数に相対パスを含んでいる場合、補完ができないけど意図的か」という話題が出たので調べてみた。

補完できない様にしているのはこの変更

39104: do not hash relative paths in findcmd() · zsh-users/zsh@b312abc
https://github.com/zsh-users/zsh/commit/b312abc93b3b8eae8feb4a9884b22f519a137c7f

結構古い変更。この変更が行われた理由を追ってみた所、メーリングリストでこの会話が見つかった。

Running 'type' causes false positive hashed command completion

Zsh Mailing List Archive Messages sorted by: Reverse Date , Date , Thread , Author Running 'type' ca...

http://www.zsh.org/mla/workers/2016/msg01583.html
$ zsh -f
% cd $(mtemp -d)
% touch sudofoo; chmod +x $_
% ./sudo<TAB>
<becomes>
% ./sudofoo <^C>
% type -w ./sudo
./sudo: none
% ./sudo<TAB>
./sudo    sudofoo*

That's wrong because ./sudo does not exist.  However, it's hashed:

% print $commands[./sudo]
/usr/bin/./sudo

To confuse matters further, even though "./sudo" is hashed, a subsequent
'type -w ./sudo' will print "none", because the hash node lacks the
HASHED bit in its .flags and the PATH_DIRS option is unset by default.

/usr/bin 対しては ./sudo が存在する為、sudofoo に対する ./sudo からの補完候補に sudo が出てきてしまう、これは混乱を生んでしまう」という物だった。これを回避する為に上記の変更で相対パスはハッシュしない様にしている。ちなみに bash や fish だと相対パスの中のコマンドも補完された。

相対パス上で sudofoo の一部 sudo が補完されてしまったとして困るのは、その相対パス内に危ないファイルを追加してしまった場合だろうと推測するが、そもそも相対パスを PATH に追加したい要件が僕には見つからなかった。おそらく自動的に node_modules/.bin 内のコマンドを扱える様にしたいといった物だと思う。もし zsh でやりたい人は direnv を使って動的に PATH を追加するのが良いと思う。

Posted at by