htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

git stash の tips

git stash、使ってますか?
作業中の内容を一旦別に追いやることで他のコマンドを阻害しなくなる便利なコマンドです。
git stashの詳細については、Qiitaやら各種ブログやらで説明していらっしゃる先人がたくさんおりますので、そちらにお譲りします。

tl;dr

git stash <subcommand> stash@{<n>} を省略して git stash <subcommand> <n> と書ける。

本文

みなさんご存じの通り、Gitの各種コマンドには様々なサブコマンドがあり、この git stash も例外ではありません。
この内、

  • git stash show
  • git stash apply
  • git stash pop
  • git stash drop
  • git stash branch

は何も引数を与えず実行すると、暗黙的に stash@{0} が採用されたものと見做されます1

で、引数を与える場合、特にstashの番号を与える場合は stash@{2} とか stash@{3} とか、 stash@{n} のフォーマットで書きます。
これ、数タイプではありますが結構面倒くさいですよね。typoの可能性だって出て来ます。

GitはコミットをSHA1のハッシュで管理しています。
多くのGitコマンドはコミットの指定にハッシュ値の入力を要求しますが2、このハッシュ値は現在のリポジトリの中で一意に特定さえできれば、最低4文字まで省略できることになっています。

例えば、リポジトリcaf1234... というコミットがあり、 他に caf1 で始まる他のコミットがなければ、
git checkout caf1 でこのコミットにチェックアウトすることができます。
caf10... というコミットが他にあれば caf1 では候補が2つあることになってしまうので、
一文字追加して git checkout caf12 でチェックアウトできますね3

で、私思いました。ハッシュ値が省略できるなら、もしかしたらstashの指定も省略できるのではないかと。
何気なく git stash drop 1 と入力して実行してみたら、何と、通りました。

ドキュメントを読んでみるとこんな記述があります。

The latest stash you created is stored in refs/stash; older stashes are found in the reflog of this reference and can be named using the usual reflog syntax (e.g. stash@{0} is the most recently created stash, stash@{1} is the one before it, stash@{2.hours.ago} is also possible). Stashes may also be referenced by specifying just the stash index (e.g. the integer n is equivalent to stash@{n}).

太字部分を雑に訳すと「インデックスだけを指定して参照することもできます(例えば nstash@{n} と等価です)」といった感じでしょうか。

恥ずかしながら、今までこの略記を知りませんでした。
いろいろな git stash に言及している記事を読んでもこの略記について触れているものは私の観測範囲にはありませんでした。
意外と知られていないのかな、と思ってこのエントリを書きました。

ちなみに HEAD@ という略記がありますが、これは結構有名ですよね。

ref: Git - git-stash Documentation


  1. ドキュメントを読むと必ずしも stash@{0} ではないかのようなニュアンスで書かれていますが…

  2. またはシンボル(master とか)やタグなど

  3. 一意に定まらない場合はあいまいな候補のリストを表示してくれます。

Visual Studio Code for Windows の Integrated Terminal に MSYS2 の fish を使う

fish、便利です。

当方は未だにWindows 8.1 UpdateなのでWSL1は使えません。
なので Git for Windows SDK をインストールして簡易的なシェル環境を構築してます。

これが pacman があるのでなかなか快適なんですね。
nodeもこれで入れられるのでchocolateyもほぼ不要になります。

Git for Windows の本来のシェルは Git Bash ですが、他のシェルも pacman 経由でインストールできるため、私は fish をインストールしてこれをデフォルトにしています。

本題です。
慣れたシェルを使っていると、あらゆる場面で導入したくなるもので、VSCode の Integrated Terminal にも fish を導入したくなりました。

で、おもむろに Ctrl + , で設定画面出しますね。

{
    "terminal.integrated.shell.windows": "C:/git-sdk-64/usr/bin/fish.exe",
    "terminal.integrated.shellArgs.windows": [
        "-i",
        "--login",
    ],
}

と入力します。2
Ctrl + @ でターミナルを表示するとどうでしょう、エラーの嵐になります。
いろいろ調べた結果、 $PATH がほとんど空っぽになっていることが分かりました。

"C:/git-sdk-64/usr/bin/fish.exe" の代わりに "C:/git-sdk-64/usr/bin/bash.exe" と書いた場合は何の問題もなく起動し、パスも普通に通ってしまうので謎です。

ちなみに bash でうまく行くからと言って

{
    "terminal.integrated.shell.windows": "C:/git-sdk-64/usr/bin/bash.exe",
    "terminal.integrated.shellArgs.windows": [
        "-c",
        "/usr/bin/fish",
    ],
}

としても無駄です。
盛大にエラります。

結構な時間を試行錯誤でアレコレし、

{
    "terminal.integrated.shell.windows": "C:/git-sdk-64/usr/bin/fish.exe",
    "terminal.integrated.shellArgs.windows": [
        "-c",
        "eval 'set -x PATH ~/bin /mingw64/bin /usr/bin $PATH; and /usr/bin/fish'",
    ],
}

と書くことでひとまず解決しました。

fish では変数にスペース区切りで値を与えると配列として格納されます。
$PATH も配列で持っているので、その先頭に強引にパスを与えてしまいます。
fish.exe をコールしている段階では $PATH は無事のようなので、ここで set -x PATH ... としてしまいました。3

VSCode には、シェルに与える環境変数をオーバーライドする terminal.integrated.env.windows という設定値もあるのですが、ここだと ~$PATH が展開されない為、已む無くコマンド評価で与えています。

正直言ってワークアラウンド的でスマートさに欠けるので、もっといい方法があれば是非教えていただきたいですね。
あとなぜ $PATH が消え去るのかの原因も分かる方いらっしゃれば…。


  1. Windows Subsystem for Linux

  2. 余談ですが、実は Windows でもほとんどの場面でパスの区切り文字にバックスラッシュ「\」の代わりにスラッシュ「/」を使っても問題ないです。

  3. fish における set -x FOO barbash で言う export FOO=bar のようなもの。

式途中に null として評価されてしまうケースの回避方法

F# の話題です。
let x = A().B.C.D とあるとき、もしA().Bnull だったら?
F# で定義した型は AllowNullLiteralAttribute 属性を付与するか Unchecked.defaultof<'a> を使用しない限り、たとえ参照型だろうが null になることはありません。
でもCLRのクラスは null の可能性があります。
.NET Framework の資産を使う以上、 NullReferenceException と付き合っていく必要はどうしてもあります。

一番愚直な方法は、いわゆる nullチェックです。

let x =
    match A() with
    | null -> null
    | x ->
        match x.B with
        | null -> null
        | x ->
            match x.C with
            | null -> null
            | x -> x.D

…ツラすぎる。

次、Maybeモナドみたいなコンピュテーション式を使う方法。

[<AutoOpen>]
module Maybe =
    type MaybeBuilder() =
        member __.Bind (x, f) = if x = null then null else f x
        member __.Return x = x
    let maybe = MaybeBuilder()

let x = maybe {
    let! x = A()
    let! x = x.B
    let! x = x.C
    return x.D }

さっきよりは大分マシになりましたね。

まぁこれでいいんですが、もう一歩やってみました。

gist.github.com

クッソ長いんですが、要は評価したい式を引用式として与えることで、途中で null を検出したら全体を null とするってことです。
nullNone、非 nullSome x として見れば、まさにMaybeモナドがやっていることとニアリーイコールの結果を得られるわけです。

Gist のコメントに書いたやつのコピペですが、

open System
open QuotationUtility

let nullUri : Uri = null
let result1 = Quotation.evaluate <@ "not null value" @>
let result2 = Quotation.evaluate <@ (Uri("http://example.com/")).AbsoluteUri @>
let result3 = Quotation.evaluate <@ (null : Uri).AbsoluteUri @>
let result4 = Quotation.evaluate <@ nullUri.AbsoluteUri @>

printfn "1 : [%A] / 2 : [%A] / 3 : [%A] / 4 : [%A]" result1 result2 result3 result4
(* 1 : ["not null value"] / 2 : ["http://example.com/"] / 3 : [<null>] / 4 : [<null>] *)

大体こんな感じになります。
なんとなーく動いてるように見えますね?
上の例で言えば let result = Quotation.evaluate <@ A().B.C.D @> とすることで、仮に A().Bnull だったとしても result には null が束縛されるだけです。もちろん D まで正しく評価できれば D の値が束縛されます。

たぶんバグあるので複雑な式を与えるとエラると思いますが、少なくとも単純な式なら null を検出できているようです。
これを使えばスマートに nullチェックできるんじゃないでしょうか。
リフレクションなので、評価回数が増えてくると速度低下に繋がるところがネックですね。

ところで、F#のシンタックスハイライト、なんか毒々しいですね。

Java5 での開発のつらみをやわらげる tips

仕事で Java5 の案件がきました。
このご時世に Java5 とか負の遺産以外のなにものでもないですが、仕事なので仕方ありません。

拡張for文とか可変長引数とかアノテーションとかジェネリクスとか、それ以前と比べればかなりマシなものの、
現代的なプログラミング言語の数々と比べてしまうとゴミカスだと思います。

何よりツラいのが、高階関数がないこと。
高階関数がないので副作用を抑えるコードを書きづらいのです。
仕方がないのでjava.lang.reflect.Methodクラスを使います。

こんなクラスを書きました。

追記 (2017/11/16 11:23)

長くて記事の本質が見えにくくなってしまうので、Gistに移してリンクにしました。
It provides some functional programming feature to Java5 · GitHub

これはこれで毎度Methodクラスのインスタンスを作らなくてはならないとか型チェックが効かず実行時エラーの恐れがあるとか別のつらみがありますし、
内部でもインスタンス作りまくったりしてて非常にパフォーマンスは悪いと思いますが、
引き換えにちょっとだけ関数型ちっくに書くことができるようになりました。

import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

class Example {
    public static void main(String[] args) {
        // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 の ArrayList
        final List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < 10; ++i) {
            list.add(Integer.valueOf(i));
        }
        
        try {
            final Method isEven = Example.class.getMethod("isEven", int.class);
            final Method stringValueOf = String.class.getMethod("valueOf", int.class);
            final Method println = PrintStream.class.getMethod("println", String.class);

            // メソッドチェイン的な
            new IterableUtils<Integer>(list)          // IterableUtils インスタンスに変換
                .filter(isEven)                       // 2 で割り切れる数のみにフィルタ
                .map(String.class, stringValueOf)     // String 型にマッピング
                .forEach(System.out, println);        // 一つずつ System.out に出力 ==> 0, 2, 4, 6, 8
            
            final Method plus = Example.class.getMethod("plus", int.class, int.class);
            
            // モジュールそのまま呼ぶ感じ
            // 畳み込んで合計
            System.out.println(IterableUtils.reduce(list, 0, plus));  // ==> 45
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static boolean isEven(int i) {
        return i % 2 == 0;
    }
    
    public static int plus(int lhs, int rhs) {
        return lhs + rhs;
    }
}

うん、問題なく動いてるように見えますね。
これでゴテゴテのオブジェクト指向言語に無理矢理関数型パラダイムを持ち込むことができました。

この手のライブラリってどっかにありそうなもんなんですが、探してもそれらしいの見つからなかったんですよね…。

Perl の range operator

これPerlerの皆さまからすれば「何を今更」な話かもしれませんが、個人的に驚いたので書きます。

Perlには範囲演算子というのがあって、他の言語でも度々採用されてる便利な演算子なんですが、
例えば

my @nums = (1..5);

とすることで 1 から 5 までの連番を含む配列を定義できます。

単純な文字列なら、

my @strings = ("aa".."ae");
print "@strings\n";          #==> aa ab ac ad ae

とかもできますね。

さて、この2例目の("aa".."ae")みたいな文字列の範囲を配列にするにあたって、

# これは期待通り
my @arr1 = ("A".."Z"); #==> A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

# これも期待通り
my @arr2 = ("a".."z"); #==> a b c d e f g h i j k l m n o p q r s t u v w x y z

# じゃあこれは…?
my @arr3 = ("A".."z");

私は3つ目の出力はA B C ... X Y Z [ \ ] ^ _ ` a b c ... x y zとなると期待しました。
なぜなら私の知る他の言語では同じように組むとそういう結果になるからです。

でもPerlではA B C D E F G H I J K L M N O P Q R S T U V W X Y Zでした。
期待する結果を得るには

my @arr4 = map {chr} (ord "A"..ord "z");

とする必要があったようです。

理由について多少なり調べてみましたが、どうも納得のいく結論には達しませんでした。
「そういう仕様だから」ということで終わりにします。